From 41fd114a5e7cbaa5ce2f1c3dc66281cab7b75c6b Mon Sep 17 00:00:00 2001 From: electricbubble Date: Wed, 14 Apr 2021 10:03:01 +0800 Subject: [PATCH] first commit --- LICENSE | 21 + README.md | 102 +++ afc.go | 520 +++++++++++++++ afc_test.go | 97 +++ device.go | 607 +++++++++++++++++ device_test.go | 95 +++ go.mod | 8 + go.sum | 14 + housearrest.go | 57 ++ housearrest_test.go | 58 ++ idevice.go | 345 ++++++++++ imagemounter.go | 141 ++++ imagemounter_test.go | 49 ++ installationproxy.go | 96 +++ installationproxy_test.go | 67 ++ instruments.go | 284 ++++++++ instruments_test.go | 94 +++ lockdown.go | 622 ++++++++++++++++++ lockdown_test.go | 43 ++ pkg/libimobiledevice/afc.go | 105 +++ pkg/libimobiledevice/afcmessage.go | 187 ++++++ pkg/libimobiledevice/auxbuffer.go | 137 ++++ pkg/libimobiledevice/client_dtxmessage.go | 380 +++++++++++ pkg/libimobiledevice/client_servicepacket.go | 80 +++ pkg/libimobiledevice/housearrest.go | 56 ++ pkg/libimobiledevice/imagemounter.go | 107 +++ pkg/libimobiledevice/installationproxy.go | 79 +++ pkg/libimobiledevice/instruments.go | 41 ++ pkg/libimobiledevice/keyedarchiver.go | 251 +++++++ pkg/libimobiledevice/lib.go | 29 + pkg/libimobiledevice/lockdown.go | 183 ++++++ pkg/libimobiledevice/packet_afc.go | 86 +++ pkg/libimobiledevice/packet_dtxmessage.go | 201 ++++++ pkg/libimobiledevice/packet_location.go | 53 ++ pkg/libimobiledevice/packet_service.go | 46 ++ pkg/libimobiledevice/packet_usbmux.go | 111 ++++ pkg/libimobiledevice/screenshot.go | 52 ++ pkg/libimobiledevice/simulatelocation.go | 128 ++++ pkg/libimobiledevice/testmanagerd.go | 45 ++ pkg/libimobiledevice/usbmux.go | 423 ++++++++++++ pkg/nskeyedarchiver/nsarray.go | 37 ++ pkg/nskeyedarchiver/nsarray_test.go | 18 + pkg/nskeyedarchiver/nsdictionary.go | 44 ++ pkg/nskeyedarchiver/nsdictionary_test.go | 18 + pkg/nskeyedarchiver/nskeyedarchiver.go | 79 +++ pkg/nskeyedarchiver/nskeyedarchiver_test.go | 38 ++ pkg/nskeyedarchiver/nsnull.go | 28 + pkg/nskeyedarchiver/nsurl.go | 37 ++ pkg/nskeyedarchiver/nsurl_test.go | 13 + pkg/nskeyedarchiver/nsuuid.go | 50 ++ pkg/nskeyedarchiver/nsuuid_test.go | 14 + pkg/nskeyedarchiver/xctcapabilities.go | 10 + pkg/nskeyedarchiver/xctestconfiguration.go | 88 +++ .../xctestconfiguration_test.go | 14 + screenshot.go | 118 ++++ screenshot_test.go | 56 ++ simulatelocation.go | 27 + simulatelocation_test.go | 48 ++ testmanagerd.go | 49 ++ usbmux.go | 148 +++++ usbmux_test.go | 67 ++ xctestmanagerdaemon.go | 152 +++++ 62 files changed, 7253 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 afc.go create mode 100644 afc_test.go create mode 100644 device.go create mode 100644 device_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 housearrest.go create mode 100644 housearrest_test.go create mode 100644 idevice.go create mode 100644 imagemounter.go create mode 100644 imagemounter_test.go create mode 100644 installationproxy.go create mode 100644 installationproxy_test.go create mode 100644 instruments.go create mode 100644 instruments_test.go create mode 100644 lockdown.go create mode 100644 lockdown_test.go create mode 100644 pkg/libimobiledevice/afc.go create mode 100644 pkg/libimobiledevice/afcmessage.go create mode 100644 pkg/libimobiledevice/auxbuffer.go create mode 100644 pkg/libimobiledevice/client_dtxmessage.go create mode 100644 pkg/libimobiledevice/client_servicepacket.go create mode 100644 pkg/libimobiledevice/housearrest.go create mode 100644 pkg/libimobiledevice/imagemounter.go create mode 100644 pkg/libimobiledevice/installationproxy.go create mode 100644 pkg/libimobiledevice/instruments.go create mode 100644 pkg/libimobiledevice/keyedarchiver.go create mode 100644 pkg/libimobiledevice/lib.go create mode 100644 pkg/libimobiledevice/lockdown.go create mode 100644 pkg/libimobiledevice/packet_afc.go create mode 100644 pkg/libimobiledevice/packet_dtxmessage.go create mode 100644 pkg/libimobiledevice/packet_location.go create mode 100644 pkg/libimobiledevice/packet_service.go create mode 100644 pkg/libimobiledevice/packet_usbmux.go create mode 100644 pkg/libimobiledevice/screenshot.go create mode 100644 pkg/libimobiledevice/simulatelocation.go create mode 100644 pkg/libimobiledevice/testmanagerd.go create mode 100644 pkg/libimobiledevice/usbmux.go create mode 100644 pkg/nskeyedarchiver/nsarray.go create mode 100644 pkg/nskeyedarchiver/nsarray_test.go create mode 100644 pkg/nskeyedarchiver/nsdictionary.go create mode 100644 pkg/nskeyedarchiver/nsdictionary_test.go create mode 100644 pkg/nskeyedarchiver/nskeyedarchiver.go create mode 100644 pkg/nskeyedarchiver/nskeyedarchiver_test.go create mode 100644 pkg/nskeyedarchiver/nsnull.go create mode 100644 pkg/nskeyedarchiver/nsurl.go create mode 100644 pkg/nskeyedarchiver/nsurl_test.go create mode 100644 pkg/nskeyedarchiver/nsuuid.go create mode 100644 pkg/nskeyedarchiver/nsuuid_test.go create mode 100644 pkg/nskeyedarchiver/xctcapabilities.go create mode 100644 pkg/nskeyedarchiver/xctestconfiguration.go create mode 100644 pkg/nskeyedarchiver/xctestconfiguration_test.go create mode 100644 screenshot.go create mode 100644 screenshot_test.go create mode 100644 simulatelocation.go create mode 100644 simulatelocation_test.go create mode 100644 testmanagerd.go create mode 100644 usbmux.go create mode 100644 usbmux_test.go create mode 100644 xctestmanagerdaemon.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..31c8533 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 electricbubble + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f26de30 --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# Golang-iDevice + +## Installation + +```shell script +go get github.com/electricbubble/gidevice +``` + +#### Devices + +```go +package main + +import ( + giDevice "github.com/electricbubble/gidevice" + "log" +) + +func main() { + usbmux, err := giDevice.NewUsbmux() + if err != nil { + log.Fatalln(err) + } + + devices, err := usbmux.Devices() + if err != nil { + log.Fatal(err) + } + + for _, dev := range devices { + log.Println(dev.Properties().SerialNumber, dev.Properties().ProductID, dev.Properties().DeviceID) + } +} + +``` + + +#### XCTest + +```go +package main + +import ( + "fmt" + giDevice "github.com/electricbubble/gidevice" + "log" + "sync" + "time" +) + +func main() { + usbmux, err := giDevice.NewUsbmux() + if err != nil { + log.Fatal(err) + } + + devices, err := usbmux.Devices() + if err != nil { + log.Fatal(err) + } + + if len(devices) == 0 { + log.Fatal("No Device") + } + + dev := devices[0] + + out, cancel, err := dev.XCTest("com.leixipaopao.WebDriverAgentRunner.xctrunner") + if err != nil { + log.Fatal(err) + } + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + for s := range out { + fmt.Print(s) + } + }() + + go func() { + time.Sleep(10 * time.Second) + cancel() + log.Println("DONE") + wg.Done() + }() + + wg.Wait() +} + +``` + +## Thanks + +| |About| +|---|---| +|[libimobiledevice/libimobiledevice](https://github.com/libimobiledevice/libimobiledevice)|A cross-platform protocol library to communicate with iOS devices| +|[anonymous5l/iConsole](https://github.com/anonymous5l/iConsole)|iOS usbmuxd communication impl iTunes protocol| +|[alibaba/taobao-iphone-device](https://github.com/alibaba/taobao-iphone-device)|tidevice can be used to communicate with iPhone device| + +Thank you [JetBrains](https://www.jetbrains.com/?from=gwda) for providing free open source licenses diff --git a/afc.go b/afc.go new file mode 100644 index 0000000..0b468d2 --- /dev/null +++ b/afc.go @@ -0,0 +1,520 @@ +package giDevice + +import ( + "bytes" + "encoding/binary" + "fmt" + "github.com/electricbubble/gidevice/pkg/libimobiledevice" + "io" + "path" + "strconv" + "time" +) + +var _ Afc = (*afc)(nil) + +func newAfc(client *libimobiledevice.AfcClient) *afc { + return &afc{client: client} +} + +type afc struct { + client *libimobiledevice.AfcClient +} + +func (c *afc) DiskInfo() (info *AfcDiskInfo, err error) { + if err = c.client.Send(libimobiledevice.AfcOperationGetDeviceInfo, nil, nil); err != nil { + return nil, fmt.Errorf("afc send 'DiskInfo': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return nil, fmt.Errorf("afc receive 'DiskInfo': %w", err) + } + + m := respMsg.Map() + info = &AfcDiskInfo{ + Model: m["Model"], + } + if info.TotalBytes, err = strconv.ParseUint(m["FSTotalBytes"], 10, 64); err != nil { + return nil, fmt.Errorf("afc 'DiskInfo': %w", err) + } + if info.FreeBytes, err = strconv.ParseUint(m["FSFreeBytes"], 10, 64); err != nil { + return nil, fmt.Errorf("afc 'DiskInfo': %w", err) + } + if info.BlockSize, err = strconv.ParseUint(m["FSBlockSize"], 10, 64); err != nil { + return nil, fmt.Errorf("afc 'DiskInfo': %w", err) + } + + return +} + +func (c *afc) ReadDir(dirname string) (names []string, err error) { + if err = c.client.Send(libimobiledevice.AfcOperationReadDir, toCString(dirname), nil); err != nil { + return nil, fmt.Errorf("afc send 'ReadDir': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return nil, fmt.Errorf("afc receive 'ReadDir': %w", err) + } + + names = respMsg.Strings() + return +} + +func (c *afc) Stat(filename string) (info *AfcFileInfo, err error) { + if err = c.client.Send(libimobiledevice.AfcOperationGetFileInfo, toCString(filename), nil); err != nil { + return nil, fmt.Errorf("afc send 'Stat': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return nil, fmt.Errorf("afc receive 'Stat': %w", err) + } + + m := respMsg.Map() + info = &AfcFileInfo{ + source: m, + name: path.Base(filename), + ifmt: m["st_ifmt"], + } + if info.creationTime, err = strconv.ParseUint(m["st_birthtime"], 10, 64); err != nil { + return nil, fmt.Errorf("afc 'Stat': %w", err) + } + if info.blocks, err = strconv.ParseUint(m["st_blocks"], 10, 64); err != nil { + return nil, fmt.Errorf("afc 'Stat': %w", err) + } + if info.modTime, err = strconv.ParseUint(m["st_mtime"], 10, 64); err != nil { + return nil, fmt.Errorf("afc 'Stat': %w", err) + } + if info.nlink, err = strconv.ParseUint(m["st_nlink"], 10, 64); err != nil { + return nil, fmt.Errorf("afc 'Stat': %w", err) + } + if info.size, err = strconv.ParseUint(m["st_size"], 10, 64); err != nil { + return nil, fmt.Errorf("afc 'Stat': %w", err) + } + + return +} + +func (c *afc) Open(filename string, mode AfcFileMode) (file *AfcFile, err error) { + buf := new(bytes.Buffer) + if err = binary.Write(buf, binary.LittleEndian, uint64(mode)); err != nil { + return nil, fmt.Errorf("afc send 'Open': %w", err) + } + buf.Write(toCString(filename)) + + if err = c.client.Send(libimobiledevice.AfcOperationFileOpen, buf.Bytes(), nil); err != nil { + return nil, fmt.Errorf("afc send 'Open': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return nil, fmt.Errorf("afc receive 'Open': %w", err) + } + + if respMsg.Operation != libimobiledevice.AfcOperationFileOpenResult { + return nil, fmt.Errorf("afc operation mistake 'Open': '%d'", respMsg.Operation) + } + + file = &AfcFile{ + client: c.client, + fd: respMsg.Uint64(), + } + return +} + +func (c *afc) Remove(filePath string) (err error) { + if err = c.client.Send(libimobiledevice.AfcOperationRemovePath, toCString(filePath), nil); err != nil { + return fmt.Errorf("afc send 'Remove': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return fmt.Errorf("afc receive 'Remove': %w", err) + } + if err = respMsg.Err(); err != nil { + return fmt.Errorf("afc 'Remove': %w", err) + } + + return +} + +func (c *afc) Rename(oldPath string, newPath string) (err error) { + if err = c.client.Send(libimobiledevice.AfcOperationRenamePath, toCString(oldPath, newPath), nil); err != nil { + return fmt.Errorf("afc send 'Rename': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return fmt.Errorf("afc receive 'Rename': %w", err) + } + if err = respMsg.Err(); err != nil { + return fmt.Errorf("afc 'Rename': %w", err) + } + + return +} + +func (c *afc) Mkdir(path string) (err error) { + if err = c.client.Send(libimobiledevice.AfcOperationMakeDir, toCString(path), nil); err != nil { + return fmt.Errorf("afc send 'Mkdir': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return fmt.Errorf("afc receive 'Mkdir': %w", err) + } + if err = respMsg.Err(); err != nil { + return fmt.Errorf("afc 'Mkdir': %w", err) + } + + return +} + +func (c *afc) Link(oldName string, newName string, linkType AfcLinkType) (err error) { + buf := new(bytes.Buffer) + _ = binary.Write(buf, binary.LittleEndian, uint64(linkType)) + buf.Write(toCString(oldName, newName)) + + if err = c.client.Send(libimobiledevice.AfcOperationMakeLink, buf.Bytes(), nil); err != nil { + return fmt.Errorf("afc send 'Link': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return fmt.Errorf("afc receive 'Link': %w", err) + } + if err = respMsg.Err(); err != nil { + return fmt.Errorf("afc 'Link': %w", err) + } + + return +} + +func (c *afc) Truncate(filePath string, size int64) (err error) { + buf := new(bytes.Buffer) + _ = binary.Write(buf, binary.LittleEndian, uint64(size)) + buf.Write(toCString(filePath)) + + if err = c.client.Send(libimobiledevice.AfcOperationTruncateFile, buf.Bytes(), nil); err != nil { + return fmt.Errorf("afc send 'Truncate': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return fmt.Errorf("afc receive 'Truncate': %w", err) + } + if err = respMsg.Err(); err != nil { + return fmt.Errorf("afc 'Truncate': %w", err) + } + + return +} + +func (c *afc) SetFileModTime(filePath string, modTime time.Time) (err error) { + buf := new(bytes.Buffer) + _ = binary.Write(buf, binary.LittleEndian, uint64(modTime.Unix())) + buf.Write(toCString(filePath)) + + if err = c.client.Send(libimobiledevice.AfcOperationSetFileModTime, buf.Bytes(), nil); err != nil { + return fmt.Errorf("afc send 'SetFileModTime': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return fmt.Errorf("afc receive 'SetFileModTime': %w", err) + } + if err = respMsg.Err(); err != nil { + return fmt.Errorf("afc 'SetFileModTime': %w", err) + } + + return +} + +func (c *afc) Hash(filePath string) ([]byte, error) { + var err error + if err = c.client.Send(libimobiledevice.AfcOperationGetFileHash, toCString(filePath), nil); err != nil { + return nil, fmt.Errorf("afc send 'Hash': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return nil, fmt.Errorf("afc receive 'Hash': %w", err) + } + if err = respMsg.Err(); err != nil { + return nil, fmt.Errorf("afc 'Hash': %w", err) + } + + return respMsg.Payload, nil +} + +func (c *afc) HashWithRange(filePath string, start, end uint64) ([]byte, error) { + buf := new(bytes.Buffer) + _ = binary.Write(buf, binary.LittleEndian, start) + _ = binary.Write(buf, binary.LittleEndian, end) + buf.Write(toCString(filePath)) + + var err error + if err = c.client.Send(libimobiledevice.AfcOperationGetFileHashRange, buf.Bytes(), nil); err != nil { + return nil, fmt.Errorf("afc send 'HashWithRange': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return nil, fmt.Errorf("afc receive 'HashWithRange': %w", err) + } + if err = respMsg.Err(); err != nil { + return nil, fmt.Errorf("afc 'HashWithRange': %w", err) + } + + return respMsg.Payload, nil +} + +// RemoveAll since iOS6+ +func (c *afc) RemoveAll(path string) (err error) { + if err = c.client.Send(libimobiledevice.AfcOperationRemovePathAndContents, toCString(path), nil); err != nil { + return fmt.Errorf("afc send 'RemoveAll': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = c.client.Receive(); err != nil { + return fmt.Errorf("afc receive 'RemoveAll': %w", err) + } + if err = respMsg.Err(); err != nil { + return fmt.Errorf("afc 'RemoveAll': %w", err) + } + + return +} + +func toCString(s ...string) []byte { + buf := new(bytes.Buffer) + for _, v := range s { + buf.WriteString(v) + buf.WriteByte(0) + } + return buf.Bytes() +} + +type AfcDiskInfo struct { + Model string + TotalBytes uint64 + FreeBytes uint64 + BlockSize uint64 +} + +type AfcFileInfo struct { + name string + + creationTime uint64 + blocks uint64 + ifmt string + modTime uint64 + nlink uint64 + size uint64 + + source map[string]string +} + +func (f *AfcFileInfo) Name() string { + return f.name +} + +func (f *AfcFileInfo) Size() int64 { + return int64(f.size) +} + +// func (f *AfcFileInfo) Mode() os.FileMode { +// return os.ModeType +// } + +func (f *AfcFileInfo) ModTime() time.Time { + return time.Unix(0, int64(f.modTime)) +} + +func (f *AfcFileInfo) IsDir() bool { + return f.ifmt == "S_IFDIR" +} + +// func (f *AfcFileInfo) Sys() interface{} { +// return f.source +// } + +func (f *AfcFileInfo) CreationTime() time.Time { + return time.Unix(0, int64(f.creationTime)) +} + +// func (f *AfcFileInfo) Blocks() uint64 { +// return f.blocks +// } + +// func (f *AfcFileInfo) Format() string { +// return f.ifmt +// } + +// func (f *AfcFileInfo) Link() uint64 { +// return f.nlink +// } + +// func (f *AfcFileInfo) PhysicalSize(info *AfcDiskInfo) int64 { +// return int64(f.blocks * (info.BlockSize / 8)) +// } + +type AfcFileMode uint32 + +const ( + AfcFileModeRdOnly AfcFileMode = 0x00000001 + AfcFileModeRw AfcFileMode = 0x00000002 + AfcFileModeWrOnly AfcFileMode = 0x00000003 + AfcFileModeWr AfcFileMode = 0x00000004 + AfcFileModeAppend AfcFileMode = 0x00000005 + AfcFileModeRdAppend AfcFileMode = 0x00000006 +) + +type AfcLockType int + +const ( + AfcLockTypeSharedLock AfcLockType = 1 | 4 + AfcLockTypeExclusiveLock AfcLockType = 2 | 4 + AfcLockTypeUnlock AfcLockType = 8 | 4 +) + +type AfcFile struct { + client *libimobiledevice.AfcClient + fd uint64 + reader *bytes.Reader +} + +func (f *AfcFile) op(o ...uint64) []byte { + buf := new(bytes.Buffer) + _ = binary.Write(buf, binary.LittleEndian, f.fd) + + if len(o) == 0 { + return buf.Bytes() + } + + for _, v := range o { + _ = binary.Write(buf, binary.LittleEndian, v) + } + + return buf.Bytes() +} + +func (f *AfcFile) Lock(lockType AfcLockType) (err error) { + if err = f.client.Send(libimobiledevice.AfcOperationFileRefLock, f.op(uint64(lockType)), nil); err != nil { + return fmt.Errorf("afc file send 'Lock': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = f.client.Receive(); err != nil { + return fmt.Errorf("afc file receive 'Lock': %w", err) + } + if err = respMsg.Err(); err != nil { + return fmt.Errorf("afc file 'Lock': %w", err) + } + return +} + +func (f *AfcFile) Unlock() (err error) { + return f.Lock(AfcLockTypeUnlock) +} + +func (f *AfcFile) Read(b []byte) (n int, err error) { + if err = f.client.Send(libimobiledevice.AfcOperationFileRead, f.op(uint64(len(b))), nil); err != nil { + return -1, fmt.Errorf("afc file send 'Read': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = f.client.Receive(); err != nil { + return -1, fmt.Errorf("afc file receive 'Read': %w", err) + } + if err = respMsg.Err(); err != nil { + return -1, fmt.Errorf("afc file 'Read': %w", err) + } + + if respMsg.Payload == nil { + return 0, io.EOF + } + + if f.reader == nil { + f.reader = bytes.NewReader(respMsg.Payload) + } + + return f.reader.Read(b) +} + +func (f *AfcFile) Write(b []byte) (n int, err error) { + if err = f.client.Send(libimobiledevice.AfcOperationFileWrite, f.op(), b); err != nil { + return -1, fmt.Errorf("afc file send 'Write': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = f.client.Receive(); err != nil { + return -1, fmt.Errorf("afc file receive 'Write': %w", err) + } + if err = respMsg.Err(); err != nil { + return -1, fmt.Errorf("afc file 'Write': %w", err) + } + + n = len(b) + return +} + +func (f *AfcFile) Tell() (n uint64, err error) { + if err = f.client.Send(libimobiledevice.AfcOperationFileTell, f.op(), nil); err != nil { + return 0, fmt.Errorf("afc file 'Tell': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = f.client.Receive(); err != nil { + return 0, fmt.Errorf("afc file receive 'Tell': %w", err) + } + if err = respMsg.Err(); err != nil { + return 0, fmt.Errorf("afc file 'Tell': %w", err) + } + + if respMsg.Operation != libimobiledevice.AfcOperationFileTellResult { + return 0, fmt.Errorf("afc operation mistake 'Tell': '%d'", respMsg.Operation) + } + + n = respMsg.Uint64() + return +} + +func (f *AfcFile) Seek(offset int64, whence int) (ret int64, err error) { + if err = f.client.Send(libimobiledevice.AfcOperationFileSeek, f.op(uint64(whence), uint64(offset)), nil); err != nil { + return -1, fmt.Errorf("afc file 'Seek': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = f.client.Receive(); err != nil { + return -1, fmt.Errorf("afc file receive 'Seek': %w", err) + } + if err = respMsg.Err(); err != nil { + return -1, fmt.Errorf("afc file 'Seek': %w", err) + } + + var tell uint64 + if tell, err = f.Tell(); err != nil { + return -1, err + } + + ret = int64(tell) + return +} + +func (f *AfcFile) Truncate(size int64) (err error) { + if err = f.client.Send(libimobiledevice.AfcOperationFileSetSize, f.op(uint64(size)), nil); err != nil { + return fmt.Errorf("afc file 'Truncate': %w", err) + } + var respMsg *libimobiledevice.AfcMessage + if respMsg, err = f.client.Receive(); err != nil { + return fmt.Errorf("afc file receive 'Truncate': %w", err) + } + if err = respMsg.Err(); err != nil { + return fmt.Errorf("afc file 'Truncate': %w", err) + } + + return +} + +func (f *AfcFile) Close() (err error) { + if err = f.client.Send(libimobiledevice.AfcOperationFileClose, f.op(), nil); err != nil { + return fmt.Errorf("afc file 'Close': %w", err) + } + if _, err = f.client.Receive(); err != nil { + return fmt.Errorf("afc file receive 'Close': %w", err) + } + + return +} + +type AfcLinkType int + +const ( + AfcLinkTypeHardLink AfcLinkType = 1 + AfcLinkTypeSymLink AfcLinkType = 2 +) diff --git a/afc_test.go b/afc_test.go new file mode 100644 index 0000000..33999d0 --- /dev/null +++ b/afc_test.go @@ -0,0 +1,97 @@ +package giDevice + +import ( + "fmt" + "io" + "log" + "os" + "testing" +) + +var afcSrv Afc + +func setupAfcSrv(t *testing.T) { + setupLockdownSrv(t) + + var err error + if lockdownSrv, err = dev.lockdownService(); err != nil { + t.Fatal(err) + } + + if afcSrv, err = lockdownSrv.AfcService(); err != nil { + t.Fatal(err) + } +} + +func Test_afc_DiskInfo(t *testing.T) { + setupAfcSrv(t) + + info, err := afcSrv.DiskInfo() + if err != nil { + t.Fatal(err) + } + log.Printf("%10s: %s\n", "Model", info.Model) + log.Printf("%10s: %d\n", "BlockSize", info.BlockSize/8) + log.Printf("%10s: %s\n", "FreeSpace", byteCountDecimal(int64(info.FreeBytes))) + log.Printf("%10s: %s\n", "UsedSpace", byteCountDecimal(int64(info.TotalBytes-info.FreeBytes))) + log.Printf("%10s: %s\n", "TotalSpace", byteCountDecimal(int64(info.TotalBytes))) +} + +func byteCountDecimal(b int64) string { + const unit = 1000 + if b < unit { + return fmt.Sprintf("%dB", b) + } + div, exp := int64(unit), 0 + for n := b / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f%cB", float64(b)/float64(div), "kMGTPE"[exp]) +} + +func Test_afc_ReadDir(t *testing.T) { + setupAfcSrv(t) + + names, err := afcSrv.ReadDir("Downloads") + if err != nil { + t.Fatal(err) + } + for _, name := range names { + t.Log(name) + } +} + +func Test_afc_Stat(t *testing.T) { + setupAfcSrv(t) + + fileInfo, err := afcSrv.Stat("Downloads/downloads.28.sqlitedb") + if err != nil { + t.Fatal(err) + } + t.Log(fileInfo.Name()) + t.Log(fileInfo.IsDir()) + t.Log(fileInfo.CreationTime()) + t.Log(fileInfo.ModTime()) + t.Log(fileInfo.Size()) + t.Log(byteCountDecimal(fileInfo.Size())) +} + +func Test_afc_Open(t *testing.T) { + setupAfcSrv(t) + + afcFile, err := afcSrv.Open("DCIM/105APPLE/IMG_5977.JPEG", AfcFileModeRdOnly) + if err != nil { + t.Fatal(err) + } + + userHomeDir, _ := os.UserHomeDir() + file, err := os.Create(userHomeDir + "/Desktop/tmp.jpeg") + if err != nil { + t.Fatal(err) + } + + if _, err = io.Copy(file, afcFile); err != nil { + t.Fatal(err) + } +} diff --git a/device.go b/device.go new file mode 100644 index 0000000..b48c4d9 --- /dev/null +++ b/device.go @@ -0,0 +1,607 @@ +package giDevice + +import ( + "bytes" + "context" + "fmt" + "github.com/electricbubble/gidevice/pkg/libimobiledevice" + "github.com/electricbubble/gidevice/pkg/nskeyedarchiver" + uuid "github.com/satori/go.uuid" + "howett.net/plist" + "strings" + "time" +) + +const LockdownPort = 62078 + +var _ Device = (*device)(nil) + +func newDevice(client *libimobiledevice.UsbmuxClient, properties DeviceProperties) *device { + return &device{ + umClient: client, + properties: &properties, + } +} + +type device struct { + umClient *libimobiledevice.UsbmuxClient + lockdownClient *libimobiledevice.LockdownClient + + properties *DeviceProperties + + lockdown *lockdown + imageMounter ImageMounter + screenshot Screenshot + simulateLocation SimulateLocation + installationProxy InstallationProxy + instruments Instruments + houseArrest HouseArrest +} + +func (d *device) Properties() DeviceProperties { + return *d.properties +} + +func (d *device) NewConnect(port int) (InnerConn, error) { + newClient, err := libimobiledevice.NewUsbmuxClient() + if err != nil { + return nil, err + } + + var pkt libimobiledevice.Packet + if pkt, err = newClient.NewPlistPacket( + newClient.NewConnectRequest(d.properties.DeviceID, port), + ); err != nil { + return nil, err + } + + if err = newClient.SendPacket(pkt); err != nil { + return nil, err + } + + if _, err = newClient.ReceivePacket(); err != nil { + return nil, err + } + + return newClient.InnerConn(), err +} + +func (d *device) ReadPairRecord() (pairRecord *PairRecord, err error) { + var pkt libimobiledevice.Packet + if pkt, err = d.umClient.NewPlistPacket( + d.umClient.NewReadPairRecordRequest(d.properties.SerialNumber), + ); err != nil { + return nil, err + } + + if err = d.umClient.SendPacket(pkt); err != nil { + return nil, err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = d.umClient.ReceivePacket(); err != nil { + return nil, err + } + + var reply = struct { + Data []byte `plist:"PairRecordData"` + }{} + if err = respPkt.Unmarshal(&reply); err != nil { + return nil, err + } + + var record PairRecord + if _, err = plist.Unmarshal(reply.Data, &record); err != nil { + return nil, err + } + + pairRecord = &record + return +} + +func (d *device) SavePairRecord(pairRecord *PairRecord) (err error) { + var data []byte + if data, err = plist.Marshal(pairRecord, plist.XMLFormat); err != nil { + return err + } + + var pkt libimobiledevice.Packet + if pkt, err = d.umClient.NewPlistPacket( + d.umClient.NewSavePairRecordRequest(d.properties.SerialNumber, d.properties.DeviceID, data), + ); err != nil { + return err + } + + if err = d.umClient.SendPacket(pkt); err != nil { + return err + } + + if _, err = d.umClient.ReceivePacket(); err != nil { + return err + } + + return +} + +func (d *device) DeletePairRecord() (err error) { + var pkt libimobiledevice.Packet + if pkt, err = d.umClient.NewPlistPacket( + d.umClient.NewDeletePairRecordRequest(d.properties.SerialNumber), + ); err != nil { + return err + } + + if err = d.umClient.SendPacket(pkt); err != nil { + return err + } + + if _, err = d.umClient.ReceivePacket(); err != nil { + return err + } + + return +} + +func (d *device) lockdownService() (lockdown Lockdown, err error) { + if d.lockdown != nil { + return d.lockdown, nil + } + + var innerConn InnerConn + if innerConn, err = d.NewConnect(LockdownPort); err != nil { + return nil, err + } + d.lockdownClient = libimobiledevice.NewLockdownClient(innerConn) + d.lockdown = newLockdown(d) + _, err = d.lockdown._getProductVersion() + lockdown = d.lockdown + return +} + +func (d *device) QueryType() (LockdownType, error) { + if _, err := d.lockdownService(); err != nil { + return LockdownType{}, err + } + return d.lockdown.QueryType() +} + +func (d *device) GetValue(domain, key string) (v interface{}, err error) { + if _, err = d.lockdownService(); err != nil { + return nil, err + } + return d.lockdown.GetValue(domain, key) +} + +func (d *device) Pair() (pairRecord *PairRecord, err error) { + if _, err = d.lockdownService(); err != nil { + return nil, err + } + return d.lockdown.Pair() +} + +func (d *device) imageMounterService() (imageMounter ImageMounter, err error) { + if d.imageMounter != nil { + return d.imageMounter, nil + } + if _, err = d.lockdownService(); err != nil { + return nil, err + } + if d.imageMounter, err = d.lockdown.ImageMounterService(); err != nil { + return nil, err + } + imageMounter = d.imageMounter + return +} + +func (d *device) Images(imgType ...string) (imageSignatures [][]byte, err error) { + if _, err = d.imageMounterService(); err != nil { + return nil, err + } + if len(imgType) == 0 { + imgType = []string{"Developer"} + } + return d.imageMounter.Images(imgType[0]) +} + +func (d *device) MountDeveloperDiskImage(dmgPath string, signaturePath string) (err error) { + if _, err = d.imageMounterService(); err != nil { + return err + } + devImgPath := "/private/var/mobile/Media/PublicStaging/staging.dimage" + return d.imageMounter.UploadImageAndMount("Developer", devImgPath, dmgPath, signaturePath) +} + +func (d *device) screenshotService() (screenshot Screenshot, err error) { + if d.screenshot != nil { + return d.screenshot, nil + } + + if _, err = d.lockdownService(); err != nil { + return nil, err + } + if d.screenshot, err = d.lockdown.ScreenshotService(); err != nil { + return nil, err + } + screenshot = d.screenshot + return +} + +func (d *device) Screenshot() (raw *bytes.Buffer, err error) { + if _, err = d.screenshotService(); err != nil { + return nil, err + } + return d.screenshot.Take() +} + +func (d *device) simulateLocationService() (simulateLocation SimulateLocation, err error) { + if d.simulateLocation != nil { + return d.simulateLocation, nil + } + if _, err = d.lockdownService(); err != nil { + return nil, err + } + if d.simulateLocation, err = d.lockdown.SimulateLocationService(); err != nil { + return nil, err + } + simulateLocation = d.simulateLocation + return +} + +func (d *device) SimulateLocationUpdate(longitude float64, latitude float64, coordinateSystem ...CoordinateSystem) (err error) { + if _, err = d.simulateLocationService(); err != nil { + return err + } + return d.simulateLocation.Update(longitude, latitude, coordinateSystem...) +} + +func (d *device) SimulateLocationRecover() (err error) { + if _, err = d.simulateLocationService(); err != nil { + return err + } + return d.simulateLocation.Recover() +} + +func (d *device) installationProxyService() (installationProxy InstallationProxy, err error) { + if d.installationProxy != nil { + return d.installationProxy, nil + } + if _, err = d.lockdownService(); err != nil { + return nil, err + } + if d.installationProxy, err = d.lockdown.InstallationProxyService(); err != nil { + return nil, err + } + installationProxy = d.installationProxy + return +} + +func (d *device) InstallationProxyBrowse(opts ...InstallationProxyOption) (currentList []interface{}, err error) { + if _, err = d.installationProxyService(); err != nil { + return nil, err + } + return d.installationProxy.Browse(opts...) +} + +func (d *device) InstallationProxyLookup(opts ...InstallationProxyOption) (lookupResult interface{}, err error) { + if _, err = d.installationProxyService(); err != nil { + return nil, err + } + return d.installationProxy.Lookup(opts...) +} + +func (d *device) instrumentsService() (instruments Instruments, err error) { + if d.instruments != nil { + return d.instruments, nil + } + if _, err = d.lockdownService(); err != nil { + return nil, err + } + if d.instruments, err = d.lockdown.InstrumentsService(); err != nil { + return nil, err + } + instruments = d.instruments + return +} + +func (d *device) AppLaunch(bundleID string, opts ...AppLaunchOption) (pid int, err error) { + if _, err = d.instrumentsService(); err != nil { + return 0, err + } + return d.instruments.AppLaunch(bundleID, opts...) +} + +func (d *device) AppKill(pid int) (err error) { + if _, err = d.instrumentsService(); err != nil { + return err + } + return d.instruments.AppKill(pid) +} + +func (d *device) AppRunningProcesses() (processes []Process, err error) { + if _, err = d.instrumentsService(); err != nil { + return nil, err + } + return d.instruments.AppRunningProcesses() +} + +func (d *device) AppList(opts ...AppListOption) (apps []Application, err error) { + if _, err = d.instrumentsService(); err != nil { + return nil, err + } + return d.instruments.AppList(opts...) +} + +func (d *device) DeviceInfo() (devInfo *DeviceInfo, err error) { + if _, err = d.instrumentsService(); err != nil { + return nil, err + } + return d.instruments.DeviceInfo() +} + +func (d *device) testmanagerdService() (testmanagerd Testmanagerd, err error) { + if _, err = d.lockdownService(); err != nil { + return nil, err + } + if testmanagerd, err = d.lockdown.TestmanagerdService(); err != nil { + return nil, err + } + return +} +func (d *device) HouseArrestService() (houseArrest HouseArrest, err error) { + if d.houseArrest != nil { + return d.houseArrest, nil + } + if _, err = d.lockdownService(); err != nil { + return nil, err + } + if d.houseArrest, err = d.lockdown.HouseArrestService(); err != nil { + return nil, err + } + houseArrest = d.houseArrest + return +} + +func (d *device) XCTest(bundleID string) (out <-chan string, cancel context.CancelFunc, err error) { + ctx, cancelFunc := context.WithCancel(context.TODO()) + _out := make(chan string) + + xcodeVersion := uint64(30) + + var tmSrv1 Testmanagerd + if tmSrv1, err = d.testmanagerdService(); err != nil { + return _out, cancelFunc, err + } + + var xcTestManager1 XCTestManagerDaemon + if xcTestManager1, err = tmSrv1.newXCTestManagerDaemon(); err != nil { + return _out, cancelFunc, err + } + + var version []int + if version, err = d.lockdown._getProductVersion(); err != nil { + return _out, cancelFunc, err + } + + if DeviceVersion(version...) >= DeviceVersion(11, 0, 0) { + if err = xcTestManager1.initiateControlSession(xcodeVersion); err != nil { + return _out, cancelFunc, err + } + } + + var tmSrv2 Testmanagerd + if tmSrv2, err = d.testmanagerdService(); err != nil { + return _out, cancelFunc, err + } + + var xcTestManager2 XCTestManagerDaemon + if xcTestManager2, err = tmSrv2.newXCTestManagerDaemon(); err != nil { + return _out, cancelFunc, err + } + + xcTestManager2.registerCallback("_XCT_logDebugMessage:", func(m libimobiledevice.DTXMessageResult) { + // more information ( each operation ) + // fmt.Println("###### xcTestManager2 ### -->", m) + if strings.Contains(fmt.Sprintf("%s", m), "Received test runner ready reply with error: (null)") { + // fmt.Println("###### xcTestManager2 ### -->", fmt.Sprintf("%v", m.Aux[0])) + if err = xcTestManager2.startExecutingTestPlan(xcodeVersion); err != nil { + debugLog(fmt.Sprintf("startExecutingTestPlan %d: %s", xcodeVersion, err)) + return + } + } + }) + xcTestManager2.registerCallback("_Unregistered_Golang-iDevice", func(m libimobiledevice.DTXMessageResult) { + // more information + // _XCT_testRunnerReadyWithCapabilities: + // _XCT_didBeginExecutingTestPlan + // _XCT_didBeginInitializingForUITesting + // _XCT_testSuite:didStartAt: + // _XCT_testCase:method:willStartActivity: + // _XCT_testCase:method:didFinishActivity: + // _XCT_testCaseDidStartForTestClass:method: + // fmt.Println("###### xcTestManager2 ### _Unregistered -->", m) + }) + + sessionId := uuid.NewV4() + if err = xcTestManager2.initiateSession(xcodeVersion, nskeyedarchiver.NewNSUUID(sessionId.Bytes())); err != nil { + return _out, cancelFunc, err + } + + if _, err = d.installationProxyService(); err != nil { + return _out, cancelFunc, err + } + + var vResult interface{} + if vResult, err = d.installationProxy.Lookup(WithBundleIDs(bundleID)); err != nil { + return _out, cancelFunc, err + } + + lookupResult := vResult.(map[string]interface{}) + lookupResult = lookupResult[bundleID].(map[string]interface{}) + appContainer := lookupResult["Container"].(string) + appPath := lookupResult["Path"].(string) + + var pathXCTestCfg string + if pathXCTestCfg, err = d._uploadXCTestConfiguration(bundleID, sessionId, lookupResult); err != nil { + return _out, cancelFunc, err + } + + if _, err = d.instrumentsService(); err != nil { + return _out, cancelFunc, err + } + + if err = d.instruments.appProcess(bundleID); err != nil { + return _out, cancelFunc, err + } + + pathXCTestConfiguration := appContainer + pathXCTestCfg + + appEnv := map[string]interface { + }{ + "CA_ASSERT_MAIN_THREAD_TRANSACTIONS": "0", + "CA_DEBUG_TRANSACTIONS": "0", + "DYLD_FRAMEWORK_PATH": appPath + "/Frameworks:", + "DYLD_LIBRARY_PATH": appPath + "/Frameworks", + "NSUnbufferedIO": "YES", + "SQLITE_ENABLE_THREAD_ASSERTIONS": "1", + "WDA_PRODUCT_BUNDLE_IDENTIFIER": "", + "XCTestConfigurationFilePath": pathXCTestConfiguration, // Running tests with active test configuration: + // "XCTestBundlePath": fmt.Sprintf("%s/PlugIns/%s.xctest", appPath, name), // !!! ERROR + // "XCTestSessionIdentifier": sessionId.String(), // !!! ERROR + // "XCTestSessionIdentifier": "", + "XCODE_DBG_XPC_EXCLUSIONS": "com.apple.dt.xctestSymbolicator", + "MJPEG_SERVER_PORT": "", + "USE_PORT": "", + "LLVM_PROFILE_FILE": appContainer + "/tmp/%p.profraw", + } + if DeviceVersion(version...) >= DeviceVersion(11, 0, 0) { + appEnv["DYLD_INSERT_LIBRARIES"] = "/Developer/usr/lib/libMainThreadChecker.dylib" + appEnv["OS_ACTIVITY_DT_MODE"] = "YES" + } + appArgs := []interface{}{ + "-NSTreatUnknownArgumentsAsOpen", "NO", + "-ApplePersistenceIgnoreState", "YES", + } + appOpt := map[string]interface{}{ + "StartSuspendedKey": uint64(0), + } + if DeviceVersion(version...) >= DeviceVersion(12, 0, 0) { + appOpt["ActivateSuspended"] = uint64(1) + } + + d.instruments.registerCallback("outputReceived:fromProcess:atTime:", func(m libimobiledevice.DTXMessageResult) { + // fmt.Println("###### instruments ### -->", m.Aux[0]) + _out <- fmt.Sprintf("%s", m.Aux[0]) + }) + + var pid int + if pid, err = d.instruments.AppLaunch(bundleID, + WithAppPath(appPath), + WithEnvironment(appEnv), + WithArguments(appArgs), + WithOptions(appOpt), + WithKillExisting(true), + ); err != nil { + return _out, cancelFunc, err + } + + if err = d.instruments.startObserving(pid); err != nil { + return _out, cancelFunc, err + } + + if DeviceVersion(version...) >= DeviceVersion(12, 0, 0) { + err = xcTestManager1.authorizeTestSession(pid) + } else if DeviceVersion(version...) <= DeviceVersion(9, 0, 0) { + err = xcTestManager1.initiateControlSessionForTestProcessID(pid) + } else { + err = xcTestManager1.initiateControlSessionForTestProcessIDProtocolVersion(pid, xcodeVersion) + } + if err != nil { + return _out, cancelFunc, err + } + + go func() { + // for range ctx.Done() { + // if _err := d.AppKill(pid); _err != nil { + // debugLog(fmt.Sprintf("xctest kill: %d", pid)) + // } + // tmSrv1.close() + // tmSrv2.close() + // xcTestManager1.close() + // xcTestManager2.close() + // time.Sleep(time.Second) + // close(_out) + // fmt.Println("#####DONE") + // } + + for { + select { + case <-ctx.Done(): + tmSrv1.close() + tmSrv2.close() + xcTestManager1.close() + xcTestManager2.close() + if _err := d.AppKill(pid); _err != nil { + debugLog(fmt.Sprintf("xctest kill: %d", pid)) + } + time.Sleep(time.Second) + close(_out) + return + } + } + }() + + return _out, cancelFunc, err +} + +func (d *device) _uploadXCTestConfiguration(bundleID string, sessionId uuid.UUID, lookupResult map[string]interface{}) (pathXCTestCfg string, err error) { + if _, err = d.HouseArrestService(); err != nil { + return "", err + } + + var appAfc Afc + if appAfc, err = d.houseArrest.Container(bundleID); err != nil { + return "", err + } + + appTmpFilenames, err := appAfc.ReadDir("/tmp") + if err != nil { + return "", err + } + + for _, tName := range appTmpFilenames { + if strings.HasSuffix(tName, ".xctestconfiguration") { + if _err := appAfc.Remove(fmt.Sprintf("/tmp/%s", tName)); _err != nil { + debugLog(fmt.Sprintf("remove /tmp/%s: %s", tName, err)) + continue + } + } + } + + nameExec := lookupResult["CFBundleExecutable"].(string) + name := nameExec[:len(nameExec)-len("-Runner")] + appPath := lookupResult["Path"].(string) + + pathXCTestCfg = fmt.Sprintf("/tmp/%s-%s.xctestconfiguration", name, strings.ToUpper(sessionId.String())) + + var content []byte + if content, err = nskeyedarchiver.Marshal( + nskeyedarchiver.NewXCTestConfiguration( + nskeyedarchiver.NewNSUUID(sessionId.Bytes()), + nskeyedarchiver.NewNSURL(fmt.Sprintf("%s/PlugIns/%s.xctest", appPath, name)), + bundleID, + appPath, + ), + ); err != nil { + return "", err + } + + var file *AfcFile + if file, err = appAfc.Open(pathXCTestCfg, AfcFileModeWr); err != nil { + return "", err + } + if _, err = file.Write(content); err != nil { + return "", err + } + + return +} diff --git a/device_test.go b/device_test.go new file mode 100644 index 0000000..cfa415b --- /dev/null +++ b/device_test.go @@ -0,0 +1,95 @@ +package giDevice + +import ( + "fmt" + "sync" + "testing" + "time" +) + +var dev Device + +func setupDevice(t *testing.T) { + setupUsbmux(t) + devices, err := um.Devices() + if err != nil { + t.Fatal(err) + } + + if len(devices) == 0 { + t.Fatal("No Device") + } + + dev = devices[0] +} +func Test_device_ReadPairRecord(t *testing.T) { + setupDevice(t) + + pairRecord, err := dev.ReadPairRecord() + if err != nil { + t.Fatal(err) + } + + t.Log(pairRecord.HostID, pairRecord.SystemBUID, pairRecord.WiFiMACAddress) +} + +func Test_device_NewConnect(t *testing.T) { + setupDevice(t) + + if _, err := dev.NewConnect(LockdownPort); err != nil { + t.Fatal(err) + } +} + +func Test_device_DeletePairRecord(t *testing.T) { + setupDevice(t) + + if err := dev.DeletePairRecord(); err != nil { + t.Fatal(err) + } + +} + +func Test_device_SavePairRecord(t *testing.T) { + setupLockdownSrv(t) + + pairRecord, err := lockdownSrv.Pair() + if err != nil { + t.Fatal(err) + } + + err = dev.SavePairRecord(pairRecord) + if err != nil { + t.Fatal(err) + } +} + +func Test_device_XCTest(t *testing.T) { + setupLockdownSrv(t) + + bundleID = "com.leixipaopao.WebDriverAgentRunner.xctrunner" + out, cancel, err := dev.XCTest(bundleID) + if err != nil { + t.Fatal(err) + } + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + for s := range out { + fmt.Print(s) + } + }() + + go func() { + time.Sleep(10 * time.Second) + cancel() + t.Log("DONE") + wg.Done() + }() + + wg.Wait() + + time.Sleep(5 * time.Second) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..761b4de --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/electricbubble/gidevice + +go 1.16 + +require ( + github.com/satori/go.uuid v1.2.0 // indirect + howett.net/plist v0.0.0-20201203080718-1454fab16a06 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c9cf7f1 --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +howett.net/plist v0.0.0-20201203080718-1454fab16a06 h1:QDxUo/w2COstK1wIBYpzQlHX/NqaQTcf9jyz347nI58= +howett.net/plist v0.0.0-20201203080718-1454fab16a06/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= diff --git a/housearrest.go b/housearrest.go new file mode 100644 index 0000000..f5c60a7 --- /dev/null +++ b/housearrest.go @@ -0,0 +1,57 @@ +package giDevice + +import "github.com/electricbubble/gidevice/pkg/libimobiledevice" + +var _ HouseArrest = (*houseArrest)(nil) + +func newHouseArrest(client *libimobiledevice.HouseArrestClient) *houseArrest { + return &houseArrest{ + client: client, + } +} + +type houseArrest struct { + client *libimobiledevice.HouseArrestClient +} + +func (h *houseArrest) Documents(bundleID string) (afc Afc, err error) { + var pkt libimobiledevice.Packet + if pkt, err = h.client.NewXmlPacket( + h.client.NewDocumentsRequest(bundleID), + ); err != nil { + return nil, err + } + + if err = h.client.SendPacket(pkt); err != nil { + return nil, err + } + + if _, err = h.client.ReceivePacket(); err != nil { + return nil, err + } + + afcClient := libimobiledevice.NewAfcClient(h.client.InnerConn()) + afc = newAfc(afcClient) + return +} + +func (h *houseArrest) Container(bundleID string) (afc Afc, err error) { + var pkt libimobiledevice.Packet + if pkt, err = h.client.NewXmlPacket( + h.client.NewContainerRequest(bundleID), + ); err != nil { + return nil, err + } + + if err = h.client.SendPacket(pkt); err != nil { + return nil, err + } + + if _, err = h.client.ReceivePacket(); err != nil { + return nil, err + } + + afcClient := libimobiledevice.NewAfcClient(h.client.InnerConn()) + afc = newAfc(afcClient) + return +} diff --git a/housearrest_test.go b/housearrest_test.go new file mode 100644 index 0000000..8af8d99 --- /dev/null +++ b/housearrest_test.go @@ -0,0 +1,58 @@ +package giDevice + +import ( + "testing" +) + +var houseArrestSrv HouseArrest + +func setupHouseArrestSrv(t *testing.T) { + setupLockdownSrv(t) + + var err error + if lockdownSrv, err = dev.lockdownService(); err != nil { + t.Fatal(err) + } + + if houseArrestSrv, err = lockdownSrv.HouseArrestService(); err != nil { + t.Fatal(err) + } +} + +func Test_houseArrest_Documents(t *testing.T) { + setupHouseArrestSrv(t) + + bundleID = "com.apple.iMovie" + appAfc, err := houseArrestSrv.Documents(bundleID) + if err != nil { + t.Fatal(err) + } + + names, err := appAfc.ReadDir("Documents") + if err != nil { + t.Fatal(err) + } + + for _, name := range names { + t.Log(name) + } +} + +func Test_houseArrest_Container(t *testing.T) { + setupHouseArrestSrv(t) + + bundleID = "com.apple.iMovie" + appAfc, err := houseArrestSrv.Documents(bundleID) + if err != nil { + t.Fatal(err) + } + + names, err := appAfc.ReadDir("Documents") + if err != nil { + t.Fatal(err) + } + + for _, name := range names { + t.Log(name) + } +} diff --git a/idevice.go b/idevice.go new file mode 100644 index 0000000..bbd7d77 --- /dev/null +++ b/idevice.go @@ -0,0 +1,345 @@ +package giDevice + +import ( + "bytes" + "context" + "fmt" + "github.com/electricbubble/gidevice/pkg/libimobiledevice" + "github.com/electricbubble/gidevice/pkg/nskeyedarchiver" + "log" + "time" +) + +type Usbmux interface { + Devices() ([]Device, error) + ReadBUID() (string, error) + Listen(chan Device) (context.CancelFunc, error) +} + +type Device interface { + Properties() DeviceProperties + + NewConnect(port int) (InnerConn, error) + ReadPairRecord() (pairRecord *PairRecord, err error) + SavePairRecord(pairRecord *PairRecord) (err error) + DeletePairRecord() (err error) + + lockdownService() (lockdown Lockdown, err error) + QueryType() (LockdownType, error) + GetValue(domain, key string) (v interface{}, err error) + Pair() (pairRecord *PairRecord, err error) + + imageMounterService() (imageMounter ImageMounter, err error) + Images(imgType ...string) (imageSignatures [][]byte, err error) + MountDeveloperDiskImage(dmgPath string, signaturePath string) (err error) + + screenshotService() (lockdown Screenshot, err error) + Screenshot() (raw *bytes.Buffer, err error) + + simulateLocationService() (simulateLocation SimulateLocation, err error) + SimulateLocationUpdate(longitude float64, latitude float64, coordinateSystem ...CoordinateSystem) (err error) + SimulateLocationRecover() (err error) + + installationProxyService() (installationProxy InstallationProxy, err error) + InstallationProxyBrowse(opts ...InstallationProxyOption) (currentList []interface{}, err error) + InstallationProxyLookup(opts ...InstallationProxyOption) (lookupResult interface{}, err error) + + instrumentsService() (instruments Instruments, err error) + AppLaunch(bundleID string, opts ...AppLaunchOption) (pid int, err error) + AppKill(pid int) (err error) + AppRunningProcesses() (processes []Process, err error) + AppList(opts ...AppListOption) (apps []Application, err error) + DeviceInfo() (devInfo *DeviceInfo, err error) + + HouseArrestService() (houseArrest HouseArrest, err error) + + XCTest(bundleID string) (out <-chan string, cancel context.CancelFunc, err error) +} + +type DeviceProperties = libimobiledevice.DeviceProperties + +type Lockdown interface { + QueryType() (LockdownType, error) + GetValue(domain, key string) (v interface{}, err error) + SetValue(domain, key string, value interface{}) (err error) + Pair() (pairRecord *PairRecord, err error) + EnterRecovery() (err error) + + handshake() (err error) + + startSession(pairRecord *PairRecord) (err error) + stopSession() (err error) + startService(service string, escrowBag []byte) (dynamicPort int, enableSSL bool, err error) + + ImageMounterService() (imageMounter ImageMounter, err error) + ScreenshotService() (screenshot Screenshot, err error) + SimulateLocationService() (simulateLocation SimulateLocation, err error) + InstallationProxyService() (installationProxy InstallationProxy, err error) + InstrumentsService() (instruments Instruments, err error) + TestmanagerdService() (testmanagerd Testmanagerd, err error) + AfcService() (afc Afc, err error) + HouseArrestService() (houseArrest HouseArrest, err error) +} + +type ImageMounter interface { + Images(imgType string) (imageSignatures [][]byte, err error) + UploadImage(imgType, dmgPath string, signatureData []byte) (err error) + Mount(imgType, devImgPath string, signatureData []byte) (err error) + + UploadImageAndMount(imgType, devImgPath, dmgPath, signaturePath string) (err error) +} + +type Screenshot interface { + exchange() (err error) + Take() (raw *bytes.Buffer, err error) +} + +type SimulateLocation interface { + Update(longitude float64, latitude float64, coordinateSystem ...CoordinateSystem) (err error) + // Recover try to revert back + Recover() (err error) +} + +type InstallationProxy interface { + Browse(opts ...InstallationProxyOption) (currentList []interface{}, err error) + Lookup(opts ...InstallationProxyOption) (lookupResult interface{}, err error) +} + +type Instruments interface { + AppLaunch(bundleID string, opts ...AppLaunchOption) (pid int, err error) + AppKill(pid int) (err error) + AppRunningProcesses() (processes []Process, err error) + AppList(opts ...AppListOption) (apps []Application, err error) + DeviceInfo() (devInfo *DeviceInfo, err error) + + appProcess(bundleID string) (err error) + startObserving(pid int) (err error) + + notifyOfPublishedCapabilities() (err error) + requestChannel(channel string) (id uint32, err error) + + registerCallback(obj string, cb func(m libimobiledevice.DTXMessageResult)) +} + +type Testmanagerd interface { + notifyOfPublishedCapabilities() (err error) + requestChannel(channel string) (id uint32, err error) + newXCTestManagerDaemon() (xcTestManager XCTestManagerDaemon, err error) + + invoke(selector string, args *libimobiledevice.AuxBuffer, channel uint32, expectsReply bool) (*libimobiledevice.DTXMessageResult, error) + + registerCallback(obj string, cb func(m libimobiledevice.DTXMessageResult)) + close() +} + +type Afc interface { + DiskInfo() (diskInfo *AfcDiskInfo, err error) + ReadDir(dirname string) (names []string, err error) + Stat(filename string) (info *AfcFileInfo, err error) + Open(filename string, mode AfcFileMode) (file *AfcFile, err error) + Remove(filePath string) (err error) + Rename(oldPath string, newPath string) (err error) + Mkdir(path string) (err error) + Link(oldName string, newName string, linkType AfcLinkType) (err error) + Truncate(filePath string, size int64) (err error) + SetFileModTime(filePath string, modTime time.Time) (err error) + // Hash sha1 algorithm + Hash(filePath string) ([]byte, error) + // HashWithRange sha1 algorithm with file range + HashWithRange(filePath string, start, end uint64) ([]byte, error) + RemoveAll(path string) (err error) +} + +type HouseArrest interface { + Documents(bundleID string) (afc Afc, err error) + Container(bundleID string) (afc Afc, err error) +} + +type XCTestManagerDaemon interface { + // initiateControlSession iOS 11+ + initiateControlSession(XcodeVersion uint64) (err error) + startExecutingTestPlan(XcodeVersion uint64) (err error) + initiateSession(XcodeVersion uint64, nsUUID *nskeyedarchiver.NSUUID) (err error) + // authorizeTestSession iOS 12+ + authorizeTestSession(pid int) (err error) + // initiateControlSessionForTestProcessID <= iOS 9 + initiateControlSessionForTestProcessID(pid int) (err error) + // initiateControlSessionForTestProcessIDProtocolVersion iOS > 9 && iOS < 12 + initiateControlSessionForTestProcessIDProtocolVersion(pid int, XcodeVersion uint64) (err error) + + registerCallback(obj string, cb func(m libimobiledevice.DTXMessageResult)) + close() +} + +type InnerConn = libimobiledevice.InnerConn + +type LockdownType = libimobiledevice.LockdownType + +type PairRecord = libimobiledevice.PairRecord + +type CoordinateSystem = libimobiledevice.CoordinateSystem + +const ( + CoordinateSystemWGS84 = libimobiledevice.CoordinateSystemWGS84 + CoordinateSystemBD09 = libimobiledevice.CoordinateSystemBD09 + CoordinateSystemGCJ02 = libimobiledevice.CoordinateSystemGCJ02 +) + +type ApplicationType = libimobiledevice.ApplicationType + +const ( + ApplicationTypeSystem = libimobiledevice.ApplicationTypeSystem + ApplicationTypeUser = libimobiledevice.ApplicationTypeUser + ApplicationTypeInternal = libimobiledevice.ApplicationTypeInternal + ApplicationTypeAny = libimobiledevice.ApplicationTypeAny +) + +type installationProxyOption = libimobiledevice.InstallationProxyOption + +type InstallationProxyOption func(*installationProxyOption) + +func WithApplicationType(appType ApplicationType) InstallationProxyOption { + return func(opt *installationProxyOption) { + opt.ApplicationType = appType + } +} + +func WithReturnAttributes(attrs ...string) InstallationProxyOption { + return func(opt *installationProxyOption) { + if len(opt.ReturnAttributes) == 0 { + opt.ReturnAttributes = attrs + } else { + opt.ReturnAttributes = append(opt.ReturnAttributes, attrs...) + } + opt.ReturnAttributes = _removeDuplicate(opt.ReturnAttributes) + } +} + +func WithBundleIDs(BundleIDs ...string) InstallationProxyOption { + return func(opt *installationProxyOption) { + if len(opt.BundleIDs) == 0 { + opt.BundleIDs = BundleIDs + } else { + opt.BundleIDs = append(opt.BundleIDs, BundleIDs...) + } + opt.BundleIDs = _removeDuplicate(opt.BundleIDs) + } +} + +func WithMetaData(b bool) InstallationProxyOption { + return func(opt *installationProxyOption) { + opt.MetaData = b + } +} + +type appLaunchOption struct { + appPath string + environment map[string]interface{} + arguments []interface{} + options map[string]interface{} +} + +type AppLaunchOption func(option *appLaunchOption) + +func WithAppPath(appPath string) AppLaunchOption { + return func(opt *appLaunchOption) { + opt.appPath = appPath + } +} + +func WithEnvironment(environment map[string]interface{}) AppLaunchOption { + return func(opt *appLaunchOption) { + opt.environment = environment + } +} + +func WithArguments(arguments []interface{}) AppLaunchOption { + return func(opt *appLaunchOption) { + opt.arguments = arguments + } +} + +func WithOptions(options map[string]interface{}) AppLaunchOption { + return func(opt *appLaunchOption) { + for k, v := range options { + opt.options[k] = v + } + } +} + +func WithKillExisting(b bool) AppLaunchOption { + return func(opt *appLaunchOption) { + v := uint64(0) + if b { + v = uint64(1) + } + opt.options["KillExisting"] = v + } +} + +type appListOption struct { + appsMatching map[string]interface{} + updateToken string +} + +type AppListOption func(option *appListOption) + +func WithAppsMatching(appsMatching map[string]interface{}) AppListOption { + return func(opt *appListOption) { + opt.appsMatching = appsMatching + } +} + +func WithUpdateToken(updateToken string) AppListOption { + return func(opt *appListOption) { + opt.updateToken = updateToken + } +} + +type Process struct { + IsApplication bool `json:"isApplication"` + Name string `json:"name"` + Pid int `json:"pid"` + RealAppName string `json:"realAppName"` + StartDate time.Time `json:"startDate"` +} + +func _removeDuplicate(strSlice []string) []string { + existed := make(map[string]bool, len(strSlice)) + noRepeat := make([]string, 0, len(strSlice)) + for _, str := range strSlice { + if _, ok := existed[str]; ok { + continue + } + existed[str] = true + noRepeat = append(noRepeat, str) + } + return noRepeat +} + +func DeviceVersion(version ...int) int { + if len(version) < 3 { + tmp := make([]int, 3) + copy(tmp, version) + version = tmp + } + maj, min, patch := version[0], version[1], version[2] + return ((maj & 0xFF) << 16) | ((min & 0xFF) << 8) | (patch & 0xFF) +} + +var debugFlag = false + +// SetDebug sets debug mode +func SetDebug(debug bool, libDebug ...bool) { + debugFlag = debug + if len(libDebug) >= 1 { + libimobiledevice.SetDebug(libDebug[0]) + } +} + +func debugLog(msg string) { + if !debugFlag { + return + } + log.Println(fmt.Sprintf("[go-iDevice-debug] %s", msg)) +} diff --git a/imagemounter.go b/imagemounter.go new file mode 100644 index 0000000..aa72f33 --- /dev/null +++ b/imagemounter.go @@ -0,0 +1,141 @@ +package giDevice + +import ( + "fmt" + "github.com/electricbubble/gidevice/pkg/libimobiledevice" + "os" +) + +var _ ImageMounter = (*imageMounter)(nil) + +func newImageMounter(client *libimobiledevice.ImageMounterClient) *imageMounter { + return &imageMounter{ + client: client, + } +} + +type imageMounter struct { + client *libimobiledevice.ImageMounterClient +} + +func (m *imageMounter) Images(imgType string) (imageSignatures [][]byte, err error) { + var pkt libimobiledevice.Packet + if pkt, err = m.client.NewXmlPacket( + m.client.NewBasicRequest(libimobiledevice.CommandTypeLookupImage, imgType), + ); err != nil { + return nil, err + } + + if err = m.client.SendPacket(pkt); err != nil { + return nil, err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = m.client.ReceivePacket(); err != nil { + return nil, err + } + + var reply libimobiledevice.ImageMounterLookupImageResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return nil, err + } + + imageSignatures = reply.ImageSignature + return +} + +func (m *imageMounter) UploadImage(imgType, dmgPath string, signatureData []byte) (err error) { + var dmgFileInfo os.FileInfo + if dmgFileInfo, err = os.Stat(dmgPath); err != nil { + return err + } + + var pkt libimobiledevice.Packet + if pkt, err = m.client.NewXmlPacket( + m.client.NewReceiveBytesRequest(imgType, uint32(dmgFileInfo.Size()), signatureData), + ); err != nil { + return err + } + + if err = m.client.SendPacket(pkt); err != nil { + return err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = m.client.ReceivePacket(); err != nil { + return err + } + + var reply libimobiledevice.ImageMounterBasicResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return err + } + + if reply.Status != "ReceiveBytesAck" { + return fmt.Errorf("image mounter 'ReceiveBytes' status: %s", reply.Status) + } + + var dmgData []byte + if dmgData, err = os.ReadFile(dmgPath); err != nil { + return err + } + + if err = m.client.SendDmg(dmgData); err != nil { + return err + } + if respPkt, err = m.client.ReceivePacket(); err != nil { + return err + } + + if err = respPkt.Unmarshal(&reply); err != nil { + return err + } + + if reply.Status != "Complete" { + return fmt.Errorf("image mounter 'SendDmg' status: %s", reply.Status) + } + + return +} + +func (m *imageMounter) Mount(imgType, devImgPath string, signatureData []byte) (err error) { + var pkt libimobiledevice.Packet + if pkt, err = m.client.NewXmlPacket( + m.client.NewMountImageRequest(imgType, devImgPath, signatureData), + ); err != nil { + return err + } + + if err = m.client.SendPacket(pkt); err != nil { + return err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = m.client.ReceivePacket(); err != nil { + return err + } + + var reply libimobiledevice.ImageMounterBasicResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return err + } + + if reply.Status != "Complete" { + return fmt.Errorf("image mounter 'MountImage' status: %s", reply.Status) + } + return +} + +func (m *imageMounter) UploadImageAndMount(imgType, devImgPath, dmgPath, signaturePath string) (err error) { + var signatureData []byte + if signatureData, err = os.ReadFile(signaturePath); err != nil { + return err + } + if err = m.UploadImage(imgType, dmgPath, signatureData); err != nil { + return err + } + if err = m.Mount(imgType, devImgPath, signatureData); err != nil { + return err + } + return +} diff --git a/imagemounter_test.go b/imagemounter_test.go new file mode 100644 index 0000000..ab30502 --- /dev/null +++ b/imagemounter_test.go @@ -0,0 +1,49 @@ +package giDevice + +import ( + "encoding/base64" + "testing" +) + +var imageMounterSrv ImageMounter + +func setupImageMounterSrv(t *testing.T) { + setupLockdownSrv(t) + + var err error + if lockdownSrv, err = dev.lockdownService(); err != nil { + t.Fatal(err) + } + + // Once + // dev.Images() + if imageMounterSrv, err = lockdownSrv.ImageMounterService(); err != nil { + t.Fatal(err) + } +} + +func Test_imageMounter_Images(t *testing.T) { + setupImageMounterSrv(t) + + // imageSignatures, err := dev.Images() + imageSignatures, err := imageMounterSrv.Images("Developer") + if err != nil { + t.Fatal(err) + } + + for i, imgSign := range imageSignatures { + t.Logf("%2d, %s", i+1, base64.StdEncoding.EncodeToString(imgSign)) + } +} + +func Test_imageMounter_UploadImageAndMount(t *testing.T) { + setupImageMounterSrv(t) + + devImgPath := "/private/var/mobile/Media/PublicStaging/staging.dimage" + dmgPath := "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/14.4/DeveloperDiskImage.dmg" + signaturePath := "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/14.4/DeveloperDiskImage.dmg.signature" + + if err := imageMounterSrv.UploadImageAndMount("Developer", devImgPath, dmgPath, signaturePath); err != nil { + t.Fatal(err) + } +} diff --git a/installationproxy.go b/installationproxy.go new file mode 100644 index 0000000..b31ff80 --- /dev/null +++ b/installationproxy.go @@ -0,0 +1,96 @@ +package giDevice + +import ( + "fmt" + "github.com/electricbubble/gidevice/pkg/libimobiledevice" +) + +var _ InstallationProxy = (*installationProxy)(nil) + +func newInstallationProxy(client *libimobiledevice.InstallationProxyClient) *installationProxy { + return &installationProxy{ + client: client, + } +} + +type installationProxy struct { + client *libimobiledevice.InstallationProxyClient +} + +func (p *installationProxy) Browse(opts ...InstallationProxyOption) (currentList []interface{}, err error) { + opt := new(installationProxyOption) + if len(opts) == 0 { + opt = nil + } else { + for _, optFunc := range opts { + optFunc(opt) + } + } + + var pkt libimobiledevice.Packet + if pkt, err = p.client.NewXmlPacket( + p.client.NewBasicRequest(libimobiledevice.CommandTypeBrowse, opt), + ); err != nil { + return nil, err + } + + if err = p.client.SendPacket(pkt); err != nil { + return nil, err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = p.client.ReceivePacket(); err != nil { + return nil, err + } + + var reply libimobiledevice.InstallationProxyBrowseResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return nil, err + } + if reply.Status != "BrowsingApplications" { + return nil, fmt.Errorf("installation proxy 'Browse' status: %s", reply.Status) + } + + currentList = reply.CurrentList + return +} + +func (p *installationProxy) Lookup(opts ...InstallationProxyOption) (lookupResult interface{}, err error) { + opt := new(installationProxyOption) + if len(opts) == 0 { + opt = nil + } else { + for _, optFunc := range opts { + optFunc(opt) + } + } + + var pkt libimobiledevice.Packet + if pkt, err = p.client.NewXmlPacket( + p.client.NewBasicRequest(libimobiledevice.CommandTypeLookup, opt), + ); err != nil { + return nil, err + } + + if err = p.client.SendPacket(pkt); err != nil { + return nil, err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = p.client.ReceivePacket(); err != nil { + return nil, err + } + + var reply libimobiledevice.InstallationProxyLookupResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return nil, err + } + if reply.Status != "Complete" { + return nil, fmt.Errorf("installation proxy 'Lookup' status: %s", reply.Status) + } + + lookupResult = reply.LookupResult + + return + +} diff --git a/installationproxy_test.go b/installationproxy_test.go new file mode 100644 index 0000000..a74891f --- /dev/null +++ b/installationproxy_test.go @@ -0,0 +1,67 @@ +package giDevice + +import ( + "testing" +) + +var installationProxySrv InstallationProxy + +func setupInstallationProxySrv(t *testing.T) { + setupLockdownSrv(t) + + var err error + if lockdownSrv, err = dev.lockdownService(); err != nil { + t.Fatal(err) + } + + if installationProxySrv, err = lockdownSrv.InstallationProxyService(); err != nil { + t.Fatal(err) + } +} + +func Test_installationProxy_Browse(t *testing.T) { + setupInstallationProxySrv(t) + + // currentList, err := installationProxySrv.Browse(WithMetaData(true)) + // currentList, err := installationProxySrv.Browse(WithReturnAttributes("CFBundleIdentifier", "SequenceNumber", "SequenceNumber")) + // currentList, err := installationProxySrv.Browse(WithApplicationType(ApplicationTypeSystem)) + // currentList, err := installationProxySrv.Browse(WithApplicationType(ApplicationTypeSystem), WithReturnAttributes("ApplicationType", "ApplicationType")) + // currentList, err := dev.InstallationProxyBrowse() + currentList, err := installationProxySrv.Browse() + // currentList, err := installationProxySrv.Browse(WithBundleIDs("com.apple.MusicUIService"), WithBundleIDs("com.apple.Home.HomeControlService")) + if err != nil { + t.Fatal(err) + } + + t.Log(len(currentList)) + + for _, cl := range currentList { + app := cl.(map[string]interface{}) + t.Log(app) + } +} + +func Test_installationProxy_Lookup(t *testing.T) { + setupInstallationProxySrv(t) + + // lookupResult, err := installationProxySrv.Lookup() + // lookupResult, err := dev.InstallationProxyLookup( + lookupResult, err := installationProxySrv.Lookup( + // WithApplicationType(ApplicationTypeUser), + // WithApplicationType(ApplicationTypeSystem), + // WithReturnAttributes("CFBundleDevelopmentRegion"), + // WithReturnAttributes("CFBundleDisplayName", "CFBundleIdentifier"), + // WithBundleIDs("com.apple.mobilephone"), + WithBundleIDs("com.leixipaopao.WebDriverAgentRunner.xctrunner"), + ) + if err != nil { + t.Fatal(err) + } + + ret := lookupResult.(map[string]interface{}) + t.Log(len(ret)) + + for k, v := range ret { + t.Log(k, "-->", v) + } +} diff --git a/instruments.go b/instruments.go new file mode 100644 index 0000000..a07f09d --- /dev/null +++ b/instruments.go @@ -0,0 +1,284 @@ +package giDevice + +import ( + "encoding/json" + "fmt" + "github.com/electricbubble/gidevice/pkg/libimobiledevice" +) + +var _ Instruments = (*instruments)(nil) + +func newInstruments(client *libimobiledevice.InstrumentsClient) *instruments { + return &instruments{ + client: client, + } +} + +type instruments struct { + client *libimobiledevice.InstrumentsClient +} + +func (i *instruments) notifyOfPublishedCapabilities() (err error) { + _, err = i.client.NotifyOfPublishedCapabilities() + return +} + +func (i *instruments) requestChannel(channel string) (id uint32, err error) { + return i.client.RequestChannel(channel) +} + +func (i *instruments) AppLaunch(bundleID string, opts ...AppLaunchOption) (pid int, err error) { + opt := new(appLaunchOption) + opt.appPath = "" + opt.options = map[string]interface{}{ + "StartSuspendedKey": uint64(0), + "KillExisting": uint64(0), + } + if len(opts) != 0 { + for _, optFunc := range opts { + optFunc(opt) + } + } + + var id uint32 + if id, err = i.requestChannel("com.apple.instruments.server.services.processcontrol"); err != nil { + return 0, err + } + + args := libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(opt.appPath); err != nil { + return 0, err + } + if err = args.AppendObject(bundleID); err != nil { + return 0, err + } + if err = args.AppendObject(opt.environment); err != nil { + return 0, err + } + if err = args.AppendObject(opt.arguments); err != nil { + return 0, err + } + if err = args.AppendObject(opt.options); err != nil { + return 0, err + } + + var result *libimobiledevice.DTXMessageResult + selector := "launchSuspendedProcessWithDevicePath:bundleIdentifier:environment:arguments:options:" + if result, err = i.client.Invoke(selector, args, id, true); err != nil { + return 0, err + } + + if nsErr, ok := result.Obj.(libimobiledevice.NSError); ok { + return 0, fmt.Errorf("%s", nsErr.NSUserInfo.(map[string]interface{})["NSLocalizedDescription"]) + } + + return int(result.Obj.(uint64)), nil +} + +func (i *instruments) appProcess(bundleID string) (err error) { + var id uint32 + if id, err = i.requestChannel("com.apple.instruments.server.services.processcontrol"); err != nil { + return err + } + + args := libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(bundleID); err != nil { + return err + } + + selector := "processIdentifierForBundleIdentifier:" + if _, err = i.client.Invoke(selector, args, id, true); err != nil { + return err + } + + return +} + +func (i *instruments) startObserving(pid int) (err error) { + var id uint32 + if id, err = i.requestChannel("com.apple.instruments.server.services.processcontrol"); err != nil { + return err + } + + args := libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(pid); err != nil { + return err + } + + var result *libimobiledevice.DTXMessageResult + selector := "startObservingPid:" + if result, err = i.client.Invoke(selector, args, id, true); err != nil { + return err + } + + if nsErr, ok := result.Obj.(libimobiledevice.NSError); ok { + return fmt.Errorf("%s", nsErr.NSUserInfo.(map[string]interface{})["NSLocalizedDescription"]) + } + return +} + +func (i *instruments) AppKill(pid int) (err error) { + var id uint32 + if id, err = i.requestChannel("com.apple.instruments.server.services.processcontrol"); err != nil { + return err + } + + args := libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(pid); err != nil { + return err + } + + selector := "killPid:" + if _, err = i.client.Invoke(selector, args, id, false); err != nil { + return err + } + + return +} + +func (i *instruments) AppRunningProcesses() (processes []Process, err error) { + var id uint32 + if id, err = i.requestChannel("com.apple.instruments.server.services.deviceinfo"); err != nil { + return nil, err + } + + selector := "runningProcesses" + + var result *libimobiledevice.DTXMessageResult + if result, err = i.client.Invoke(selector, libimobiledevice.NewAuxBuffer(), id, true); err != nil { + return nil, err + } + + objs := result.Obj.([]interface{}) + + processes = make([]Process, 0, len(objs)) + + for _, v := range objs { + m := v.(map[string]interface{}) + + var data []byte + if data, err = json.Marshal(m); err != nil { + debugLog(fmt.Sprintf("process marshal: %v\n%v\n", err, m)) + err = nil + continue + } + + var tp Process + if err = json.Unmarshal(data, &tp); err != nil { + debugLog(fmt.Sprintf("process unmarshal: %v\n%v\n", err, m)) + err = nil + continue + } + + processes = append(processes, tp) + } + + return +} + +func (i *instruments) AppList(opts ...AppListOption) (apps []Application, err error) { + opt := new(appListOption) + opt.updateToken = "" + opt.appsMatching = make(map[string]interface{}) + if len(opts) != 0 { + for _, optFunc := range opts { + optFunc(opt) + } + } + + var id uint32 + if id, err = i.requestChannel("com.apple.instruments.server.services.device.applictionListing"); err != nil { + return nil, err + } + + args := libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(opt.appsMatching); err != nil { + return nil, err + } + if err = args.AppendObject(opt.updateToken); err != nil { + return nil, err + } + + selector := "installedApplicationsMatching:registerUpdateToken:" + + var result *libimobiledevice.DTXMessageResult + if result, err = i.client.Invoke(selector, args, id, true); err != nil { + return nil, err + } + + objs := result.Obj.([]interface{}) + + for _, v := range objs { + m := v.(map[string]interface{}) + + var data []byte + if data, err = json.Marshal(m); err != nil { + debugLog(fmt.Sprintf("application marshal: %v\n%v\n", err, m)) + err = nil + continue + } + + var app Application + if err = json.Unmarshal(data, &app); err != nil { + debugLog(fmt.Sprintf("application unmarshal: %v\n%v\n", err, m)) + err = nil + continue + } + apps = append(apps, app) + } + + return +} + +func (i *instruments) DeviceInfo() (devInfo *DeviceInfo, err error) { + var id uint32 + if id, err = i.requestChannel("com.apple.instruments.server.services.deviceinfo"); err != nil { + return nil, err + } + + selector := "systemInformation" + + var result *libimobiledevice.DTXMessageResult + if result, err = i.client.Invoke(selector, libimobiledevice.NewAuxBuffer(), id, true); err != nil { + return nil, err + } + + data, err := json.Marshal(result.Obj) + if err != nil { + return nil, err + } + devInfo = new(DeviceInfo) + err = json.Unmarshal(data, devInfo) + + return +} + +func (i *instruments) registerCallback(obj string, cb func(m libimobiledevice.DTXMessageResult)) { + i.client.RegisterCallback(obj, cb) +} + +type Application struct { + AppExtensionUUIDs []string `json:"AppExtensionUUIDs,omitempty"` + BundlePath string `json:"BundlePath"` + CFBundleIdentifier string `json:"CFBundleIdentifier"` + ContainerBundleIdentifier string `json:"ContainerBundleIdentifier,omitempty"` + ContainerBundlePath string `json:"ContainerBundlePath,omitempty"` + DisplayName string `json:"DisplayName"` + ExecutableName string `json:"ExecutableName,omitempty"` + Placeholder bool `json:"Placeholder,omitempty"` + PluginIdentifier string `json:"PluginIdentifier,omitempty"` + PluginUUID string `json:"PluginUUID,omitempty"` + Restricted int `json:"Restricted"` + Type string `json:"Type"` + Version string `json:"Version"` +} + +type DeviceInfo struct { + Description string `json:"_deviceDescription"` + DisplayName string `json:"_deviceDisplayName"` + Identifier string `json:"_deviceIdentifier"` + Version string `json:"_deviceVersion"` + ProductType string `json:"_productType"` + ProductVersion string `json:"_productVersion"` + XRDeviceClassName string `json:"_xrdeviceClassName"` +} diff --git a/instruments_test.go b/instruments_test.go new file mode 100644 index 0000000..8e208e6 --- /dev/null +++ b/instruments_test.go @@ -0,0 +1,94 @@ +package giDevice + +import ( + "testing" +) + +var instrumentsSrv Instruments +var bundleID = "com.apple.Preferences" + +func setupInstrumentsSrv(t *testing.T) { + setupLockdownSrv(t) + + var err error + if lockdownSrv, err = dev.lockdownService(); err != nil { + t.Fatal(err) + } + + if instrumentsSrv, err = lockdownSrv.InstrumentsService(); err != nil { + t.Fatal(err) + } +} +func Test_instruments_AppLaunch(t *testing.T) { + setupInstrumentsSrv(t) + + // bundleID = "com.leixipaopao.WebDriverAgentRunner.xctrunner" + + // pid, err := dev.AppLaunch(bundleID) + pid, err := instrumentsSrv.AppLaunch(bundleID) + // pid, err := instrumentsSrv.AppLaunch(bundleID, WithKillExisting(true)) + // pid, err := instrumentsSrv.AppLaunch(bundleID, WithKillExisting(true), WithArguments([]interface{}{"-AppleLanguages", "(Russian)"})) + if err != nil { + t.Fatal(err) + } + t.Log(pid) +} + +func Test_instruments_AppKill(t *testing.T) { + setupInstrumentsSrv(t) + + pid, err := instrumentsSrv.AppLaunch(bundleID) + if err != nil { + t.Fatal(err) + } + t.Log(pid) + + // if err = dev.AppKill(pid); err != nil { + if err = instrumentsSrv.AppKill(pid); err != nil { + t.Fatal(err) + } +} + +func Test_instruments_AppRunningProcesses(t *testing.T) { + setupInstrumentsSrv(t) + + // processes, err := dev.AppRunningProcesses() + processes, err := instrumentsSrv.AppRunningProcesses() + if err != nil { + t.Fatal(err) + } + + for _, p := range processes { + t.Log(p.IsApplication, "\t", p.Pid, "\t", p.Name, "\t", p.RealAppName, "\t", p.StartDate) + } +} + +func Test_instruments_AppList(t *testing.T) { + setupInstrumentsSrv(t) + + // apps, err := dev.AppList() + apps, err := instrumentsSrv.AppList() + if err != nil { + t.Fatal(err) + } + + for _, app := range apps { + t.Logf("%v\t%v\t%v\t%v\t%v\n", app.Type, app.DisplayName, app.ExecutableName, app.AppExtensionUUIDs, app.BundlePath) + } +} + +func Test_instruments_DeviceInfo(t *testing.T) { + setupInstrumentsSrv(t) + + devInfo, err := instrumentsSrv.DeviceInfo() + if err != nil { + t.Fatal(err) + } + t.Log(devInfo.Description) + t.Log(devInfo.DisplayName) + t.Log(devInfo.Identifier) + t.Log(devInfo.Version) + t.Log(devInfo.ProductType) + t.Log(devInfo.ProductVersion) + t.Log(devInfo.XRDeviceClassName) +} diff --git a/lockdown.go b/lockdown.go new file mode 100644 index 0000000..54d24f4 --- /dev/null +++ b/lockdown.go @@ -0,0 +1,622 @@ +package giDevice + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "github.com/electricbubble/gidevice/pkg/libimobiledevice" + uuid "github.com/satori/go.uuid" + "math/big" + "strconv" + "strings" + "time" +) + +var _ Lockdown = (*lockdown)(nil) + +func newLockdown(dev *device) *lockdown { + return &lockdown{ + umClient: dev.umClient, + client: dev.lockdownClient, + dev: dev, + } +} + +type lockdown struct { + umClient *libimobiledevice.UsbmuxClient + client *libimobiledevice.LockdownClient + sessionID string + + dev *device + iOSVersion []int + pairRecord *PairRecord +} + +func (c *lockdown) QueryType() (LockdownType, error) { + pkt, err := c.client.NewXmlPacket( + c.client.NewBasicRequest(libimobiledevice.RequestTypeQueryType), + ) + if err != nil { + return LockdownType{}, err + } + + if err = c.client.SendPacket(pkt); err != nil { + return LockdownType{}, err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = c.client.ReceivePacket(); err != nil { + return LockdownType{}, err + } + + var reply libimobiledevice.LockdownTypeResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return LockdownType{}, err + } + + return LockdownType{Type: reply.Type}, nil +} + +func (c *lockdown) GetValue(domain, key string) (v interface{}, err error) { + pkt, err := c.client.NewXmlPacket( + c.client.NewGetValueRequest(domain, key), + ) + if err != nil { + return nil, err + } + + if err = c.client.SendPacket(pkt); err != nil { + return nil, err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = c.client.ReceivePacket(); err != nil { + return nil, err + } + + var reply libimobiledevice.LockdownValueResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return nil, err + } + + v = reply.Value + + return +} + +func (c *lockdown) SetValue(domain, key string, value interface{}) (err error) { + var pkt libimobiledevice.Packet + if pkt, err = c.client.NewXmlPacket( + c.client.NewSetValueRequest(domain, key, value), + ); err != nil { + return err + } + + if err = c.client.SendPacket(pkt); err != nil { + return err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = c.client.ReceivePacket(); err != nil { + return err + } + + var reply libimobiledevice.LockdownValueResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return err + } + + if !reply.Value.(bool) { + return errors.New("lockdown SetValue: Failed") + } + return +} + +func (c *lockdown) EnterRecovery() (err error) { + var pkt libimobiledevice.Packet + if pkt, err = c.client.NewXmlPacket( + c.client.NewEnterRecoveryRequest(), + ); err != nil { + return err + } + + if err = c.client.SendPacket(pkt); err != nil { + return err + } + + if _, err = c.client.ReceivePacket(); err != nil { + return err + } + + return +} + +func (c *lockdown) handshake() (err error) { + var lockdownType LockdownType + if lockdownType, err = c.QueryType(); err != nil { + return err + } + if lockdownType.Type != "com.apple.mobile.lockdown" { + return fmt.Errorf("lockdown handshake 'QueryType': %s", lockdownType.Type) + } + + // if (device->version < DEVICE_VERSION(7,0,0)) + // for older devices, we need to validate pairing to receive trusted host status + + if c.pairRecord, err = c.dev.ReadPairRecord(); err == nil { + return nil + } + + if !strings.Contains(err.Error(), libimobiledevice.ReplyCodeBadDevice.String()) { + return err + } + + if c.pairRecord, err = c.Pair(); err != nil { + return err + } + + err = c.dev.SavePairRecord(c.pairRecord) + + return +} + +func (c *lockdown) Pair() (pairRecord *PairRecord, err error) { + var buid string + if buid, err = newUsbmux(c.umClient).ReadBUID(); err != nil { + return nil, err + } + + var devPublicKeyPem []byte + var devWiFiAddr string + + if lockdownValue, err := c.GetValue("", "DevicePublicKey"); err != nil { + return nil, err + } else { + devPublicKeyPem = lockdownValue.([]byte) + } + if lockdownValue, err := c.GetValue("", "WiFiAddress"); err != nil { + return nil, err + } else { + devWiFiAddr = lockdownValue.(string) + } + + if pairRecord, err = generatePairRecord(devPublicKeyPem); err != nil { + return nil, err + } + + pairRecord.SystemBUID = buid + pairRecord.HostID = strings.ToUpper(uuid.NewV4().String()) + hostPrivateKey := pairRecord.HostPrivateKey + pairRecord.HostPrivateKey = nil + rootPrivateKey := pairRecord.RootPrivateKey + pairRecord.RootPrivateKey = nil + + var pkt libimobiledevice.Packet + if pkt, err = c.client.NewXmlPacket( + c.client.NewPairRequest(pairRecord), + ); err != nil { + return nil, err + } + + if err = c.client.SendPacket(pkt); err != nil { + return nil, err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = c.client.ReceivePacket(); err != nil { + return nil, err + } + + var reply libimobiledevice.LockdownPairResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return nil, err + } + + pairRecord.EscrowBag = reply.EscrowBag + pairRecord.WiFiMACAddress = devWiFiAddr + pairRecord.HostPrivateKey = hostPrivateKey + pairRecord.RootPrivateKey = rootPrivateKey + + return +} + +func (c *lockdown) startSession(pairRecord *PairRecord) (err error) { + // if we have a running session, stop current one first + if c.sessionID != "" { + if err = c.stopSession(); err != nil { + return err + } + } + + var pkt libimobiledevice.Packet + if pkt, err = c.client.NewXmlPacket( + c.client.NewStartSessionRequest(pairRecord.SystemBUID, pairRecord.HostID), + ); err != nil { + return err + } + + if err = c.client.SendPacket(pkt); err != nil { + return err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = c.client.ReceivePacket(); err != nil { + return fmt.Errorf("lockdown start session: %w", err) + } + + var reply libimobiledevice.LockdownStartSessionResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return err + } + + if reply.EnableSessionSSL { + if err = c.client.EnableSSL(c.iOSVersion, pairRecord); err != nil { + return err + } + } + + c.sessionID = reply.SessionID + return +} + +func (c *lockdown) stopSession() (err error) { + if c.sessionID == "" { + return nil + } + + var pkt libimobiledevice.Packet + if pkt, err = c.client.NewXmlPacket( + c.client.NewStopSessionRequest(c.sessionID), + ); err != nil { + return err + } + + if err = c.client.SendPacket(pkt); err != nil { + return err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = c.client.ReceivePacket(); err != nil { + return fmt.Errorf("lockdown stop session: %w", err) + } + + var reply libimobiledevice.LockdownBasicResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return err + } + + c.sessionID = "" + return +} + +func (c *lockdown) startService(service string, escrowBag []byte) (dynamicPort int, enableSSL bool, err error) { + req := c.client.NewStartServiceRequest(service) + if escrowBag != nil { + req.EscrowBag = escrowBag + } + + var pkt libimobiledevice.Packet + if pkt, err = c.client.NewXmlPacket(req); err != nil { + return 0, false, err + } + + if err = c.client.SendPacket(pkt); err != nil { + return 0, false, err + } + + respPkt, err := c.client.ReceivePacket() + if err != nil { + return 0, false, err + } + + var reply libimobiledevice.LockdownStartServiceResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return 0, false, err + } + + if reply.Error != "" { + return 0, false, fmt.Errorf("lockdown start service: %s", reply.Error) + } + + dynamicPort = reply.Port + enableSSL = reply.EnableServiceSSL + + return +} + +func (c *lockdown) ImageMounterService() (imageMounter ImageMounter, err error) { + var innerConn InnerConn + if innerConn, err = c._startService(libimobiledevice.ImageMounterServiceName, nil); err != nil { + return nil, err + } + imageMounterClient := libimobiledevice.NewImageMounterClient(innerConn) + imageMounter = newImageMounter(imageMounterClient) + return +} + +func (c *lockdown) ScreenshotService() (screenshot Screenshot, err error) { + var innerConn InnerConn + if innerConn, err = c._startService(libimobiledevice.ScreenshotServiceName, nil); err != nil { + return nil, err + } + screenshotClient := libimobiledevice.NewScreenshotClient(innerConn) + screenshot = newScreenshot(screenshotClient) + return +} + +func (c *lockdown) SimulateLocationService() (simulateLocation SimulateLocation, err error) { + var innerConn InnerConn + if innerConn, err = c._startService(libimobiledevice.SimulateLocationServiceName, nil); err != nil { + return nil, err + } + simulateLocationClient := libimobiledevice.NewSimulateLocationClient(innerConn) + simulateLocation = newSimulateLocation(simulateLocationClient) + return +} + +func (c *lockdown) InstallationProxyService() (installationProxy InstallationProxy, err error) { + var innerConn InnerConn + if innerConn, err = c._startService(libimobiledevice.InstallationProxyServiceName, nil); err != nil { + return nil, err + } + installationProxyClient := libimobiledevice.NewInstallationProxyClient(innerConn) + installationProxy = newInstallationProxy(installationProxyClient) + return +} + +func (c *lockdown) InstrumentsService() (instruments Instruments, err error) { + service := libimobiledevice.InstrumentsServiceName + if DeviceVersion(c.iOSVersion...) >= DeviceVersion(14, 0, 0) { + service = libimobiledevice.InstrumentsSecureProxyServiceName + } + + var innerConn InnerConn + if innerConn, err = c._startService(service, nil); err != nil { + return nil, err + } + instrumentsClient := libimobiledevice.NewInstrumentsClient(innerConn) + instruments = newInstruments(instrumentsClient) + + if service == libimobiledevice.InstrumentsServiceName { + _ = innerConn.DismissSSL() + } + + if err = instruments.notifyOfPublishedCapabilities(); err != nil { + return nil, err + } + + return +} + +func (c *lockdown) TestmanagerdService() (testmanagerd Testmanagerd, err error) { + service := libimobiledevice.TestmanagerdServiceName + if DeviceVersion(c.iOSVersion...) >= DeviceVersion(14, 0, 0) { + service = libimobiledevice.TestmanagerdSecureServiceName + } + + var innerConn InnerConn + if innerConn, err = c._startService(service, nil); err != nil { + return nil, err + } + testmanagerdClient := libimobiledevice.NewTestmanagerdClient(innerConn) + testmanagerd = newTestmanagerd(testmanagerdClient, c.iOSVersion) + + if service == libimobiledevice.TestmanagerdServiceName { + _ = innerConn.DismissSSL() + } + + if err = testmanagerd.notifyOfPublishedCapabilities(); err != nil { + return nil, err + } + + return +} + +func (c *lockdown) AfcService() (afc Afc, err error) { + var innerConn InnerConn + if innerConn, err = c._startService(libimobiledevice.AfcServiceName, nil); err != nil { + return nil, err + } + afcClient := libimobiledevice.NewAfcClient(innerConn) + afc = newAfc(afcClient) + return +} + +func (c *lockdown) HouseArrestService() (houseArrest HouseArrest, err error) { + var innerConn InnerConn + if innerConn, err = c._startService(libimobiledevice.HouseArrestServiceName, nil); err != nil { + return nil, err + } + houseArrestClient := libimobiledevice.NewHouseArrestClient(innerConn) + houseArrest = newHouseArrest(houseArrestClient) + return +} + +func (c *lockdown) _startService(serviceName string, escrowBag []byte) (innerConn InnerConn, err error) { + if err = c.handshake(); err != nil { + return nil, err + } + + if err = c.startSession(c.pairRecord); err != nil { + return nil, err + } + + dynamicPort, enableSSL, err := c.startService(serviceName, escrowBag) + if err != nil { + return nil, err + } + + if err = c.stopSession(); err != nil { + return nil, err + } + + if innerConn, err = c.dev.NewConnect(dynamicPort); err != nil { + return nil, err + } + // clean deadline + innerConn.Timeout(0) + + if enableSSL { + if err = innerConn.Handshake(c.iOSVersion, c.pairRecord); err != nil { + return nil, err + } + } + return +} + +func (c *lockdown) _getProductVersion() (version []int, err error) { + if c.iOSVersion != nil { + return c.iOSVersion, nil + } + + var devProductVersion []string + if lockdownValue, err := c.GetValue("", "ProductVersion"); err != nil { + return nil, err + } else { + devProductVersion = strings.Split(lockdownValue.(string), ".") + } + + version = make([]int, len(devProductVersion)) + for i, v := range devProductVersion { + version[i], _ = strconv.Atoi(v) + } + + // if len(version) == 2 { + // version = append(version, 0) + // } + c.iOSVersion = version + + return +} + +func generatePairRecord(devPublicKeyPem []byte) (pairRecord *PairRecord, err error) { + block, _ := pem.Decode(devPublicKeyPem) + var deviceKey *rsa.PublicKey + if deviceKey, err = x509.ParsePKCS1PublicKey(block.Bytes); err != nil { + return nil, err + } + + var rootKey, hostKey *rsa.PrivateKey + if rootKey, err = rsa.GenerateKey(rand.Reader, 2048); err != nil { + return nil, err + } + if hostKey, err = rsa.GenerateKey(rand.Reader, 2048); err != nil { + return nil, err + } + serialNumber := big.NewInt(0) + notBefore := time.Now() + notAfter := notBefore.Add(time.Hour * (24 * 365) * 10) + + rootTemplate := x509.Certificate{ + IsCA: true, + SerialNumber: serialNumber, + Version: 2, + SignatureAlgorithm: x509.SHA1WithRSA, + PublicKeyAlgorithm: x509.RSA, + NotBefore: notBefore, + NotAfter: notAfter, + BasicConstraintsValid: true, + } + + var caCert, cert []byte + if caCert, err = x509.CreateCertificate(rand.Reader, &rootTemplate, &rootTemplate, rootKey.Public(), rootKey); err != nil { + return nil, err + } + + hostTemplate := x509.Certificate{ + SerialNumber: serialNumber, + Version: 2, + SignatureAlgorithm: x509.SHA1WithRSA, + PublicKeyAlgorithm: x509.RSA, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + BasicConstraintsValid: true, + } + if cert, err = x509.CreateCertificate(rand.Reader, &hostTemplate, &rootTemplate, hostKey.Public(), rootKey); err != nil { + return nil, err + } + + h := sha1.New() + if _, err = h.Write(rootKey.N.Bytes()); err != nil { + return nil, err + } + subjectKeyId := h.Sum(nil) + + deviceTemplate := x509.Certificate{ + SerialNumber: serialNumber, + Version: 2, + SignatureAlgorithm: x509.SHA1WithRSA, + PublicKeyAlgorithm: x509.RSA, + NotBefore: notBefore, + NotAfter: notAfter, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + SubjectKeyId: subjectKeyId, + } + + var deviceCert []byte + if deviceCert, err = x509.CreateCertificate(rand.Reader, &deviceTemplate, &rootTemplate, deviceKey, rootKey); err != nil { + return nil, err + } + + var deviceCertPEM []byte + if deviceCertPEM, err = encodePemCertificate(deviceCert); err != nil { + return nil, err + } + + var caPEM, caPrivatePEM []byte + if caPEM, caPrivatePEM, err = encodePairPemFormat(caCert, rootKey); err != nil { + return nil, err + } + + var certPEM, certPrivatePEM []byte + if certPEM, certPrivatePEM, err = encodePairPemFormat(cert, hostKey); err != nil { + return nil, err + } + + pairRecord = new(PairRecord) + + pairRecord.DeviceCertificate = deviceCertPEM + pairRecord.HostCertificate = certPEM + pairRecord.HostPrivateKey = certPrivatePEM + pairRecord.RootCertificate = caPEM + pairRecord.RootPrivateKey = caPrivatePEM + + return +} + +func encodePairPemFormat(cert []byte, key *rsa.PrivateKey) ([]byte, []byte, error) { + p, err := encodePemCertificate(cert) + if err != nil { + return nil, nil, err + } + + buf := new(bytes.Buffer) + if err := pem.Encode(buf, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + }); err != nil { + return nil, nil, err + } + + privy := buf.Bytes() + + return p, privy, nil +} + +func encodePemCertificate(cert []byte) ([]byte, error) { + buf := new(bytes.Buffer) + if err := pem.Encode(buf, &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert, + }); err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/lockdown_test.go b/lockdown_test.go new file mode 100644 index 0000000..1423378 --- /dev/null +++ b/lockdown_test.go @@ -0,0 +1,43 @@ +package giDevice + +import ( + "testing" +) + +var lockdownSrv Lockdown + +func setupLockdownSrv(t *testing.T) { + setupDevice(t) + + var err error + if lockdownSrv, err = dev.lockdownService(); err != nil { + t.Fatal(err) + } +} + +func Test_lockdown_QueryType(t *testing.T) { + setupLockdownSrv(t) + + lockdownType, err := lockdownSrv.QueryType() + if err != nil { + t.Fatal(err) + } + + t.Log(lockdownType.Type) +} + +func Test_lockdown_GetValue(t *testing.T) { + setupLockdownSrv(t) + + v, err := lockdownSrv.GetValue("", "") + // v, err := lockdownSrv.GetValue("", "ProductVersion") + // v, err := lockdownSrv.GetValue("", "DeviceName") + // v, err := lockdownSrv.GetValue("com.apple.mobile.iTunes", "") + // v, err := lockdownSrv.GetValue("com.apple.mobile.battery", "") + // v, err := lockdownSrv.GetValue("com.apple.disk_usage", "") + if err != nil { + t.Fatal(err) + } + + t.Log(v) +} diff --git a/pkg/libimobiledevice/afc.go b/pkg/libimobiledevice/afc.go new file mode 100644 index 0000000..d9fac44 --- /dev/null +++ b/pkg/libimobiledevice/afc.go @@ -0,0 +1,105 @@ +package libimobiledevice + +import ( + "bytes" + "encoding/hex" + "fmt" +) + +const AfcServiceName = "com.apple.afc" + +func NewAfcClient(innerConn InnerConn) *AfcClient { + return &AfcClient{ + innerConn: innerConn, + } +} + +type AfcClient struct { + innerConn InnerConn + packetNum uint64 +} + +func (c *AfcClient) newPacket(operation uint64, data, payload []byte) Packet { + c.packetNum++ + pkt := &afcPacket{ + operation: operation, + packetNum: c.packetNum, + entireLen: 40, + thisLen: 40, + } + if data != nil { + n := uint64(len(data)) + pkt.entireLen += n + pkt.thisLen += n + } + if payload != nil { + pkt.entireLen += uint64(len(payload)) + } + return pkt +} + +func (c *AfcClient) Send(operation uint64, data, payload []byte) (err error) { + pkt := c.newPacket(operation, data, payload) + var raw []byte + if raw, err = pkt.Pack(); err != nil { + return fmt.Errorf("send packet (afc): %w", err) + } + + buf := new(bytes.Buffer) + buf.Write(raw) + if data != nil { + debugLog(fmt.Sprintf("--> %s ...afc data...\n", pkt)) + buf.Write(data) + } else { + debugLog(fmt.Sprintf("--> %s\n", pkt)) + } + + if err = c.innerConn.Write(buf.Bytes()); err != nil { + return fmt.Errorf("send packet (afc): %w", err) + } + + if payload != nil { + if err = c.innerConn.Write(payload); err != nil { + return fmt.Errorf("send packet (afc): %w", err) + } + } + + return +} + +func (c *AfcClient) Receive() (respMsg *AfcMessage, err error) { + var bufHeader []byte + if bufHeader, err = c.innerConn.Read(40); err != nil { + return nil, fmt.Errorf("receive packet (afc): %w", err) + } + buffer := new(bytes.Buffer) + buffer.Write(bufHeader) + var respPkt *afcPacket + if respPkt, err = new(afcPacket).unpack(buffer); err != nil { + return nil, fmt.Errorf("receive packet (afc): %w", err) + } + + respMsg = new(AfcMessage) + respMsg.Operation = respPkt.operation + + buffer.Reset() + if respPkt.entireLen > 40 { + length := int(respPkt.entireLen - 40) + var bufDataAndPayload []byte + if bufDataAndPayload, err = c.innerConn.Read(length); err != nil { + return nil, fmt.Errorf("receive packet (afc): %w", err) + } + buffer.Write(bufDataAndPayload) + } + + bufData := make([]byte, respPkt.thisLen-40) + if _, err = buffer.Read(bufData); err != nil { + return nil, fmt.Errorf("receive packet (afc buffer): %w", err) + } + respMsg.Data = bufData + respMsg.Payload = buffer.Bytes() + + debugLog(fmt.Sprintf("<-- %s\n%s\n%s", respPkt, hex.Dump(respMsg.Data), hex.Dump(respMsg.Payload))) + + return +} diff --git a/pkg/libimobiledevice/afcmessage.go b/pkg/libimobiledevice/afcmessage.go new file mode 100644 index 0000000..adcd7ca --- /dev/null +++ b/pkg/libimobiledevice/afcmessage.go @@ -0,0 +1,187 @@ +package libimobiledevice + +import ( + "bytes" + "encoding/binary" + "errors" +) + +type AfcMessage struct { + Operation uint64 + Data []byte + Payload []byte +} + +func (m *AfcMessage) Map() map[string]string { + ret := make(map[string]string) + ss := m.Strings() + if ss != nil { + for i := 0; i < len(ss); i += 2 { + ret[ss[i]] = ss[i+1] + } + } + return ret +} + +func (m *AfcMessage) Strings() []string { + if m.Operation == AfcOperationData { + bs := bytes.Split(m.Payload, []byte{0}) + ss := make([]string, len(bs)-1) + for i := 0; i < len(ss); i++ { + ss[i] = string(bs[i]) + } + return ss + } + return nil +} + +func (m *AfcMessage) Uint64() uint64 { + return binary.LittleEndian.Uint64(m.Data) +} + +func (m *AfcMessage) Err() error { + if m.Operation == AfcOperationStatus { + status := m.Uint64() + if status != AfcErrSuccess { + return toError(status) + } + } + return nil +} + +func toError(status uint64) error { + switch status { + case AfcErrUnknownError: + return errors.New("UnknownError") + case AfcErrOperationHeaderInvalid: + return errors.New("OperationHeaderInvalid") + case AfcErrNoResources: + return errors.New("NoResources") + case AfcErrReadError: + return errors.New("ReadError") + case AfcErrWriteError: + return errors.New("WriteError") + case AfcErrUnknownPacketType: + return errors.New("UnknownPacketType") + case AfcErrInvalidArgument: + return errors.New("InvalidArgument") + case AfcErrObjectNotFound: + return errors.New("ObjectNotFound") + case AfcErrObjectIsDir: + return errors.New("ObjectIsDir") + case AfcErrPermDenied: + return errors.New("PermDenied") + case AfcErrServiceNotConnected: + return errors.New("ServiceNotConnected") + case AfcErrOperationTimeout: + return errors.New("OperationTimeout") + case AfcErrTooMuchData: + return errors.New("TooMuchData") + case AfcErrEndOfData: + return errors.New("EndOfData") + case AfcErrOperationNotSupported: + return errors.New("OperationNotSupported") + case AfcErrObjectExists: + return errors.New("ObjectExists") + case AfcErrObjectBusy: + return errors.New("ObjectBusy") + case AfcErrNoSpaceLeft: + return errors.New("NoSpaceLeft") + case AfcErrOperationWouldBlock: + return errors.New("OperationWouldBlock") + case AfcErrIoError: + return errors.New("IoError") + case AfcErrOperationInterrupted: + return errors.New("OperationInterrupted") + case AfcErrOperationInProgress: + return errors.New("OperationInProgress") + case AfcErrInternalError: + return errors.New("InternalError") + case AfcErrMuxError: + return errors.New("MuxError") + case AfcErrNoMemory: + return errors.New("NoMemory") + case AfcErrNotEnoughData: + return errors.New("NotEnoughData") + case AfcErrDirNotEmpty: + return errors.New("DirNotEmpty") + } + return nil +} + +const ( + AfcOperationInvalid = 0x00000000 /* Invalid */ + AfcOperationStatus = 0x00000001 /* Status */ + AfcOperationData = 0x00000002 /* Data */ + AfcOperationReadDir = 0x00000003 /* ReadDir */ + AfcOperationReadFile = 0x00000004 /* ReadFile */ + AfcOperationWriteFile = 0x00000005 /* WriteFile */ + AfcOperationWritePart = 0x00000006 /* WritePart */ + AfcOperationTruncateFile = 0x00000007 /* TruncateFile */ + AfcOperationRemovePath = 0x00000008 /* RemovePath */ + AfcOperationMakeDir = 0x00000009 /* MakeDir */ + AfcOperationGetFileInfo = 0x0000000A /* GetFileInfo */ + AfcOperationGetDeviceInfo = 0x0000000B /* GetDeviceInfo */ + AfcOperationWriteFileAtomic = 0x0000000C /* WriteFileAtomic (tmp file+rename) */ + AfcOperationFileOpen = 0x0000000D /* FileRefOpen */ + AfcOperationFileOpenResult = 0x0000000E /* FileRefOpenResult */ + AfcOperationFileRead = 0x0000000F /* FileRefRead */ + AfcOperationFileWrite = 0x00000010 /* FileRefWrite */ + AfcOperationFileSeek = 0x00000011 /* FileRefSeek */ + AfcOperationFileTell = 0x00000012 /* FileRefTell */ + AfcOperationFileTellResult = 0x00000013 /* FileRefTellResult */ + AfcOperationFileClose = 0x00000014 /* FileRefClose */ + AfcOperationFileSetSize = 0x00000015 /* FileRefSetFileSize (ftruncate) */ + AfcOperationGetConnectionInfo = 0x00000016 /* GetConnectionInfo */ + AfcOperationSetConnectionOptions = 0x00000017 /* SetConnectionOptions */ + AfcOperationRenamePath = 0x00000018 /* RenamePath */ + AfcOperationSetFSBlockSize = 0x00000019 /* SetFSBlockSize (0x800000) */ + AfcOperationSetSocketBlockSize = 0x0000001A /* SetSocketBlockSize (0x800000) */ + AfcOperationFileRefLock = 0x0000001B /* FileRefLock */ + AfcOperationMakeLink = 0x0000001C /* MakeLink */ + AfcOperationGetFileHash = 0x0000001D /* GetFileHash */ + AfcOperationSetFileModTime = 0x0000001E /* SetModTime */ + AfcOperationGetFileHashRange = 0x0000001F /* GetFileHashWithRange */ + /* iOS 6+ */ + AfcOperationFileSetImmutableHint = 0x00000020 /* FileRefSetImmutableHint */ + AfcOperationGetSizeOfPathContents = 0x00000021 /* GetSizeOfPathContents */ + AfcOperationRemovePathAndContents = 0x00000022 /* RemovePathAndContents */ + AfcOperationDirectoryEnumeratorRefOpen = 0x00000023 /* DirectoryEnumeratorRefOpen */ + AfcOperationDirectoryEnumeratorRefOpenResult = 0x00000024 /* DirectoryEnumeratorRefOpenResult */ + AfcOperationDirectoryEnumeratorRefRead = 0x00000025 /* DirectoryEnumeratorRefRead */ + AfcOperationDirectoryEnumeratorRefClose = 0x00000026 /* DirectoryEnumeratorRefClose */ + /* iOS 7+ */ + AfcOperationFileRefReadWithOffset = 0x00000027 /* FileRefReadWithOffset */ + AfcOperationFileRefWriteWithOffset = 0x00000028 /* FileRefWriteWithOffset */ +) + +const ( + AfcErrSuccess = 0 + AfcErrUnknownError = 1 + AfcErrOperationHeaderInvalid = 2 + AfcErrNoResources = 3 + AfcErrReadError = 4 + AfcErrWriteError = 5 + AfcErrUnknownPacketType = 6 + AfcErrInvalidArgument = 7 + AfcErrObjectNotFound = 8 + AfcErrObjectIsDir = 9 + AfcErrPermDenied = 10 + AfcErrServiceNotConnected = 11 + AfcErrOperationTimeout = 12 + AfcErrTooMuchData = 13 + AfcErrEndOfData = 14 + AfcErrOperationNotSupported = 15 + AfcErrObjectExists = 16 + AfcErrObjectBusy = 17 + AfcErrNoSpaceLeft = 18 + AfcErrOperationWouldBlock = 19 + AfcErrIoError = 20 + AfcErrOperationInterrupted = 21 + AfcErrOperationInProgress = 22 + AfcErrInternalError = 23 + AfcErrMuxError = 30 + AfcErrNoMemory = 31 + AfcErrNotEnoughData = 32 + AfcErrDirNotEmpty = 33 +) diff --git a/pkg/libimobiledevice/auxbuffer.go b/pkg/libimobiledevice/auxbuffer.go new file mode 100644 index 0000000..966d45f --- /dev/null +++ b/pkg/libimobiledevice/auxbuffer.go @@ -0,0 +1,137 @@ +package libimobiledevice + +import ( + "bytes" + "encoding/binary" + "errors" + "github.com/electricbubble/gidevice/pkg/nskeyedarchiver" +) + +type AuxBuffer struct { + buf *bytes.Buffer +} + +func NewAuxBuffer() *AuxBuffer { + return &AuxBuffer{ + buf: new(bytes.Buffer), + } +} + +func (m *AuxBuffer) AppendObject(obj interface{}) error { + marshal, err := nskeyedarchiver.Marshal(obj) + if err != nil { + return err + } + m.AppendUInt32(10) + m.AppendUInt32(2) + m.AppendUInt32(uint32(len(marshal))) + m.buf.Write(marshal) + + return nil +} + +func (m *AuxBuffer) AppendInt64(v int64) { + m.AppendUInt32(10) + m.AppendUInt32(4) + m.AppendUInt64(uint64(v)) +} + +func (m *AuxBuffer) AppendInt32(v int32) { + m.AppendUInt32(10) + m.AppendUInt32(3) + m.AppendUInt32(uint32(v)) +} + +func (m *AuxBuffer) AppendUInt32(v uint32) { + _ = binary.Write(m.buf, binary.LittleEndian, v) +} + +func (m *AuxBuffer) AppendUInt64(v uint64) { + _ = binary.Write(m.buf, binary.LittleEndian, v) +} + +func (m *AuxBuffer) AppendBytes(b []byte) { + m.buf.Write(b) +} + +func (m *AuxBuffer) Len() int { + return m.buf.Len() +} + +func (m *AuxBuffer) Bytes() []byte { + dup := m.buf.Bytes() + b := make([]byte, 16) + binary.LittleEndian.PutUint64(b, 0x01f0) + binary.LittleEndian.PutUint64(b[8:], uint64(m.Len())) + return append(b, dup...) +} + +func UnmarshalAuxBuffer(b []byte) ([]interface{}, error) { + reader := bytes.NewReader(b) + var magic, pkgLen uint64 + if err := binary.Read(reader, binary.LittleEndian, &magic); err != nil { + return nil, err + } + if err := binary.Read(reader, binary.LittleEndian, &pkgLen); err != nil { + return nil, err + } + + // if magic != 0x1df0 { + // TODO magic + // return nil, errors.New("magic not equal 0x1df0") + // } + + if pkgLen > uint64(len(b)-16) { + return nil, errors.New("package length not enough") + } + + var ret []interface{} + + for reader.Len() > 0 { + var flag, typ uint32 + if err := binary.Read(reader, binary.LittleEndian, &flag); err != nil { + return nil, err + } + if err := binary.Read(reader, binary.LittleEndian, &typ); err != nil { + return nil, err + } + switch typ { + case 2: + var l uint32 + if err := binary.Read(reader, binary.LittleEndian, &l); err != nil { + return nil, err + } + plistBuf := make([]byte, l) + if _, err := reader.Read(plistBuf); err != nil { + return nil, err + } + archiver := NewNSKeyedArchiver() + d, err := archiver.Unmarshal(plistBuf) + if err != nil { + return nil, err + } + ret = append(ret, d) + case 3, 5: + var i int32 + if err := binary.Read(reader, binary.LittleEndian, &i); err != nil { + return nil, err + } + ret = append(ret, i) + case 4, 6: + var i int64 + if err := binary.Read(reader, binary.LittleEndian, &i); err != nil { + return nil, err + } + ret = append(ret, i) + case 10: + // TODO Dictionary key + // fmt.Println("Dictionary key!") + continue + default: + // fmt.Printf("unknown type %d\n", typ) + break + } + } + + return ret, nil +} diff --git a/pkg/libimobiledevice/client_dtxmessage.go b/pkg/libimobiledevice/client_dtxmessage.go new file mode 100644 index 0000000..8574940 --- /dev/null +++ b/pkg/libimobiledevice/client_dtxmessage.go @@ -0,0 +1,380 @@ +package libimobiledevice + +import ( + "bytes" + "context" + "encoding/hex" + "fmt" + "github.com/electricbubble/gidevice/pkg/nskeyedarchiver" + "io" + "strings" + "sync" + "time" + "unsafe" +) + +const _unregistered = "_Unregistered_Golang-iDevice" + +func newDtxMessageClient(innerConn InnerConn) *dtxMessageClient { + c := &dtxMessageClient{ + innerConn: innerConn, + msgID: 0, + publishedChannels: make(map[string]int32), + openedChannels: make(map[string]uint32), + toReply: make(chan *dtxMessageHeaderPacket), + + mu: sync.Mutex{}, + resultMap: make(map[interface{}]*DTXMessageResult), + + callbackMap: make(map[string]func(m DTXMessageResult)), + } + c.RegisterCallback(_unregistered, func(m DTXMessageResult) {}) + c.startReceive() + c.startWaitingForReply() + c.ctx, c.cancelFunc = context.WithCancel(context.TODO()) + return c +} + +type dtxMessageClient struct { + innerConn InnerConn + msgID uint32 + + publishedChannels map[string]int32 + openedChannels map[string]uint32 + + toReply chan *dtxMessageHeaderPacket + + mu sync.Mutex + resultMap map[interface{}]*DTXMessageResult + + callbackMap map[string]func(m DTXMessageResult) + + ctx context.Context + cancelFunc context.CancelFunc +} + +func (c *dtxMessageClient) SendDTXMessage(selector string, aux []byte, channelCode uint32, expectsReply bool) (msgID uint32, err error) { + payload := new(dtxMessagePayloadPacket) + header := &dtxMessageHeaderPacket{ + ExpectsReply: 1, + } + + flag := 0x1000 + if !expectsReply { + flag = 0 + header.ExpectsReply = 0 + } + + var sel []byte + if sel, err = nskeyedarchiver.Marshal(selector); err != nil { + return 0, err + } + + if aux == nil { + aux = make([]byte, 0) + } + + payload.Flags = uint32(0x2 | flag) + payload.AuxiliaryLength = uint32(len(aux)) + payload.TotalLength = uint64(len(aux)) + uint64(len(sel)) + + header.Magic = 0x1F3D5B79 + header.CB = uint32(unsafe.Sizeof(*header)) + header.FragmentId = 0 + header.FragmentCount = 1 + header.Length = uint32(unsafe.Sizeof(*payload)) + uint32(payload.TotalLength) + c.msgID++ + header.Identifier = c.msgID + header.ConversationIndex = 0 + header.ChannelCode = channelCode + + msgPkt := new(dtxMessagePacket) + msgPkt.Header = header + msgPkt.Payload = payload + msgPkt.Aux = aux + msgPkt.Sel = sel + + raw, err := msgPkt.Pack() + if err != nil { + return 0, err + } + + debugLog(fmt.Sprintf("--> %s\n", msgPkt)) + msgID = header.Identifier + err = c.innerConn.Write(raw) + return +} + +func (c *dtxMessageClient) ReceiveDTXMessage() (result *DTXMessageResult, err error) { + bufPayload := new(bytes.Buffer) + + header := new(dtxMessageHeaderPacket) + var needToReply *dtxMessageHeaderPacket = nil + + for { + header = new(dtxMessageHeaderPacket) + + lenHeader := int(unsafe.Sizeof(*header)) + var bufHeader []byte + if bufHeader, err = c.innerConn.Read(lenHeader); err != nil { + return nil, fmt.Errorf("receive: length of DTXMessageHeader: %w", err) + } + + if header, err = header.unpack(bytes.NewBuffer(bufHeader)); err != nil { + return nil, fmt.Errorf("receive: DTXMessageHeader unpack: %w", err) + } + + if header.ExpectsReply == 1 { + needToReply = header + } + + if header.Magic != 0x1F3D5B79 { + return nil, fmt.Errorf("receive: bad magic %x", header.Magic) + } + + if header.ConversationIndex == 1 { + if header.Identifier != c.msgID { + return nil, fmt.Errorf("receive: except identifier %d new identifier %d", c.msgID, header.Identifier) + } + } else if header.ConversationIndex == 0 { + if header.Identifier > c.msgID { + c.msgID = header.Identifier + } + } else { + return nil, fmt.Errorf("receive: invalid conversationIndex %d", header.ConversationIndex) + } + + if header.FragmentId == 0 && header.FragmentCount > 1 { + continue + } + + var data []byte + if data, err = c.innerConn.Read(int(header.Length)); err != nil { + return nil, fmt.Errorf("receive: length of DTXMessageHeader: %w", err) + } + bufPayload.Write(data) + + if header.FragmentId == header.FragmentCount-1 { + break + } + } + + rawPayload := bufPayload.Bytes() + payload := new(dtxMessagePayloadPacket) + if payload, err = payload.unpack(bufPayload); err != nil { + return nil, fmt.Errorf("receive: unpack DTXMessagePayload: %w", err) + } + + compress := (payload.Flags & 0xff000) >> 12 + if compress != 0 { + return nil, fmt.Errorf("receive: message is compressed type %d", compress) + } + + payloadSize := uint32(unsafe.Sizeof(*payload)) + objOffset := payloadSize + payload.AuxiliaryLength + + aux := rawPayload[payloadSize : payloadSize+payload.AuxiliaryLength] + obj := rawPayload[objOffset : uint64(objOffset)+(payload.TotalLength-uint64(payload.AuxiliaryLength))] + + debugLog(fmt.Sprintf( + "<-- DTXMessage %s\n%s\n"+ + "%s\n%s\n", + header.String(), payload.String(), + hex.Dump(aux), hex.Dump(obj), + )) + + result = new(DTXMessageResult) + + if len(aux) > 0 { + if aux, err := UnmarshalAuxBuffer(aux); err != nil { + return nil, fmt.Errorf("receive: unpack AUX: %w", err) + } else { + result.Aux = aux + } + } + + if len(obj) > 0 { + if obj, err := NewNSKeyedArchiver().Unmarshal(obj); err != nil { + return nil, fmt.Errorf("receive: unpack NSKeyedArchiver: %w", err) + } else { + result.Obj = obj + } + } + + sObj, ok := result.Obj.(string) + if fn, do := c.callbackMap[sObj]; do { + fn(*result) + } else { + c.callbackMap[_unregistered](*result) + } + + if needToReply != nil { + go func() { c.toReply <- needToReply }() + } else { + var sk interface{} = header.Identifier + + if ok && sObj == "_notifyOfPublishedCapabilities:" { + sk = "_notifyOfPublishedCapabilities:" + } + c.mu.Lock() + c.resultMap[sk] = result + c.mu.Unlock() + + } + + return +} + +func (c *dtxMessageClient) Connection() (publishedChannels map[string]int32, err error) { + args := NewAuxBuffer() + if err = args.AppendObject(map[string]interface{}{ + "com.apple.private.DTXBlockCompression": uint64(2), + "com.apple.private.DTXConnection": uint64(1), + }); err != nil { + return nil, fmt.Errorf("connection DTXMessage: %w", err) + } + + selector := "_notifyOfPublishedCapabilities:" + if _, err = c.SendDTXMessage(selector, args.Bytes(), 0, false); err != nil { + return nil, fmt.Errorf("connection send: %w", err) + } + + var result *DTXMessageResult + if result, err = c.GetResult(selector); err != nil { + return nil, fmt.Errorf("connection receive: %w", err) + } + + if result.Obj.(string) != "_notifyOfPublishedCapabilities:" { + return nil, fmt.Errorf("connection: response mismatch: %s", result.Obj) + } + + aux := result.Aux[0].(map[string]interface{}) + for k, v := range aux { + c.publishedChannels[k] = int32(v.(uint64)) + } + + return c.publishedChannels, nil +} + +func (c *dtxMessageClient) MakeChannel(channel string) (id uint32, err error) { + var ok bool + if id, ok = c.openedChannels[channel]; ok { + return id, nil + } + + id = uint32(len(c.openedChannels) + 1) + args := NewAuxBuffer() + args.AppendInt32(int32(id)) + if err = args.AppendObject(channel); err != nil { + return 0, fmt.Errorf("make channel DTXMessage: %w", err) + } + + selector := "_requestChannelWithCode:identifier:" + + var msgID uint32 + if msgID, err = c.SendDTXMessage(selector, args.Bytes(), 0, true); err != nil { + return 0, fmt.Errorf("make channel send: %w", err) + } + + if _, err = c.GetResult(msgID); err != nil { + return 0, fmt.Errorf("make channel receive: %w", err) + } + + c.openedChannels[channel] = id + + return +} + +func (c *dtxMessageClient) RegisterCallback(obj string, cb func(m DTXMessageResult)) { + c.callbackMap[obj] = cb +} + +func (c *dtxMessageClient) GetResult(key interface{}) (*DTXMessageResult, error) { + startTime := time.Now() + for { + time.Sleep(100 * time.Millisecond) + c.mu.Lock() + if v, ok := c.resultMap[key]; ok { + delete(c.resultMap, key) + c.mu.Unlock() + return v, nil + } else { + c.mu.Unlock() + } + if elapsed := time.Since(startTime); elapsed > 30*time.Second { + return nil, fmt.Errorf("dtx: get result: timeout after %v", elapsed) + } + } +} + +func (c *dtxMessageClient) Close() { + c.cancelFunc() + c.innerConn.Close() +} + +func (c *dtxMessageClient) startReceive() { + go func() { + for { + select { + case <-c.ctx.Done(): + return + default: + if _, err := c.ReceiveDTXMessage(); err != nil { + debugLog(fmt.Sprintf("dtx: receive: %s", err)) + if strings.Contains(err.Error(), io.EOF.Error()) { + break + } + } + } + } + }() +} + +func (c *dtxMessageClient) startWaitingForReply() { + go func() { + for { + select { + case <-c.ctx.Done(): + return + case reqHeader := <-c.toReply: + replyPayload := new(dtxMessagePayloadPacket) + replyPayload.Flags = 0 + replyPayload.AuxiliaryLength = 0 + replyPayload.TotalLength = 0 + + replyHeader := new(dtxMessageHeaderPacket) + replyHeader.Magic = 0x1F3D5B79 + replyHeader.CB = uint32(unsafe.Sizeof(*replyHeader)) + replyHeader.FragmentId = 0 + replyHeader.FragmentCount = 1 + replyHeader.Length = uint32(unsafe.Sizeof(*replyPayload)) + uint32(replyPayload.TotalLength) + replyHeader.Identifier = reqHeader.Identifier + replyHeader.ConversationIndex = reqHeader.ConversationIndex + 1 + replyHeader.ChannelCode = reqHeader.ChannelCode + replyHeader.ExpectsReply = 0 + + replyPkt := new(dtxMessagePacket) + replyPkt.Header = replyHeader + replyPkt.Payload = replyPayload + replyPkt.Aux = nil + replyPkt.Sel = nil + + raw, err := replyPkt.Pack() + if err != nil { + debugLog(fmt.Sprintf("pack: reply DTXMessage: %s", err)) + continue + } + + if err = c.innerConn.Write(raw); err != nil { + debugLog(fmt.Sprintf("send: reply DTXMessage: %s", err)) + continue + } + } + } + }() +} + +type DTXMessageResult struct { + Obj interface{} + Aux []interface{} +} diff --git a/pkg/libimobiledevice/client_servicepacket.go b/pkg/libimobiledevice/client_servicepacket.go new file mode 100644 index 0000000..29d7540 --- /dev/null +++ b/pkg/libimobiledevice/client_servicepacket.go @@ -0,0 +1,80 @@ +package libimobiledevice + +import ( + "bytes" + "encoding/binary" + "fmt" + "howett.net/plist" +) + +func newServicePacketClient(innerConn InnerConn) *servicePacketClient { + return &servicePacketClient{ + innerConn: innerConn, + } +} + +type servicePacketClient struct { + innerConn InnerConn +} + +func (c *servicePacketClient) NewXmlPacket(req interface{}) (Packet, error) { + return c.newPacket(req, plist.XMLFormat) +} + +func (c *servicePacketClient) NewBinaryPacket(req interface{}) (Packet, error) { + return c.newPacket(req, plist.BinaryFormat) +} + +func (c *servicePacketClient) newPacket(req interface{}, format int) (Packet, error) { + pkt := new(servicePacket) + if buf, err := plist.Marshal(req, format); err != nil { + return nil, fmt.Errorf("plist packet marshal: %w", err) + } else { + pkt.body = buf + } + pkt.length = uint32(len(pkt.body)) + return pkt, nil +} + +func (c *servicePacketClient) SendPacket(pkt Packet) (err error) { + var raw []byte + if raw, err = pkt.Pack(); err != nil { + return fmt.Errorf("send packet: %w", err) + } + debugLog(fmt.Sprintf("--> %s\n", pkt)) + return c.innerConn.Write(raw) +} + +func (c *servicePacketClient) ReceivePacket() (respPkt Packet, err error) { + var bufLen []byte + if bufLen, err = c.innerConn.Read(4); err != nil { + return nil, fmt.Errorf("receive packet: %w", err) + } + lenPkg := binary.BigEndian.Uint32(bufLen) + + buffer := bytes.NewBuffer([]byte{}) + buffer.Write(bufLen) + + var buf []byte + if buf, err = c.innerConn.Read(int(lenPkg)); err != nil { + return nil, fmt.Errorf("receive packet: %w", err) + } + buffer.Write(buf) + + if respPkt, err = new(servicePacket).Unpack(buffer); err != nil { + return nil, fmt.Errorf("receive packet: %w", err) + } + + debugLog(fmt.Sprintf("<-- %s\n", respPkt)) + + var reply LockdownBasicResponse + if err = respPkt.Unmarshal(&reply); err != nil { + return nil, fmt.Errorf("receive packet: %w", err) + } + + if reply.Error != "" { + return nil, fmt.Errorf("receive packet: %s", reply.Error) + } + + return +} diff --git a/pkg/libimobiledevice/housearrest.go b/pkg/libimobiledevice/housearrest.go new file mode 100644 index 0000000..4d219a7 --- /dev/null +++ b/pkg/libimobiledevice/housearrest.go @@ -0,0 +1,56 @@ +package libimobiledevice + +const HouseArrestServiceName = "com.apple.mobile.house_arrest" + +const ( + CommandTypeVendDocuments CommandType = "VendDocuments" + CommandTypeVendContainer CommandType = "VendContainer" +) + +func NewHouseArrestClient(innerConn InnerConn) *HouseArrestClient { + return &HouseArrestClient{ + newServicePacketClient(innerConn), + } +} + +type HouseArrestClient struct { + client *servicePacketClient +} + +func (c *HouseArrestClient) NewBasicRequest(cmdType CommandType, bundleID string) *HouseArrestBasicRequest { + return &HouseArrestBasicRequest{ + Command: cmdType, + Identifier: bundleID, + } +} + +func (c *HouseArrestClient) NewDocumentsRequest(bundleID string) *HouseArrestBasicRequest { + return c.NewBasicRequest(CommandTypeVendDocuments, bundleID) +} + +func (c *HouseArrestClient) NewContainerRequest(bundleID string) *HouseArrestBasicRequest { + return c.NewBasicRequest(CommandTypeVendContainer, bundleID) +} + +func (c *HouseArrestClient) NewXmlPacket(req interface{}) (Packet, error) { + return c.client.NewXmlPacket(req) +} + +func (c *HouseArrestClient) SendPacket(pkt Packet) (err error) { + return c.client.SendPacket(pkt) +} + +func (c *HouseArrestClient) ReceivePacket() (respPkt Packet, err error) { + return c.client.ReceivePacket() +} + +func (c *HouseArrestClient) InnerConn() InnerConn { + return c.client.innerConn +} + +type ( + HouseArrestBasicRequest struct { + Command CommandType `plist:"Command"` + Identifier string `plist:"Identifier"` + } +) diff --git a/pkg/libimobiledevice/imagemounter.go b/pkg/libimobiledevice/imagemounter.go new file mode 100644 index 0000000..7dd7ba9 --- /dev/null +++ b/pkg/libimobiledevice/imagemounter.go @@ -0,0 +1,107 @@ +package libimobiledevice + +import ( + "errors" + "fmt" + "io" + "strings" +) + +const ImageMounterServiceName = "com.apple.mobile.mobile_image_mounter" + +var ErrDeviceLocked = errors.New("device locked") + +type CommandType string + +const ( + CommandTypeLookupImage CommandType = "LookupImage" + CommandTypeReceiveBytes CommandType = "ReceiveBytes" + CommandTypeMountImage CommandType = "MountImage" +) + +func NewImageMounterClient(innerConn InnerConn) *ImageMounterClient { + return &ImageMounterClient{ + client: newServicePacketClient(innerConn), + } +} + +type ImageMounterClient struct { + client *servicePacketClient +} + +func (c *ImageMounterClient) NewBasicRequest(cmdType CommandType, imgType string) *ImageMounterBasicRequest { + return &ImageMounterBasicRequest{ + Command: cmdType, + ImageType: imgType, + } +} + +func (c *ImageMounterClient) NewReceiveBytesRequest(imgType string, imgSize uint32, imgSignature []byte) *ImageMounterReceiveBytesRequest { + return &ImageMounterReceiveBytesRequest{ + ImageMounterBasicRequest: *c.NewBasicRequest(CommandTypeReceiveBytes, imgType), + ImageSize: imgSize, + ImageSignature: imgSignature, + } +} + +func (c *ImageMounterClient) NewMountImageRequest(imgType, imgPath string, imgSignature []byte) *ImageMounterMountImageRequest { + return &ImageMounterMountImageRequest{ + ImageMounterBasicRequest: *c.NewBasicRequest(CommandTypeMountImage, imgType), + ImagePath: imgPath, + ImageSignature: imgSignature, + } +} + +func (c *ImageMounterClient) NewXmlPacket(req interface{}) (Packet, error) { + return c.client.NewXmlPacket(req) +} + +func (c *ImageMounterClient) SendPacket(pkt Packet) (err error) { + return c.client.SendPacket(pkt) +} + +func (c *ImageMounterClient) ReceivePacket() (respPkt Packet, err error) { + respPkt, err = c.client.ReceivePacket() + if err != nil { + if strings.Contains(err.Error(), io.EOF.Error()) { + return nil, ErrDeviceLocked + } + } + return +} + +func (c *ImageMounterClient) SendDmg(data []byte) (err error) { + debugLog(fmt.Sprintf("--> ...DmgData...\n")) + return c.client.innerConn.Write(data) +} + +type ( + ImageMounterBasicRequest struct { + Command CommandType `plist:"Command"` + ImageType string `plist:"ImageType"` + } + + ImageMounterReceiveBytesRequest struct { + ImageMounterBasicRequest + ImageSignature []byte `plist:"ImageSignature"` + ImageSize uint32 `plist:"ImageSize"` + } + + ImageMounterMountImageRequest struct { + ImageMounterBasicRequest + ImagePath string `plist:"ImagePath"` + ImageSignature []byte `plist:"ImageSignature"` + } +) + +type ( + ImageMounterBasicResponse struct { + LockdownBasicResponse + Status string `plist:"Status"` + } + + ImageMounterLookupImageResponse struct { + ImageMounterBasicResponse + ImageSignature [][]byte `plist:"ImageSignature"` + } +) diff --git a/pkg/libimobiledevice/installationproxy.go b/pkg/libimobiledevice/installationproxy.go new file mode 100644 index 0000000..b4c66d7 --- /dev/null +++ b/pkg/libimobiledevice/installationproxy.go @@ -0,0 +1,79 @@ +package libimobiledevice + +const InstallationProxyServiceName = "com.apple.mobile.installation_proxy" + +const ( + CommandTypeBrowse CommandType = "Browse" + CommandTypeLookup CommandType = "Lookup" +) + +type ApplicationType string + +const ( + ApplicationTypeSystem ApplicationType = "System" + ApplicationTypeUser ApplicationType = "User" + ApplicationTypeInternal ApplicationType = "internal" + ApplicationTypeAny ApplicationType = "Any" +) + +func NewInstallationProxyClient(innerConn InnerConn) *InstallationProxyClient { + return &InstallationProxyClient{ + client: newServicePacketClient(innerConn), + } +} + +type InstallationProxyClient struct { + client *servicePacketClient +} + +func (c *InstallationProxyClient) NewBasicRequest(cmdType CommandType, opt *InstallationProxyOption) *InstallationProxyBasicRequest { + req := &InstallationProxyBasicRequest{Command: cmdType} + if opt != nil { + req.ClientOptions = opt + } + return req +} + +func (c *InstallationProxyClient) NewXmlPacket(req interface{}) (Packet, error) { + return c.client.NewXmlPacket(req) +} + +func (c *InstallationProxyClient) SendPacket(pkt Packet) (err error) { + return c.client.SendPacket(pkt) +} + +func (c *InstallationProxyClient) ReceivePacket() (respPkt Packet, err error) { + return c.client.ReceivePacket() +} + +type InstallationProxyOption struct { + ApplicationType ApplicationType `plist:"ApplicationType,omitempty"` + ReturnAttributes []string `plist:"ReturnAttributes,omitempty"` + MetaData bool `plist:"com.apple.mobile_installation.metadata,omitempty"` + BundleIDs []string `plist:"BundleIDs,omitempty"` /* for Lookup */ +} + +type ( + InstallationProxyBasicRequest struct { + Command CommandType `plist:"Command"` + ClientOptions *InstallationProxyOption `plist:"ClientOptions,omitempty"` + } +) + +type ( + InstallationProxyBasicResponse struct { + Status string `plist:"Status"` + } + + InstallationProxyLookupResponse struct { + InstallationProxyBasicResponse + LookupResult interface{} `plist:"LookupResult"` + } + + InstallationProxyBrowseResponse struct { + InstallationProxyBasicResponse + CurrentAmount int `json:"CurrentAmount"` + CurrentIndex int `json:"CurrentIndex"` + CurrentList []interface{} `json:"CurrentList"` + } +) diff --git a/pkg/libimobiledevice/instruments.go b/pkg/libimobiledevice/instruments.go new file mode 100644 index 0000000..63e9a2a --- /dev/null +++ b/pkg/libimobiledevice/instruments.go @@ -0,0 +1,41 @@ +package libimobiledevice + +const ( + InstrumentsServiceName = "com.apple.instruments.remoteserver" + InstrumentsSecureProxyServiceName = "com.apple.instruments.remoteserver.DVTSecureSocketProxy" +) + +func NewInstrumentsClient(innerConn InnerConn) *InstrumentsClient { + return &InstrumentsClient{ + client: newDtxMessageClient(innerConn), + } +} + +type InstrumentsClient struct { + client *dtxMessageClient +} + +func (c *InstrumentsClient) NotifyOfPublishedCapabilities() (publishedChannels map[string]int32, err error) { + return c.client.Connection() +} + +func (c *InstrumentsClient) RequestChannel(channel string) (id uint32, err error) { + return c.client.MakeChannel(channel) +} + +func (c *InstrumentsClient) Invoke(selector string, args *AuxBuffer, channelCode uint32, expectsReply bool) (result *DTXMessageResult, err error) { + var msgID uint32 + if msgID, err = c.client.SendDTXMessage(selector, args.Bytes(), channelCode, expectsReply); err != nil { + return nil, err + } + if expectsReply { + if result, err = c.client.GetResult(msgID); err != nil { + return nil, err + } + } + return +} + +func (c *InstrumentsClient) RegisterCallback(obj string, cb func(m DTXMessageResult)) { + c.client.RegisterCallback(obj, cb) +} diff --git a/pkg/libimobiledevice/keyedarchiver.go b/pkg/libimobiledevice/keyedarchiver.go new file mode 100644 index 0000000..1cb2e52 --- /dev/null +++ b/pkg/libimobiledevice/keyedarchiver.go @@ -0,0 +1,251 @@ +package libimobiledevice + +import ( + "reflect" + "time" + + "howett.net/plist" +) + +const nsNull = "$null" + +func newKeyedArchiver() *KeyedArchiver { + return &KeyedArchiver{ + Archiver: "NSKeyedArchiver", + Version: 100000, + } +} + +type KeyedArchiver struct { + Archiver string `plist:"$archiver"` + Objects []interface{} `plist:"$objects"` + Top ArchiverRoot `plist:"$top"` + Version int `plist:"$version"` +} + +func (ka *KeyedArchiver) UID() plist.UID { + return plist.UID(len(ka.Objects)) +} + +type ArchiverRoot struct { + Root plist.UID `plist:"root"` +} + +type ArchiverClasses struct { + Classes []string `plist:"$classes"` + ClassName string `plist:"$classname"` +} + +var ( + NSMutableDictionaryClass = &ArchiverClasses{ + Classes: []string{"NSMutableDictionary", "NSDictionary", "NSObject"}, + ClassName: "NSMutableDictionary", + } + NSDictionaryClass = &ArchiverClasses{ + Classes: []string{"NSDictionary", "NSObject"}, + ClassName: "NSDictionary", + } + NSMutableArrayClass = &ArchiverClasses{ + Classes: []string{"NSMutableArray", "NSArray", "NSObject"}, + ClassName: "NSMutableArray", + } + NSArrayClass = &ArchiverClasses{ + Classes: []string{"NSArray", "NSObject"}, + ClassName: "NSArray", + } + NSMutableDataClass = &ArchiverClasses{ + Classes: []string{"NSMutableArray", "NSArray", "NSObject"}, + ClassName: "NSMutableArray", + } + NSDataClass = &ArchiverClasses{ + Classes: []string{"NSData", "NSObject"}, + ClassName: "NSData", + } + NSDateClass = &ArchiverClasses{ + Classes: []string{"NSDate", "NSObject"}, + ClassName: "NSDate", + } + NSErrorClass = &ArchiverClasses{ + Classes: []string{"NSError", "NSObject"}, + ClassName: "NSError", + } +) + +type NSObject struct { + Class plist.UID `plist:"$class"` +} + +type NSArray struct { + NSObject + Values []plist.UID `plist:"NS.objects"` +} + +type NSDictionary struct { + NSArray + Keys []plist.UID `plist:"NS.keys"` +} + +type NSData struct { + NSObject + Data []byte `plist:"NS.data"` +} + +type NSError struct { + NSCode int + NSDomain string + NSUserInfo interface{} +} + +type NSKeyedArchiver struct { + objRefVal []interface{} + objRef map[interface{}]plist.UID +} + +func NewNSKeyedArchiver() *NSKeyedArchiver { + return &NSKeyedArchiver{ + objRef: make(map[interface{}]plist.UID), + } +} + +func (ka *NSKeyedArchiver) id(v interface{}) plist.UID { + var ref plist.UID + if id, ok := ka.objRef[v]; !ok { + ref = plist.UID(len(ka.objRef)) + ka.objRefVal = append(ka.objRefVal, v) + ka.objRef[v] = ref + } else { + ref = id + } + return ref +} + +func (ka *NSKeyedArchiver) flushToStruct(root *KeyedArchiver) { + for i := 0; i < len(ka.objRefVal); i++ { + val := ka.objRefVal[i] + vt := reflect.ValueOf(val) + if vt.Kind() == reflect.Ptr { + val = vt.Elem().Interface() + } + root.Objects = append(root.Objects, val) + } +} + +func (ka *NSKeyedArchiver) clear() { + ka.objRef = make(map[interface{}]plist.UID) + ka.objRefVal = []interface{}{} +} + +type XCTestConfiguration struct { + Contents map[string]interface{} +} + +func (ka *NSKeyedArchiver) Marshal(obj interface{}) ([]byte, error) { + val := reflect.ValueOf(obj) + typ := val.Type() + + root := newKeyedArchiver() + + var tmpTop plist.UID + + ka.id(nsNull) + + switch typ.Kind() { + case reflect.Map: + m := &NSDictionary{} + m.Class = ka.id(NSDictionaryClass) + keys := val.MapKeys() + for _, v := range keys { + m.Keys = append(m.Keys, ka.id(v.Interface())) + m.Values = append(m.Values, ka.id(val.MapIndex(v).Interface())) + } + tmpTop = ka.id(m) + case reflect.Slice, reflect.Array: + if typ.Elem().Kind() == reflect.Uint8 { + d := &NSData{} + d.Class = ka.id(NSDataClass) + var w []byte + for i := 0; i < val.Len(); i++ { + w = append(w, uint8(val.Index(i).Uint())) + } + d.Data = w + } + a := &NSArray{} + a.Class = ka.id(NSArrayClass) + for i := 0; i < val.Len(); i++ { + a.Values = append(a.Values, ka.id(val.Index(i).Interface())) + } + tmpTop = ka.id(a) + case reflect.String: + tmpTop = ka.id(obj) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + tmpTop = ka.id(obj) + } + + root.Top.Root = tmpTop + + ka.flushToStruct(root) + + ka.clear() + + return plist.Marshal(root, plist.BinaryFormat) +} + +func (ka *NSKeyedArchiver) convertValue(v interface{}) interface{} { + if m, ok := v.(map[string]interface{}); ok { + className := ka.objRefVal[m["$class"].(plist.UID)].(map[string]interface{})["$classname"] + + switch className { + case NSMutableDictionaryClass.Classes[0], NSDictionaryClass.Classes[0]: + ret := make(map[string]interface{}) + keys := m["NS.keys"].([]interface{}) + values := m["NS.objects"].([]interface{}) + + for i := 0; i < len(keys); i++ { + key := ka.objRefVal[keys[i].(plist.UID)].(string) + val := ka.convertValue(ka.objRefVal[values[i].(plist.UID)]) + ret[key] = val + } + return ret + case NSMutableArrayClass.Classes[0], NSArrayClass.Classes[0]: + ret := make([]interface{}, 0) + values := m["NS.objects"].([]interface{}) + for i := 0; i < len(values); i++ { + ret = append(ret, ka.convertValue(values[i])) + } + return ret + case NSMutableDataClass.Classes[0], NSDataClass.Classes[0]: + return m["NS.data"].([]byte) + case NSDateClass.Classes[0]: + return time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC). + Add(time.Duration(m["NS.time"].(float64)) * time.Second) + case NSErrorClass.Classes[0]: + err := &NSError{} + err.NSCode = int(m["NSCode"].(uint64)) + err.NSDomain = ka.objRefVal[m["NSDomain"].(plist.UID)].(string) + err.NSUserInfo = ka.convertValue(ka.objRefVal[m["NSUserInfo"].(plist.UID)]) + return *err + } + } else if uid, ok := v.(plist.UID); ok { + return ka.convertValue(ka.objRefVal[uid]) + } + return v +} + +func (ka *NSKeyedArchiver) Unmarshal(b []byte) (interface{}, error) { + archiver := new(KeyedArchiver) + + if _, err := plist.Unmarshal(b, archiver); err != nil { + return nil, err + } + + for _, v := range archiver.Objects { + ka.objRefVal = append(ka.objRefVal, v) + } + + ret := ka.convertValue(ka.objRefVal[archiver.Top.Root]) + + ka.clear() + + return ret, nil +} diff --git a/pkg/libimobiledevice/lib.go b/pkg/libimobiledevice/lib.go new file mode 100644 index 0000000..950981e --- /dev/null +++ b/pkg/libimobiledevice/lib.go @@ -0,0 +1,29 @@ +package libimobiledevice + +import ( + "bytes" + "fmt" + "log" +) + +type Packet interface { + Pack() ([]byte, error) + Unpack(buffer *bytes.Buffer) (Packet, error) + Unmarshal(v interface{}) error + + String() string +} + +var debugFlag = false + +// SetDebug sets debug mode +func SetDebug(debug bool) { + debugFlag = debug +} + +func debugLog(msg string) { + if !debugFlag { + return + } + log.Println(fmt.Sprintf("[%s-debug] %s", ProgramName, msg)) +} diff --git a/pkg/libimobiledevice/lockdown.go b/pkg/libimobiledevice/lockdown.go new file mode 100644 index 0000000..e96f6b9 --- /dev/null +++ b/pkg/libimobiledevice/lockdown.go @@ -0,0 +1,183 @@ +package libimobiledevice + +const ProtocolVersion = "2" + +const LockdownPort = 62078 + +type RequestType string + +const ( + RequestTypeQueryType RequestType = "QueryType" + RequestTypeSetValue RequestType = "SetValue" + RequestTypeGetValue RequestType = "GetValue" + RequestTypePair RequestType = "Pair" + RequestTypeEnterRecovery RequestType = "EnterRecovery" + RequestTypeStartSession RequestType = "StartSession" + RequestTypeStopSession RequestType = "StopSession" + RequestTypeStartService RequestType = "StartService" +) + +type LockdownType struct { + Type string `plist:"Type"` +} + +func NewLockdownClient(innerConn InnerConn) *LockdownClient { + return &LockdownClient{ + client: newServicePacketClient(innerConn), + } +} + +type LockdownClient struct { + client *servicePacketClient +} + +func (c *LockdownClient) NewBasicRequest(reqType RequestType) *LockdownBasicRequest { + return &LockdownBasicRequest{ + Label: BundleID, + ProtocolVersion: ProtocolVersion, + Request: reqType, + } +} + +func (c *LockdownClient) NewGetValueRequest(domain, key string) *LockdownValueRequest { + return &LockdownValueRequest{ + LockdownBasicRequest: *c.NewBasicRequest(RequestTypeGetValue), + Domain: domain, + Key: key, + } +} + +func (c *LockdownClient) NewSetValueRequest(domain, key string, value interface{}) *LockdownValueRequest { + return &LockdownValueRequest{ + LockdownBasicRequest: *c.NewBasicRequest(RequestTypeSetValue), + Domain: domain, + Key: key, + Value: value, + } +} + +func (c *LockdownClient) NewEnterRecoveryRequest() *LockdownBasicRequest { + return c.NewBasicRequest(RequestTypeEnterRecovery) +} + +func (c *LockdownClient) NewPairRequest(pairRecord *PairRecord) *LockdownPairRequest { + return &LockdownPairRequest{ + LockdownBasicRequest: *c.NewBasicRequest(RequestTypePair), + PairRecord: pairRecord, + PairingOptions: map[string]interface{}{ + "ExtendedPairingErrors": true, + }, + } +} + +func (c *LockdownClient) NewStartSessionRequest(buid, hostID string) *LockdownStartSessionRequest { + return &LockdownStartSessionRequest{ + LockdownBasicRequest: *c.NewBasicRequest(RequestTypeStartSession), + SystemBUID: buid, + HostID: hostID, + } +} + +func (c *LockdownClient) NewStopSessionRequest(sessionID string) *LockdownStopSessionRequest { + return &LockdownStopSessionRequest{ + LockdownBasicRequest: *c.NewBasicRequest(RequestTypeStopSession), + SessionID: sessionID, + } +} + +func (c *LockdownClient) NewStartServiceRequest(service string) *LockdownStartServiceRequest { + return &LockdownStartServiceRequest{ + LockdownBasicRequest: *c.NewBasicRequest(RequestTypeStartService), + Service: service, + } +} + +func (c *LockdownClient) NewXmlPacket(req interface{}) (Packet, error) { + return c.client.NewXmlPacket(req) +} + +func (c *LockdownClient) SendPacket(pkt Packet) (err error) { + return c.client.SendPacket(pkt) +} + +func (c *LockdownClient) ReceivePacket() (respPkt Packet, err error) { + return c.client.ReceivePacket() +} + +func (c *LockdownClient) EnableSSL(version []int, pairRecord *PairRecord) (err error) { + return c.client.innerConn.Handshake(version, pairRecord) +} + +type ( + LockdownBasicRequest struct { + Label string `plist:"Label"` + ProtocolVersion string `plist:"ProtocolVersion"` + Request RequestType `plist:"Request"` + } + + LockdownValueRequest struct { + LockdownBasicRequest + Domain string `plist:"Domain,omitempty"` + Key string `plist:"Key,omitempty"` + Value interface{} `plist:"Value,omitempty"` + } + + LockdownPairRequest struct { + LockdownBasicRequest + PairRecord *PairRecord `plist:"PairRecord"` + PairingOptions map[string]interface{} `plist:"PairingOptions"` + } + + LockdownStartSessionRequest struct { + LockdownBasicRequest + SystemBUID string `plist:"SystemBUID"` + HostID string `plist:"HostID"` + } + + LockdownStopSessionRequest struct { + LockdownBasicRequest + SessionID string `plist:"SessionID"` + } + + LockdownStartServiceRequest struct { + LockdownBasicRequest + Service string `plist:"Service"` + EscrowBag []byte `plist:"EscrowBag,omitempty"` + } +) + +type ( + LockdownBasicResponse struct { + Request string `plist:"Request"` + Error string `plist:"Error"` + } + + LockdownTypeResponse struct { + LockdownBasicResponse + Type string `plist:"Type"` + } + + LockdownValueResponse struct { + LockdownBasicResponse + Key string `plist:"Key"` + Value interface{} `plist:"Value"` + } + + LockdownPairResponse struct { + LockdownBasicResponse + EscrowBag []byte `plist:"EscrowBag"` + } + + LockdownStartSessionResponse struct { + LockdownBasicResponse + EnableSessionSSL bool `plist:"EnableSessionSSL"` + SessionID string `plist:"SessionID"` + } + + LockdownStartServiceResponse struct { + LockdownBasicResponse + EnableServiceSSL bool `plist:"EnableServiceSSL"` + Port int `plist:"Port"` + Service string `plist:"Service"` + } +) diff --git a/pkg/libimobiledevice/packet_afc.go b/pkg/libimobiledevice/packet_afc.go new file mode 100644 index 0000000..d6a73cf --- /dev/null +++ b/pkg/libimobiledevice/packet_afc.go @@ -0,0 +1,86 @@ +package libimobiledevice + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" +) + +var afcHeader = []byte{0x43, 0x46, 0x41, 0x36, 0x4C, 0x50, 0x41, 0x41} + +var _ Packet = (*afcPacket)(nil) + +type afcPacket struct { + entireLen uint64 + thisLen uint64 + packetNum uint64 + operation uint64 +} + +func (p *afcPacket) Pack() ([]byte, error) { + buf := new(bytes.Buffer) + buf.Write(afcHeader) + + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, p.entireLen) + buf.Write(b) + binary.LittleEndian.PutUint64(b, p.thisLen) + buf.Write(b) + binary.LittleEndian.PutUint64(b, p.packetNum) + buf.Write(b) + binary.LittleEndian.PutUint64(b, p.operation) + buf.Write(b) + + return buf.Bytes(), nil +} + +func (p *afcPacket) Unpack(buffer *bytes.Buffer) (Packet, error) { + return p.unpack(buffer) +} + +func (p *afcPacket) unpack(buffer *bytes.Buffer) (*afcPacket, error) { + magic := make([]byte, 8) + if _, err := buffer.Read(magic); err != nil { + return nil, fmt.Errorf("afc packet unpack: %w", err) + } + if bytes.Compare(magic, afcHeader) != 0 { + return nil, errors.New("afc packet unpack: header not match") + } + + respPkt := new(afcPacket) + if err := binary.Read(buffer, binary.LittleEndian, &respPkt.entireLen); err != nil { + return nil, fmt.Errorf("afc packet unpack: %w", err) + } + if err := binary.Read(buffer, binary.LittleEndian, &respPkt.thisLen); err != nil { + return nil, fmt.Errorf("afc packet unpack: %w", err) + } + if err := binary.Read(buffer, binary.LittleEndian, &respPkt.packetNum); err != nil { + return nil, fmt.Errorf("afc packet unpack: %w", err) + } + if err := binary.Read(buffer, binary.LittleEndian, &respPkt.operation); err != nil { + return nil, fmt.Errorf("afc packet unpack: %w", err) + } + return respPkt, nil +} + +func (p *afcPacket) Unmarshal(v interface{}) error { + // switch msg := v.(type) { + // case *AfcMessage: + // // msg.EntireLen = p.entireLen + // // msg.ThisLen = p.thisLen + // // msg.PacketNum = p.packetNum + // msg.Operation = p.operation + // default: + // return errors.New("the type of the method parameter must be '*AfcMessage'") + // } + // return nil + panic("never use (afcPacket)") +} + +func (p *afcPacket) String() string { + return fmt.Sprintf( + "EntireLen: %d, ThisLen: %d, PacketNum: %d, Operation: %X\n", + p.entireLen, p.thisLen, p.packetNum, p.operation, + ) +} diff --git a/pkg/libimobiledevice/packet_dtxmessage.go b/pkg/libimobiledevice/packet_dtxmessage.go new file mode 100644 index 0000000..aa8607c --- /dev/null +++ b/pkg/libimobiledevice/packet_dtxmessage.go @@ -0,0 +1,201 @@ +package libimobiledevice + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "fmt" +) + +var _ Packet = (*dtxMessagePayloadPacket)(nil) +var _ Packet = (*dtxMessageHeaderPacket)(nil) +var _ Packet = (*dtxMessagePacket)(nil) + +type dtxMessagePayloadPacket struct { + Flags uint32 + AuxiliaryLength uint32 + TotalLength uint64 +} + +func (p *dtxMessagePayloadPacket) Pack() ([]byte, error) { + buf := new(bytes.Buffer) + + b := make([]byte, 4) + binary.LittleEndian.PutUint32(b, p.Flags) + buf.Write(b) + binary.LittleEndian.PutUint32(b, p.AuxiliaryLength) + buf.Write(b) + + b = make([]byte, 8) + binary.LittleEndian.PutUint64(b, p.TotalLength) + buf.Write(b) + + return buf.Bytes(), nil +} + +func (p *dtxMessagePayloadPacket) Unpack(buffer *bytes.Buffer) (pkt Packet, err error) { + return p.unpack(buffer) +} + +func (p *dtxMessagePayloadPacket) unpack(buffer *bytes.Buffer) (pkt *dtxMessagePayloadPacket, err error) { + respPkt := new(dtxMessagePayloadPacket) + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.Flags); err != nil { + return nil, fmt.Errorf("packet (DTXMessagePayloadHeader) unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.AuxiliaryLength); err != nil { + return nil, fmt.Errorf("packet (DTXMessagePayloadHeader) unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.TotalLength); err != nil { + return nil, fmt.Errorf("packet (DTXMessagePayloadHeader) unpack: %w", err) + } + return respPkt, nil +} + +func (p *dtxMessagePayloadPacket) Unmarshal(v interface{}) error { + panic("never use (dtxMessagePayloadHeader)") +} + +func (p *dtxMessagePayloadPacket) String() string { + return fmt.Sprintf("DTXMessagePayloadHeader Flags: %d, AuxiliaryLength: %d, TotalLength: %d\n", + p.Flags, p.AuxiliaryLength, p.TotalLength, + ) +} + +type dtxMessageHeaderPacket struct { + Magic uint32 + CB uint32 + FragmentId uint16 + FragmentCount uint16 + Length uint32 + Identifier uint32 + ConversationIndex uint32 + ChannelCode uint32 + ExpectsReply uint32 +} + +func (p *dtxMessageHeaderPacket) Pack() ([]byte, error) { + buf := new(bytes.Buffer) + + b := make([]byte, 4) + binary.LittleEndian.PutUint32(b, p.Magic) + buf.Write(b) + binary.LittleEndian.PutUint32(b, p.CB) + buf.Write(b) + + b = make([]byte, 2) + binary.LittleEndian.PutUint16(b, p.FragmentId) + buf.Write(b) + binary.LittleEndian.PutUint16(b, p.FragmentCount) + buf.Write(b) + + b = make([]byte, 4) + binary.LittleEndian.PutUint32(b, p.Length) + buf.Write(b) + binary.LittleEndian.PutUint32(b, p.Identifier) + buf.Write(b) + binary.LittleEndian.PutUint32(b, p.ConversationIndex) + buf.Write(b) + binary.LittleEndian.PutUint32(b, p.ChannelCode) + buf.Write(b) + binary.LittleEndian.PutUint32(b, p.ExpectsReply) + buf.Write(b) + + return buf.Bytes(), nil +} + +func (p *dtxMessageHeaderPacket) Unpack(buffer *bytes.Buffer) (pkt Packet, err error) { + return p.unpack(buffer) +} + +func (p *dtxMessageHeaderPacket) unpack(buffer *bytes.Buffer) (pkt *dtxMessageHeaderPacket, err error) { + respPkt := new(dtxMessageHeaderPacket) + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.Magic); err != nil { + return nil, fmt.Errorf("packet (DTXMessageHeader) unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.CB); err != nil { + return nil, fmt.Errorf("packet (DTXMessageHeader) unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.FragmentId); err != nil { + return nil, fmt.Errorf("packet (DTXMessageHeader) unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.FragmentCount); err != nil { + return nil, fmt.Errorf("packet (DTXMessageHeader) unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.Length); err != nil { + return nil, fmt.Errorf("packet (DTXMessageHeader) unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.Identifier); err != nil { + return nil, fmt.Errorf("packet (DTXMessageHeader) unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.ConversationIndex); err != nil { + return nil, fmt.Errorf("packet (DTXMessageHeader) unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.ChannelCode); err != nil { + return nil, fmt.Errorf("packet (DTXMessageHeader) unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.ExpectsReply); err != nil { + return nil, fmt.Errorf("packet (DTXMessageHeader) unpack: %w", err) + } + return respPkt, nil +} + +func (p *dtxMessageHeaderPacket) Unmarshal(v interface{}) error { + panic("never use (DTXMessageHeader)") +} + +func (p *dtxMessageHeaderPacket) String() string { + return fmt.Sprintf("DTXMessageHeader Magic: %d, CB: %d, FragmentId: %d, FragmentCount: %d\n"+ + "Length: %d, Identifier: %d, ConversationIndex: %d, ChannelCode: %d, ExpectsReply: %d\n", + p.Magic, p.CB, p.FragmentId, p.FragmentCount, + p.Length, p.Identifier, p.ConversationIndex, p.ChannelCode, p.ExpectsReply, + ) +} + +type dtxMessagePacket struct { + Header *dtxMessageHeaderPacket + Payload *dtxMessagePayloadPacket + Aux []byte + Sel []byte +} + +func (p *dtxMessagePacket) Pack() ([]byte, error) { + buf := new(bytes.Buffer) + + raw, err := p.Header.Pack() + if err != nil { + return nil, fmt.Errorf("packet (DTXMessagePacket) pack: %w", err) + } + buf.Write(raw) + + if raw, err = p.Payload.Pack(); err != nil { + return nil, fmt.Errorf("packet (DTXMessagePacket) pack: %w", err) + } + buf.Write(raw) + + if p.Aux != nil || len(p.Aux) != 0 { + buf.Write(p.Aux) + } + if p.Sel != nil || len(p.Sel) != 0 { + buf.Write(p.Sel) + } + + return buf.Bytes(), nil +} + +func (p *dtxMessagePacket) Unpack(buffer *bytes.Buffer) (Packet, error) { + panic("implement me") +} + +func (p *dtxMessagePacket) Unmarshal(v interface{}) error { + panic("never use (DTXMessagePacket)") +} + +func (p *dtxMessagePacket) String() string { + return fmt.Sprintf( + "DTXMessagePacket %s\n%s\n"+ + "%s\n%s\n", + p.Header.String(), p.Payload.String(), + // p.Aux, p.Sel, + hex.Dump(p.Aux), hex.Dump(p.Sel), + ) +} diff --git a/pkg/libimobiledevice/packet_location.go b/pkg/libimobiledevice/packet_location.go new file mode 100644 index 0000000..9085873 --- /dev/null +++ b/pkg/libimobiledevice/packet_location.go @@ -0,0 +1,53 @@ +package libimobiledevice + +import ( + "bytes" + "encoding/binary" + "fmt" + "strconv" +) + +var _ Packet = (*locationPacket)(nil) + +type locationPacket struct { + lon float64 + lat float64 +} + +func (l *locationPacket) Pack() ([]byte, error) { + buf := new(bytes.Buffer) + + if err := binary.Write(buf, binary.BigEndian, uint32(0)); err != nil { + return nil, fmt.Errorf("packet (location) pack: %w", err) + } + + latS := []byte(strconv.FormatFloat(l.lat, 'E', -1, 64)) + if err := binary.Write(buf, binary.BigEndian, uint32(len(latS))); err != nil { + return nil, fmt.Errorf("packet (location) pack: %w", err) + } + if err := binary.Write(buf, binary.BigEndian, latS); err != nil { + return nil, fmt.Errorf("packet (location) pack: %w", err) + } + + lonS := []byte(strconv.FormatFloat(l.lon, 'E', -1, 64)) + if err := binary.Write(buf, binary.BigEndian, uint32(len(lonS))); err != nil { + return nil, fmt.Errorf("packet (location) pack: %w", err) + } + if err := binary.Write(buf, binary.BigEndian, lonS); err != nil { + return nil, fmt.Errorf("packet (location) pack: %w", err) + } + + return buf.Bytes(), nil +} + +func (l *locationPacket) Unpack(buffer *bytes.Buffer) (Packet, error) { + panic("never use (location)") +} + +func (l *locationPacket) Unmarshal(v interface{}) error { + panic("never use (location)") +} + +func (l *locationPacket) String() string { + return fmt.Sprintf("lon: %v, lat: %v\n", l.lon, l.lat) +} diff --git a/pkg/libimobiledevice/packet_service.go b/pkg/libimobiledevice/packet_service.go new file mode 100644 index 0000000..03680f9 --- /dev/null +++ b/pkg/libimobiledevice/packet_service.go @@ -0,0 +1,46 @@ +package libimobiledevice + +import ( + "bytes" + "encoding/binary" + "fmt" + "howett.net/plist" +) + +var _ Packet = (*servicePacket)(nil) + +type servicePacket struct { + length uint32 + body []byte +} + +func (p *servicePacket) Pack() ([]byte, error) { + buf := new(bytes.Buffer) + + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, p.length) + buf.Write(b) + + buf.Write(p.body) + + return buf.Bytes(), nil +} + +func (p *servicePacket) Unpack(buffer *bytes.Buffer) (pkt Packet, err error) { + respPkt := new(servicePacket) + if err = binary.Read(buffer, binary.BigEndian, &respPkt.length); err != nil { + return nil, fmt.Errorf("packet (service) unpack: %w", err) + } + respPkt.body = buffer.Bytes() + + return respPkt, nil +} + +func (p *servicePacket) Unmarshal(v interface{}) (err error) { + _, err = plist.Unmarshal(p.body, v) + return +} + +func (p *servicePacket) String() string { + return fmt.Sprintf("Length: %d\n%s", p.length, p.body) +} diff --git a/pkg/libimobiledevice/packet_usbmux.go b/pkg/libimobiledevice/packet_usbmux.go new file mode 100644 index 0000000..630ca7d --- /dev/null +++ b/pkg/libimobiledevice/packet_usbmux.go @@ -0,0 +1,111 @@ +package libimobiledevice + +import ( + "bytes" + "encoding/binary" + "fmt" + "howett.net/plist" +) + +var _ Packet = (*packet)(nil) + +type packet struct { + length uint32 + version ProtoVersion + msgType ProtoMessageType + tag uint32 + body []byte +} + +func (p *packet) Pack() ([]byte, error) { + buf := new(bytes.Buffer) + + b := make([]byte, 4) + binary.LittleEndian.PutUint32(b, p.length) + buf.Write(b) + binary.LittleEndian.PutUint32(b, uint32(p.version)) + buf.Write(b) + binary.LittleEndian.PutUint32(b, uint32(p.msgType)) + buf.Write(b) + binary.LittleEndian.PutUint32(b, p.tag) + buf.Write(b) + + buf.Write(p.body) + + return buf.Bytes(), nil +} + +func (p *packet) Unpack(buffer *bytes.Buffer) (pkt Packet, err error) { + respPkt := new(packet) + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.length); err != nil { + return nil, fmt.Errorf("packet unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.version); err != nil { + return nil, fmt.Errorf("packet unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.msgType); err != nil { + return nil, fmt.Errorf("packet unpack: %w", err) + } + if err = binary.Read(buffer, binary.LittleEndian, &respPkt.tag); err != nil { + return nil, fmt.Errorf("packet unpack: %w", err) + } + respPkt.body = buffer.Bytes() + return respPkt, nil +} + +func (p *packet) Unmarshal(v interface{}) (err error) { + _, err = plist.Unmarshal(p.body, v) + return +} + +func (p *packet) String() string { + return fmt.Sprintf( + "Length: %d, Version: %d, Type: %d, Tag: %d\n%s", + p.length, p.version, p.msgType, p.tag, p.body, + ) +} + +type ( + BasicRequest struct { + MessageType MessageType `plist:"MessageType"` + BundleID string `plist:"BundleID,omitempty"` + ProgramName string `plist:"ProgName,omitempty"` + ClientVersionString string `plist:"ClientVersionString"` + LibUSBMuxVersion uint `plist:"kLibUSBMuxVersion"` + } + + ConnectRequest struct { + BasicRequest + DeviceID int `plist:"DeviceID"` + PortNumber int `plist:"PortNumber"` + } + + ReadPairRecordRequest struct { + BasicRequest + PairRecordID string `plist:"PairRecordID"` + } + + SavePairRecordRequest struct { + BasicRequest + PairRecordID string `plist:"PairRecordID"` + PairRecordData []byte `plist:"PairRecordData"` + DeviceID int `plist:"DeviceID"` + } + + DeletePairRecordRequest struct { + BasicRequest + PairRecordID string `plist:"PairRecordID"` + } +) + +type PairRecord struct { + DeviceCertificate []byte `plist:"DeviceCertificate"` + EscrowBag []byte `plist:"EscrowBag,omitempty"` + HostCertificate []byte `plist:"HostCertificate"` + HostPrivateKey []byte `plist:"HostPrivateKey,omitempty"` + HostID string `plist:"HostID"` + RootCertificate []byte `plist:"RootCertificate"` + RootPrivateKey []byte `plist:"RootPrivateKey,omitempty"` + SystemBUID string `plist:"SystemBUID"` + WiFiMACAddress string `plist:"WiFiMACAddress,omitempty"` +} diff --git a/pkg/libimobiledevice/screenshot.go b/pkg/libimobiledevice/screenshot.go new file mode 100644 index 0000000..96d4b33 --- /dev/null +++ b/pkg/libimobiledevice/screenshot.go @@ -0,0 +1,52 @@ +package libimobiledevice + +import ( + "bytes" + "encoding/binary" + "fmt" +) + +const ScreenshotServiceName = "com.apple.mobile.screenshotr" + +func NewScreenshotClient(innerConn InnerConn) *ScreenshotClient { + return &ScreenshotClient{ + client: newServicePacketClient(innerConn), + } +} + +type ScreenshotClient struct { + client *servicePacketClient +} + +func (c *ScreenshotClient) NewBinaryPacket(req interface{}) (Packet, error) { + return c.client.NewBinaryPacket(req) +} + +func (c *ScreenshotClient) SendPacket(pkt Packet) (err error) { + return c.client.SendPacket(pkt) +} + +func (c *ScreenshotClient) ReceivePacket() (respPkt Packet, err error) { + var bufLen []byte + if bufLen, err = c.client.innerConn.Read(4); err != nil { + return nil, fmt.Errorf("lockdown(Screenshot) receive: %w", err) + } + lenPkg := binary.BigEndian.Uint32(bufLen) + + buffer := bytes.NewBuffer([]byte{}) + buffer.Write(bufLen) + + var buf []byte + if buf, err = c.client.innerConn.Read(int(lenPkg)); err != nil { + return nil, fmt.Errorf("lockdown(Screenshot) receive: %w", err) + } + buffer.Write(buf) + + if respPkt, err = new(servicePacket).Unpack(buffer); err != nil { + return nil, fmt.Errorf("lockdown(Screenshot) receive: %w", err) + } + + debugLog(fmt.Sprintf("<-- %s\n", respPkt)) + + return +} diff --git a/pkg/libimobiledevice/simulatelocation.go b/pkg/libimobiledevice/simulatelocation.go new file mode 100644 index 0000000..6d0316b --- /dev/null +++ b/pkg/libimobiledevice/simulatelocation.go @@ -0,0 +1,128 @@ +package libimobiledevice + +import ( + "fmt" + "math" + "strings" +) + +const SimulateLocationServiceName = "com.apple.dt.simulatelocation" + +type CoordinateSystem string + +const ( + CoordinateSystemWGS84 CoordinateSystem = "WGS84" + CoordinateSystemBD09 CoordinateSystem = "BD09" + CoordinateSystemGCJ02 CoordinateSystem = "GCJ02" +) + +func NewSimulateLocationClient(innerConn InnerConn) *SimulateLocationClient { + return &SimulateLocationClient{ + client: newServicePacketClient(innerConn), + } +} + +type SimulateLocationClient struct { + client *servicePacketClient +} + +func (c *SimulateLocationClient) NewLocationPacket(lon, lat float64, coordinateSystem CoordinateSystem) Packet { + switch CoordinateSystem(strings.ToUpper(string(coordinateSystem))) { + case CoordinateSystemGCJ02: + lon, lat = gcj02ToWGS84(lon, lat) + case CoordinateSystemBD09: + lon, lat = bd09ToWGS84(lon, lat) + case CoordinateSystemWGS84: + _, _ = lon, lat + default: + _, _ = lon, lat + } + + pkt := new(locationPacket) + pkt.lon = lon + pkt.lat = lat + return pkt +} + +func (c *SimulateLocationClient) SendPacket(pkt Packet) (err error) { + return c.client.SendPacket(pkt) +} + +// Recover try to revert back +func (c *SimulateLocationClient) Recover() error { + data := []byte{0x00, 0x00, 0x00, 0x01} + debugLog(fmt.Sprintf("--> %+v\n", data)) + return c.client.innerConn.Write(data) +} + +const ( + xPi = math.Pi * 3000.0 / 180.0 + offset = 0.00669342162296594323 + axis = 6378245.0 +) + +func isOutOfChina(lon, lat float64) bool { + return !(lon > 73.66 && lon < 135.05 && lat > 3.86 && lat < 53.55) +} + +func delta(lon, lat float64) (float64, float64) { + dLat := transformLat(lon-105.0, lat-35.0) + dLon := transformLng(lon-105.0, lat-35.0) + + radLat := lat / 180.0 * math.Pi + magic := math.Sin(radLat) + magic = 1 - offset*magic*magic + sqrtMagic := math.Sqrt(magic) + + dLat = (dLat * 180.0) / ((axis * (1 - offset)) / (magic * sqrtMagic) * math.Pi) + dLon = (dLon * 180.0) / (axis / sqrtMagic * math.Cos(radLat) * math.Pi) + + mgLat := lat + dLat + mgLon := lon + dLon + + return mgLon, mgLat +} + +func transformLat(lon, lat float64) float64 { + var ret = -100.0 + 2.0*lon + 3.0*lat + 0.2*lat*lat + 0.1*lon*lat + 0.2*math.Sqrt(math.Abs(lon)) + ret += (20.0*math.Sin(6.0*lon*math.Pi) + 20.0*math.Sin(2.0*lon*math.Pi)) * 2.0 / 3.0 + ret += (20.0*math.Sin(lat*math.Pi) + 40.0*math.Sin(lat/3.0*math.Pi)) * 2.0 / 3.0 + ret += (160.0*math.Sin(lat/12.0*math.Pi) + 320*math.Sin(lat*math.Pi/30.0)) * 2.0 / 3.0 + return ret +} + +func transformLng(lon, lat float64) float64 { + var ret = 300.0 + lon + 2.0*lat + 0.1*lon*lon + 0.1*lon*lat + 0.1*math.Sqrt(math.Abs(lon)) + ret += (20.0*math.Sin(6.0*lon*math.Pi) + 20.0*math.Sin(2.0*lon*math.Pi)) * 2.0 / 3.0 + ret += (20.0*math.Sin(lon*math.Pi) + 40.0*math.Sin(lon/3.0*math.Pi)) * 2.0 / 3.0 + ret += (150.0*math.Sin(lon/12.0*math.Pi) + 300.0*math.Sin(lon/30.0*math.Pi)) * 2.0 / 3.0 + return ret +} + +func gcj02ToWGS84(lon, lat float64) (float64, float64) { + if isOutOfChina(lon, lat) { + return lon, lat + } + + mgLon, mgLat := delta(lon, lat) + + return lon*2 - mgLon, lat*2 - mgLat +} + +func bd09ToGCJ02(lon, lat float64) (float64, float64) { + x := lon - 0.0065 + y := lat - 0.006 + + z := math.Sqrt(x*x+y*y) - 0.00002*math.Sin(y*xPi) + theta := math.Atan2(y, x) - 0.000003*math.Cos(x*xPi) + + gLon := z * math.Cos(theta) + gLat := z * math.Sin(theta) + + return gLon, gLat +} + +func bd09ToWGS84(lon, lat float64) (float64, float64) { + lon, lat = bd09ToGCJ02(lon, lat) + return gcj02ToWGS84(lon, lat) +} diff --git a/pkg/libimobiledevice/testmanagerd.go b/pkg/libimobiledevice/testmanagerd.go new file mode 100644 index 0000000..64c88c2 --- /dev/null +++ b/pkg/libimobiledevice/testmanagerd.go @@ -0,0 +1,45 @@ +package libimobiledevice + +const ( + TestmanagerdSecureServiceName = "com.apple.testmanagerd.lockdown.secure" + TestmanagerdServiceName = "com.apple.testmanagerd.lockdown" +) + +func NewTestmanagerdClient(innerConn InnerConn) *TestmanagerdClient { + return &TestmanagerdClient{ + client: newDtxMessageClient(innerConn), + } +} + +type TestmanagerdClient struct { + client *dtxMessageClient +} + +func (t *TestmanagerdClient) Connection() (publishedChannels map[string]int32, err error) { + return t.client.Connection() +} + +func (t *TestmanagerdClient) MakeChannel(channel string) (id uint32, err error) { + return t.client.MakeChannel(channel) +} + +func (t *TestmanagerdClient) Invoke(selector string, args *AuxBuffer, channelCode uint32, expectsReply bool) (result *DTXMessageResult, err error) { + var msgID uint32 + if msgID, err = t.client.SendDTXMessage(selector, args.Bytes(), channelCode, expectsReply); err != nil { + return nil, err + } + if expectsReply { + if result, err = t.client.GetResult(msgID); err != nil { + return nil, err + } + } + return +} + +func (t *TestmanagerdClient) RegisterCallback(obj string, cb func(m DTXMessageResult)) { + t.client.RegisterCallback(obj, cb) +} + +func (t *TestmanagerdClient) Close() { + t.client.Close() +} diff --git a/pkg/libimobiledevice/usbmux.go b/pkg/libimobiledevice/usbmux.go new file mode 100644 index 0000000..3792e7d --- /dev/null +++ b/pkg/libimobiledevice/usbmux.go @@ -0,0 +1,423 @@ +package libimobiledevice + +import ( + "bytes" + "crypto/tls" + "encoding/binary" + "fmt" + "howett.net/plist" + "net" + "runtime" + "strconv" + "time" +) + +var DefaultDeadlineTimeout = 30 * time.Second + +const ( + BundleID = "electricbubble.libimobiledevice" + ProgramName = "libimobiledevice" + ClientVersion = "libimobiledevice-beta" + LibUSBMuxVersion = 3 +) + +type ReplyCode uint64 + +const ( + ReplyCodeOK ReplyCode = iota + ReplyCodeBadCommand + ReplyCodeBadDevice + ReplyCodeConnectionRefused + _ // ignore `4` + _ // ignore `5` + ReplyCodeBadVersion +) + +func (rc ReplyCode) String() string { + switch rc { + case ReplyCodeOK: + return "ok" + case ReplyCodeBadCommand: + return "bad command" + case ReplyCodeBadDevice: + return "bad device" + case ReplyCodeConnectionRefused: + return "connection refused" + case ReplyCodeBadVersion: + return "bad version" + default: + return "unknown reply code: " + strconv.Itoa(int(rc)) + } +} + +type ProtoVersion uint32 + +// proto_version == 1 +// construct message plist +// else `0`? res == `RESULT_BADVERSION` +// binary packet + +const ( + ProtoVersionBinary ProtoVersion = iota + ProtoVersionPlist +) + +type ProtoMessageType uint32 + +const ( + _ ProtoMessageType = iota + ProtoMessageTypeResult + ProtoMessageTypeConnect + ProtoMessageTypeListen + ProtoMessageTypeDeviceAdd + ProtoMessageTypeDeviceRemove + ProtoMessageTypeDevicePaired + _ // `7` + ProtoMessageTypePlist +) + +type MessageType string + +const ( + MessageTypeResult MessageType = "Result" + MessageTypeConnect MessageType = "Connect" + MessageTypeListen MessageType = "Listen" + MessageTypeDeviceAdd MessageType = "Attached" + MessageTypeDeviceRemove MessageType = "Detached" + MessageTypeReadBUID MessageType = "ReadBUID" + MessageTypeReadPairRecord MessageType = "ReadPairRecord" + MessageTypeSavePairRecord MessageType = "SavePairRecord" + MessageTypeDeletePairRecord MessageType = "DeletePairRecord" + MessageTypeDeviceList MessageType = "ListDevices" +) + +type BaseDevice struct { + MessageType MessageType `plist:"MessageType"` + DeviceID int `plist:"DeviceID"` + Properties DeviceProperties `plist:"Properties"` +} + +type DeviceProperties struct { + DeviceID int `plist:"DeviceID"` + ConnectionType string `plist:"ConnectionType"` + ConnectionSpeed int `plist:"ConnectionSpeed"` + ProductID int `plist:"ProductID"` + LocationID int `plist:"LocationID"` + SerialNumber string `plist:"SerialNumber"` + UDID string `plist:"UDID"` + USBSerialNumber string `plist:"USBSerialNumber"` + + EscapedFullServiceName string `plist:"EscapedFullServiceName"` + InterfaceIndex int `plist:"InterfaceIndex"` + NetworkAddress uint8 `plist:"NetworkAddress"` +} + +func NewUsbmuxClient(opts ...InnerConnOption) (c *UsbmuxClient, err error) { + c = &UsbmuxClient{version: ProtoVersionPlist} + var conn net.Conn + if conn, err = rawDial(); err != nil { + return nil, fmt.Errorf("usbmux connect: %w", err) + } + + innerConn := newInnerConn(conn) + for _, opt := range opts { + opt(innerConn) + } + c.innerConn = innerConn + return +} + +type UsbmuxClient struct { + innerConn InnerConn + version ProtoVersion + tag uint32 +} + +func (c *UsbmuxClient) NewBasicRequest(msgType MessageType) *BasicRequest { + return &BasicRequest{ + MessageType: msgType, + BundleID: BundleID, + ProgramName: ProgramName, + ClientVersionString: ClientVersion, + LibUSBMuxVersion: LibUSBMuxVersion, + } +} + +func (c *UsbmuxClient) NewConnectRequest(deviceID, port int) *ConnectRequest { + return &ConnectRequest{ + BasicRequest: *c.NewBasicRequest(MessageTypeConnect), + DeviceID: deviceID, + PortNumber: ((port << 8) & 0xFF00) | (port >> 8), + } +} + +func (c *UsbmuxClient) NewReadPairRecordRequest(udid string) *ReadPairRecordRequest { + return &ReadPairRecordRequest{ + BasicRequest: *c.NewBasicRequest(MessageTypeReadPairRecord), + PairRecordID: udid, + } +} + +func (c *UsbmuxClient) NewSavePairRecordRequest(udid string, deviceID int, data []byte) *SavePairRecordRequest { + return &SavePairRecordRequest{ + BasicRequest: *c.NewBasicRequest(MessageTypeSavePairRecord), + PairRecordID: udid, + PairRecordData: data, + DeviceID: deviceID, + } +} + +func (c *UsbmuxClient) NewDeletePairRecordRequest(udid string) *DeletePairRecordRequest { + return &DeletePairRecordRequest{ + BasicRequest: *c.NewBasicRequest(MessageTypeDeletePairRecord), + PairRecordID: udid, + } +} + +func (c *UsbmuxClient) NewPacket(protoMsgType ProtoMessageType) Packet { + return c.newPacket(protoMsgType) +} + +func (c *UsbmuxClient) newPacket(protoMsgType ProtoMessageType) *packet { + c.tag++ + pkt := &packet{ + version: c.version, + msgType: protoMsgType, + tag: c.tag, + } + return pkt +} + +func (c *UsbmuxClient) NewPlistPacket(req interface{}) (Packet, error) { + pkt := c.newPacket(ProtoMessageTypePlist) + if buf, err := plist.Marshal(req, plist.XMLFormat); err != nil { + return nil, fmt.Errorf("plist packet marshal: %w", err) + } else { + pkt.body = buf + } + pkt.length = uint32(len(pkt.body) + 4*4) + return pkt, nil +} + +func (c *UsbmuxClient) SendPacket(pkt Packet) (err error) { + var raw []byte + if raw, err = pkt.Pack(); err != nil { + return fmt.Errorf("usbmux send: %w", err) + } + // debugLog(fmt.Sprintf("--> Length: %d, Version: %d, Type: %d, Tag: %d\n%s\n", pkt.Length(), pkt.Version(), pkt.Type(), pkt.Tag(), pkt.Body())) + debugLog(fmt.Sprintf("--> %s\n", pkt)) + return c.innerConn.Write(raw) +} + +func (c *UsbmuxClient) ReceivePacket() (respPkt Packet, err error) { + var bufLen []byte + if bufLen, err = c.innerConn.Read(4); err != nil { + return nil, fmt.Errorf("usbmux receive: %w", err) + } + lenPkg := binary.LittleEndian.Uint32(bufLen) + + buffer := bytes.NewBuffer([]byte{}) + buffer.Write(bufLen) + + var buf []byte + if buf, err = c.innerConn.Read(int(lenPkg - 4)); err != nil { + return nil, fmt.Errorf("usbmux receive: %w", err) + } + buffer.Write(buf) + + if respPkt, err = new(packet).Unpack(buffer); err != nil { + return nil, fmt.Errorf("usbmux receive: %w", err) + } + + // debugLog(fmt.Sprintf("<-- Length: %d, Version: %d, Type: %d, Tag: %d\n%s\n", respPkt.Length(), respPkt.Version(), respPkt.Type(), respPkt.Tag(), respPkt.Body())) + debugLog(fmt.Sprintf("<-- %s\n", respPkt)) + + var reply = struct { + MessageType string `plist:"MessageType"` + Number ReplyCode `plist:"Number"` + }{} + if err = respPkt.Unmarshal(&reply); err != nil { + return nil, fmt.Errorf("usbmux receive: %w", err) + } + + if reply.Number != ReplyCodeOK { + return nil, fmt.Errorf("usbmux receive: %s", reply.Number.String()) + } + + return + +} + +func (c *UsbmuxClient) Close() { + c.innerConn.Close() +} + +func (c *UsbmuxClient) RawConn() net.Conn { + return c.innerConn.RawConn() +} + +func (c *UsbmuxClient) InnerConn() InnerConn { + return c.innerConn +} + +func rawDial() (net.Conn, error) { + dialer := net.Dialer{ + Timeout: DefaultDeadlineTimeout, + } + + var network, address string + switch runtime.GOOS { + case "darwin", "android", "linux": + network, address = "unix", "/var/run/usbmuxd" + case "windows": + network, address = "tcp", "127.0.0.1:27015" + default: + return nil, fmt.Errorf("raw dial: unsupported system: %s", runtime.GOOS) + } + + return dialer.Dial(network, address) +} + +type InnerConnOption func(innerConn InnerConn) + +func TimeoutOption(duration time.Duration) InnerConnOption { + return func(innerConn InnerConn) { + innerConn.Timeout(duration) + } +} + +type InnerConn interface { + Write(data []byte) (err error) + Read(length int) (data []byte, err error) + Handshake(version []int, pairRecord *PairRecord) (err error) + DismissSSL() (err error) + Close() + RawConn() net.Conn + Timeout(time.Duration) +} + +func newInnerConn(conn net.Conn) InnerConn { + return &safeConn{ + conn: conn, + timeout: DefaultDeadlineTimeout, + } +} + +type safeConn struct { + conn net.Conn + sslConn *tls.Conn + timeout time.Duration +} + +func (c *safeConn) Write(data []byte) (err error) { + conn := c.RawConn() + if c.timeout <= 0 { + err = conn.SetWriteDeadline(time.Time{}) + } else { + err = conn.SetWriteDeadline(time.Now().Add(c.timeout)) + } + if err != nil { + return err + } + + for totalSent := 0; totalSent < len(data); { + var sent int + if sent, err = conn.Write(data[totalSent:]); err != nil { + return err + } + if sent == 0 { + return err + } + totalSent += sent + } + return +} + +func (c *safeConn) Read(length int) (data []byte, err error) { + conn := c.RawConn() + if c.timeout <= 0 { + err = conn.SetReadDeadline(time.Time{}) + } else { + err = conn.SetReadDeadline(time.Now().Add(c.timeout)) + } + if err != nil { + return nil, err + } + + data = make([]byte, 0, length) + for len(data) < length { + buf := make([]byte, length-len(data)) + _n, _err := 0, error(nil) + if _n, _err = conn.Read(buf); _err != nil && _n == 0 { + return nil, _err + } + data = append(data, buf[:_n]...) + } + return +} + +func (c *safeConn) Handshake(version []int, pairRecord *PairRecord) (err error) { + minVersion := uint16(tls.VersionTLS11) + maxVersion := uint16(tls.VersionTLS11) + + if version[0] > 10 { + minVersion = tls.VersionTLS11 + maxVersion = tls.VersionTLS13 + } + + cert, err := tls.X509KeyPair(pairRecord.RootCertificate, pairRecord.RootPrivateKey) + if err != nil { + return err + } + + config := &tls.Config{ + Certificates: []tls.Certificate{cert}, + InsecureSkipVerify: true, + MinVersion: minVersion, + MaxVersion: maxVersion, + } + + c.sslConn = tls.Client(c.conn, config) + + if err = c.sslConn.Handshake(); err != nil { + return err + } + + return +} + +func (c *safeConn) DismissSSL() (err error) { + if c.sslConn != nil { + // err = c.sslConn.CloseWrite() + // if err = c.sslConn.CloseWrite(); err != nil { + // return err + // } + c.sslConn = nil + } + return +} + +func (c *safeConn) Close() { + if c.sslConn != nil { + if err := c.sslConn.Close(); err != nil { + debugLog(fmt.Sprintf("close: %s", err)) + } + } + if c.conn != nil { + if err := c.conn.Close(); err != nil { + debugLog(fmt.Sprintf("close: %s", err)) + } + } +} + +// RawConn `sslConn` first +func (c *safeConn) RawConn() net.Conn { + if c.sslConn != nil { + return c.sslConn + } + return c.conn +} + +func (c *safeConn) Timeout(duration time.Duration) { + c.timeout = duration +} diff --git a/pkg/nskeyedarchiver/nsarray.go b/pkg/nskeyedarchiver/nsarray.go new file mode 100644 index 0000000..385dada --- /dev/null +++ b/pkg/nskeyedarchiver/nsarray.go @@ -0,0 +1,37 @@ +package nskeyedarchiver + +import "howett.net/plist" + +type NSArray struct { + internal []interface{} +} + +func NewNSArray(value []interface{}) *NSArray { + return &NSArray{ + internal: value, + } +} + +func (ns *NSArray) archive(objects []interface{}) []interface{} { + objs := make([]interface{}, 0, len(ns.internal)) + + info := map[string]interface{}{} + objects = append(objects, info) + + for _, v := range ns.internal { + var uid plist.UID + objects, uid = archive(objects, v) + objs = append(objs, uid) + } + + info["NS.objects"] = objs + info["$class"] = plist.UID(len(objects)) + + cls := map[string]interface{}{ + "$classname": "NSArray", + "$classes": []interface{}{"NSArray", "NSObject"}, + } + objects = append(objects, cls) + + return objects +} diff --git a/pkg/nskeyedarchiver/nsarray_test.go b/pkg/nskeyedarchiver/nsarray_test.go new file mode 100644 index 0000000..5274bfc --- /dev/null +++ b/pkg/nskeyedarchiver/nsarray_test.go @@ -0,0 +1,18 @@ +package nskeyedarchiver + +import ( + "fmt" + "testing" +) + +func TestNSArray_archive(t *testing.T) { + objs := make([]interface{}, 0, 1) + value := []interface{}{ + "a", 1, + "b", "2", + "c", false, + } + array := NewNSArray(value) + objects := array.archive(objs) + fmt.Println(objects) +} diff --git a/pkg/nskeyedarchiver/nsdictionary.go b/pkg/nskeyedarchiver/nsdictionary.go new file mode 100644 index 0000000..80d442e --- /dev/null +++ b/pkg/nskeyedarchiver/nsdictionary.go @@ -0,0 +1,44 @@ +package nskeyedarchiver + +import ( + "howett.net/plist" +) + +type NSDictionary struct { + internal map[string]interface{} +} + +func NewNSDictionary(value map[string]interface{}) *NSDictionary { + return &NSDictionary{ + internal: value, + } +} + +func (ns *NSDictionary) archive(objects []interface{}) []interface{} { + keys := make([]interface{}, 0, len(ns.internal)) + objs := make([]interface{}, 0, len(ns.internal)) + + info := map[string]interface{}{} + objects = append(objects, info) + + for k, v := range ns.internal { + uid := plist.UID(len(objects)) + keys = append(keys, uid) + objects = append(objects, k) + + objects, uid = archive(objects, v) + objs = append(objs, uid) + } + + info["NS.keys"] = keys + info["NS.objects"] = objs + info["$class"] = plist.UID(len(objects)) + + cls := map[string]interface{}{ + "$classname": "NSDictionary", + "$classes": []interface{}{"NSDictionary", "NSObject"}, + } + objects = append(objects, cls) + + return objects +} diff --git a/pkg/nskeyedarchiver/nsdictionary_test.go b/pkg/nskeyedarchiver/nsdictionary_test.go new file mode 100644 index 0000000..e3becc6 --- /dev/null +++ b/pkg/nskeyedarchiver/nsdictionary_test.go @@ -0,0 +1,18 @@ +package nskeyedarchiver + +import ( + "fmt" + "testing" +) + +func TestNSDictionary_archive(t *testing.T) { + objs := make([]interface{}, 0, 1) + value := map[string]interface{}{ + "a": 1, + "b": "2", + "c": true, + } + dict := NewNSDictionary(value) + objects := dict.archive(objs) + fmt.Println(objects) +} diff --git a/pkg/nskeyedarchiver/nskeyedarchiver.go b/pkg/nskeyedarchiver/nskeyedarchiver.go new file mode 100644 index 0000000..5368c9c --- /dev/null +++ b/pkg/nskeyedarchiver/nskeyedarchiver.go @@ -0,0 +1,79 @@ +package nskeyedarchiver + +import ( + "howett.net/plist" + "reflect" +) + +func Marshal(obj interface{}) (raw []byte, err error) { + objects := []interface{}{"$null"} + objects, _ = archive(objects, obj) + archiver := map[string]interface{}{ + "$version": 100000, + "$archiver": "NSKeyedArchiver", + "$top": map[string]interface{}{"root": plist.UID(1)}, + "$objects": objects, + } + // if len(format) == 0 { + // format = []int{plist.BinaryFormat} + // } + // return plist.Marshal(archiver, format[0]) + return plist.Marshal(archiver, plist.BinaryFormat) +} + +func archive(_objects []interface{}, _value interface{}) (objects []interface{}, uid plist.UID) { + val := reflect.ValueOf(_value) + typ := val.Type() + + switch typ.Kind() { + case reflect.String, + reflect.Bool, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Uintptr: + uid = plist.UID(len(_objects)) + objects = append(_objects, _value) + return + case reflect.Map: + uid = plist.UID(len(_objects)) + vv := make(map[string]interface{}) + keys := val.MapKeys() + for _, k := range keys { + vv[k.String()] = val.MapIndex(k).Interface() + } + objects = NewNSDictionary(vv).archive(_objects) + return + case reflect.Slice, reflect.Array: + uid = plist.UID(len(_objects)) + vv := make([]interface{}, val.Len()) + for i := 0; i < val.Len(); i++ { + vv[i] = val.Index(i).Interface() + } + objects = NewNSArray(vv).archive(_objects) + return + case reflect.Struct, reflect.Ptr: + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + val = val.Elem() + } + + switch typ.Name() { + case "NSUUID": + uid = plist.UID(len(_objects)) + objects = NewNSUUID(val.Field(0).Bytes()).archive(_objects) + return + case "NSURL": + uid = plist.UID(len(_objects)) + objects = NewNSURL(val.Field(0).String()).archive(_objects) + return + case "XCTestConfiguration": + uid = plist.UID(len(_objects)) + objects = newXCTestConfiguration(_value).archive(_objects) + return + } + } + + return +} + +// TODO unarchive diff --git a/pkg/nskeyedarchiver/nskeyedarchiver_test.go b/pkg/nskeyedarchiver/nskeyedarchiver_test.go new file mode 100644 index 0000000..fb5f8e7 --- /dev/null +++ b/pkg/nskeyedarchiver/nskeyedarchiver_test.go @@ -0,0 +1,38 @@ +package nskeyedarchiver + +import ( + "fmt" + uuid "github.com/satori/go.uuid" + "testing" +) + +func TestMarshal(t *testing.T) { + // value := map[string]interface{}{ + // "a": 1, + // "b": "2", + // "c": true, + // } + + // value := []interface{}{ + // "a", 1, + // "b", "2", + // "c", false, + // } + + // value := NewNSUUID(uuid.NewV4().Bytes()) + + // value := NewNSURL("/tmp") + + value := NewXCTestConfiguration(NewNSUUID(uuid.NewV4().Bytes()), NewNSURL("/tmp"), "", "") + + raw, err := Marshal(value) + if err != nil { + t.Fatal(err) + } + + for _, v := range raw { + fmt.Printf("%x", v) + } + fmt.Println() + // fmt.Println(raw) +} diff --git a/pkg/nskeyedarchiver/nsnull.go b/pkg/nskeyedarchiver/nsnull.go new file mode 100644 index 0000000..5af05b2 --- /dev/null +++ b/pkg/nskeyedarchiver/nsnull.go @@ -0,0 +1,28 @@ +package nskeyedarchiver + +import ( + "howett.net/plist" +) + +type NSNull struct { +} + +func NewNSNull() *NSNull { + return &NSNull{} +} + +func (ns *NSNull) archive(objects []interface{}) []interface{} { + info := map[string]interface{}{} + + objects = append(objects, info) + + info["$class"] = plist.UID(len(objects)) + + cls := map[string]interface{}{ + "$classname": "NSNull", + "$classes": []interface{}{"NSNull", "NSObject"}, + } + objects = append(objects, cls) + + return objects +} diff --git a/pkg/nskeyedarchiver/nsurl.go b/pkg/nskeyedarchiver/nsurl.go new file mode 100644 index 0000000..9209de0 --- /dev/null +++ b/pkg/nskeyedarchiver/nsurl.go @@ -0,0 +1,37 @@ +package nskeyedarchiver + +import ( + "fmt" + "howett.net/plist" +) + +type NSURL struct { + internal string +} + +func NewNSURL(path string) *NSURL { + return &NSURL{ + internal: path, + } +} + +func (ns *NSURL) archive(objects []interface{}) []interface{} { + info := map[string]interface{}{} + + objects = append(objects, info) + + uid := plist.UID(0) + info["NS.base"] = uid + objects, uid = archive(objects, fmt.Sprintf("file://%s", ns.internal)) + info["NS.relative"] = uid + + info["$class"] = plist.UID(len(objects)) + + cls := map[string]interface{}{ + "$classname": "NSURL", + "$classes": []interface{}{"NSURL", "NSObject"}, + } + objects = append(objects, cls) + + return objects +} diff --git a/pkg/nskeyedarchiver/nsurl_test.go b/pkg/nskeyedarchiver/nsurl_test.go new file mode 100644 index 0000000..413a202 --- /dev/null +++ b/pkg/nskeyedarchiver/nsurl_test.go @@ -0,0 +1,13 @@ +package nskeyedarchiver + +import ( + "fmt" + "testing" +) + +func TestNSURL_archive(t *testing.T) { + objs := make([]interface{}, 0, 1) + nsurl := NewNSURL("/tmp") + objects := nsurl.archive(objs) + fmt.Println(objects) +} diff --git a/pkg/nskeyedarchiver/nsuuid.go b/pkg/nskeyedarchiver/nsuuid.go new file mode 100644 index 0000000..d7ff245 --- /dev/null +++ b/pkg/nskeyedarchiver/nsuuid.go @@ -0,0 +1,50 @@ +package nskeyedarchiver + +import ( + "encoding/hex" + "howett.net/plist" +) + +type NSUUID struct { + internal []byte +} + +func NewNSUUID(uuid []byte) *NSUUID { + return &NSUUID{ + internal: uuid, + } +} + +func (ns *NSUUID) archive(objects []interface{}) []interface{} { + info := map[string]interface{}{ + "NS.uuidbytes": ns.internal, + } + + objects = append(objects, info) + + info["$class"] = plist.UID(len(objects)) + + cls := map[string]interface{}{ + "$classname": "NSUUID", + "$classes": []interface{}{"NSUUID", "NSObject"}, + } + objects = append(objects, cls) + + return objects +} + +func (ns *NSUUID) String() string { + buf := make([]byte, 36) + + hex.Encode(buf[0:8], ns.internal[0:4]) + buf[8] = '-' + hex.Encode(buf[9:13], ns.internal[4:6]) + buf[13] = '-' + hex.Encode(buf[14:18], ns.internal[6:8]) + buf[18] = '-' + hex.Encode(buf[19:23], ns.internal[8:10]) + buf[23] = '-' + hex.Encode(buf[24:], ns.internal[10:]) + + return string(buf) +} diff --git a/pkg/nskeyedarchiver/nsuuid_test.go b/pkg/nskeyedarchiver/nsuuid_test.go new file mode 100644 index 0000000..b3376f5 --- /dev/null +++ b/pkg/nskeyedarchiver/nsuuid_test.go @@ -0,0 +1,14 @@ +package nskeyedarchiver + +import ( + "fmt" + uuid "github.com/satori/go.uuid" + "testing" +) + +func TestNSUUID_archive(t *testing.T) { + objs := make([]interface{}, 0, 1) + nsuuid := NewNSUUID(uuid.NewV4().Bytes()) + objects := nsuuid.archive(objs) + fmt.Println(objects) +} diff --git a/pkg/nskeyedarchiver/xctcapabilities.go b/pkg/nskeyedarchiver/xctcapabilities.go new file mode 100644 index 0000000..a301c25 --- /dev/null +++ b/pkg/nskeyedarchiver/xctcapabilities.go @@ -0,0 +1,10 @@ +package nskeyedarchiver + +type XCTCapabilities struct { + internal map[string]interface{} +} + +func (caps *XCTCapabilities) archive(objects []interface{}) []interface{} { + // TODO caps + return nil +} diff --git a/pkg/nskeyedarchiver/xctestconfiguration.go b/pkg/nskeyedarchiver/xctestconfiguration.go new file mode 100644 index 0000000..c6ca7dd --- /dev/null +++ b/pkg/nskeyedarchiver/xctestconfiguration.go @@ -0,0 +1,88 @@ +package nskeyedarchiver + +import ( + "howett.net/plist" + "reflect" +) + +type XCTestConfiguration struct { + internal map[string]interface{} +} + +func newXCTestConfiguration(cfg interface{}) *XCTestConfiguration { + return cfg.(*XCTestConfiguration) +} + +func NewXCTestConfiguration(nsuuid *NSUUID, nsurl *NSURL, targetBundleID, targetAppPath string) *XCTestConfiguration { + contents := map[string]interface{}{ + "aggregateStatisticsBeforeCrash": map[string]interface{}{ + "XCSuiteRecordsKey": map[string]interface{}{}, + }, + "automationFrameworkPath": "/Developer/Library/PrivateFrameworks/XCTAutomationSupport.framework", + "baselineFileRelativePath": nil, + "baselineFileURL": nil, + "defaultTestExecutionTimeAllowance": nil, + "disablePerformanceMetrics": false, + "emitOSLogs": false, + "formatVersion": 2, + "gatherLocalizableStringsData": false, + "initializeForUITesting": true, + "maximumTestExecutionTimeAllowance": nil, + "productModuleName": "WebDriverAgentRunner", // set to other value is also OK + "randomExecutionOrderingSeed": nil, + "reportActivities": true, + "reportResultsToIDE": true, + "systemAttachmentLifetime": 2, + "targetApplicationArguments": []interface{}{}, // maybe useless + "targetApplicationEnvironment": nil, + "targetApplicationPath": targetAppPath, + "testApplicationDependencies": map[string]interface{}{}, + "testApplicationUserOverrides": nil, + "testBundleRelativePath": nil, + "testExecutionOrdering": 0, + "testTimeoutsEnabled": false, + "testsDrivenByIDE": false, + "testsMustRunOnMainThread": true, + "testsToRun": nil, + "testsToSkip": nil, + "treatMissingBaselinesAsFailures": false, + "userAttachmentLifetime": 1, + "testBundleURL": nsurl, + "sessionIdentifier": nsuuid, + "targetApplicationBundleID": targetBundleID, + // "targetApplicationBundleID": "", + } + return &XCTestConfiguration{internal: contents} +} + +func (cfg *XCTestConfiguration) archive(objects []interface{}) []interface{} { + info := map[string]interface{}{} + objects = append(objects, info) + + info["$class"] = plist.UID(len(objects)) + + cls := map[string]interface{}{ + "$classname": "XCTestConfiguration", + "$classes": []interface{}{"XCTestConfiguration", "NSObject"}, + } + objects = append(objects, cls) + + for k, v := range cfg.internal { + val := reflect.ValueOf(v) + if !val.IsValid() { + info[k] = plist.UID(0) + continue + } + + typ := val.Type() + + if k != "formatVersion" && (typ.Kind() == reflect.Bool || typ.Kind() == reflect.Uintptr || typ.Kind() == reflect.Int) { + info[k] = v + } else { + var uid plist.UID + objects, uid = archive(objects, v) + info[k] = uid + } + } + return objects +} diff --git a/pkg/nskeyedarchiver/xctestconfiguration_test.go b/pkg/nskeyedarchiver/xctestconfiguration_test.go new file mode 100644 index 0000000..20e7d71 --- /dev/null +++ b/pkg/nskeyedarchiver/xctestconfiguration_test.go @@ -0,0 +1,14 @@ +package nskeyedarchiver + +import ( + "fmt" + uuid "github.com/satori/go.uuid" + "testing" +) + +func TestXCTestConfiguration_archive(t *testing.T) { + objs := make([]interface{}, 0, 1) + xcTestConfiguration := NewXCTestConfiguration(NewNSUUID(uuid.NewV4().Bytes()), NewNSURL("/tmp"), "", "") + objects := xcTestConfiguration.archive(objs) + fmt.Println(objects) +} diff --git a/screenshot.go b/screenshot.go new file mode 100644 index 0000000..721eaf8 --- /dev/null +++ b/screenshot.go @@ -0,0 +1,118 @@ +package giDevice + +import ( + "bytes" + "errors" + "fmt" + "github.com/electricbubble/gidevice/pkg/libimobiledevice" +) + +var _ Screenshot = (*screenshot)(nil) + +func newScreenshot(client *libimobiledevice.ScreenshotClient) *screenshot { + return &screenshot{ + client: client, + exchanged: false, + } +} + +type screenshot struct { + client *libimobiledevice.ScreenshotClient + exchanged bool +} + +func (s *screenshot) Take() (raw *bytes.Buffer, err error) { + if err = s.exchange(); err != nil { + return nil, err + } + + // link service + req := []interface{}{ + "DLMessageProcessMessage", + map[string]interface{}{ + "MessageType": "ScreenShotRequest", + }, + } + + var pkt libimobiledevice.Packet + if pkt, err = s.client.NewBinaryPacket(req); err != nil { + return nil, err + } + + if err = s.client.SendPacket(pkt); err != nil { + return nil, err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = s.client.ReceivePacket(); err != nil { + return nil, err + } + + var resp []interface{} + if err = respPkt.Unmarshal(&resp); err != nil { + return nil, err + } + + if resp[0].(string) != "DLMessageProcessMessage" { + return nil, fmt.Errorf("message device not ready %s %s", resp[3], resp[4]) + } + + raw = new(bytes.Buffer) + + screen := resp[1].(map[string]interface{}) + var data []byte + ok := false + if data, ok = screen["ScreenShotData"].([]byte); !ok { + return nil, errors.New("`ScreenShotData` not ready") + } + if _, err = raw.Write(data); err != nil { + return nil, err + } + return +} + +func (s *screenshot) exchange() (err error) { + if s.exchanged { + return + } + + var respPkt libimobiledevice.Packet + if respPkt, err = s.client.ReceivePacket(); err != nil { + return err + } + + var resp []interface{} + if err = respPkt.Unmarshal(&resp); err != nil { + return err + } + + req := []interface{}{ + "DLMessageVersionExchange", + "DLVersionsOk", + resp[1], + } + + var pkt libimobiledevice.Packet + if pkt, err = s.client.NewBinaryPacket(req); err != nil { + return err + } + + if err = s.client.SendPacket(pkt); err != nil { + return err + } + + if respPkt, err = s.client.ReceivePacket(); err != nil { + return err + } + + if err = respPkt.Unmarshal(&resp); err != nil { + return err + } + + if resp[3].(string) != "DLMessageDeviceReady" { + return fmt.Errorf("message device not ready %s", resp[3]) + } + + s.exchanged = true + return +} diff --git a/screenshot_test.go b/screenshot_test.go new file mode 100644 index 0000000..28d2be6 --- /dev/null +++ b/screenshot_test.go @@ -0,0 +1,56 @@ +package giDevice + +import ( + "image" + "image/jpeg" + "image/png" + "os" + "testing" +) + +var screenshotSrv Screenshot + +func setupScreenshotSrv(t *testing.T) { + setupLockdownSrv(t) + + var err error + if lockdownSrv, err = dev.lockdownService(); err != nil { + t.Fatal(err) + } + + if screenshotSrv, err = lockdownSrv.ScreenshotService(); err != nil { + t.Fatal(err) + } +} + +func Test_screenshot_Take(t *testing.T) { + setupScreenshotSrv(t) + + // raw, err := dev.Screenshot() + raw, err := screenshotSrv.Take() + if err != nil { + t.Fatal(err) + } + _ = raw + + img, format, err := image.Decode(raw) + if err != nil { + t.Fatal(err) + } + userHomeDir, _ := os.UserHomeDir() + file, err := os.Create(userHomeDir + "/Desktop/s1." + format) + if err != nil { + t.Fatal(err) + } + defer func() { _ = file.Close() }() + switch format { + case "png": + err = png.Encode(file, img) + case "jpeg": + err = jpeg.Encode(file, img, nil) + } + if err != nil { + t.Fatal(err) + } + t.Log(file.Name()) +} diff --git a/simulatelocation.go b/simulatelocation.go new file mode 100644 index 0000000..da5564f --- /dev/null +++ b/simulatelocation.go @@ -0,0 +1,27 @@ +package giDevice + +import "github.com/electricbubble/gidevice/pkg/libimobiledevice" + +var _ SimulateLocation = (*simulateLocation)(nil) + +func newSimulateLocation(client *libimobiledevice.SimulateLocationClient) *simulateLocation { + return &simulateLocation{ + client: client, + } +} + +type simulateLocation struct { + client *libimobiledevice.SimulateLocationClient +} + +func (s *simulateLocation) Update(longitude float64, latitude float64, coordinateSystem ...CoordinateSystem) (err error) { + if len(coordinateSystem) == 0 { + coordinateSystem = []CoordinateSystem{CoordinateSystemWGS84} + } + pkt := s.client.NewLocationPacket(longitude, latitude, coordinateSystem[0]) + return s.client.SendPacket(pkt) +} + +func (s *simulateLocation) Recover() (err error) { + return s.client.Recover() +} diff --git a/simulatelocation_test.go b/simulatelocation_test.go new file mode 100644 index 0000000..0bea4ad --- /dev/null +++ b/simulatelocation_test.go @@ -0,0 +1,48 @@ +package giDevice + +import "testing" + +var simulateLocationSrv SimulateLocation + +func setupSimulateLocationSrv(t *testing.T) { + setupLockdownSrv(t) + + var err error + if lockdownSrv, err = dev.lockdownService(); err != nil { + t.Fatal(err) + } + + if simulateLocationSrv, err = lockdownSrv.SimulateLocationService(); err != nil { + t.Fatal(err) + } +} + +func Test_simulateLocation_Update(t *testing.T) { + setupSimulateLocationSrv(t) + + // https://api.map.baidu.com/lbsapi/getpoint/index.html + // if err := dev.SimulateLocationUpdate(116.024067, 40.362639, CoordinateSystemBD09); err != nil { + if err := simulateLocationSrv.Update(116.024067, 40.362639, CoordinateSystemBD09); err != nil { + t.Fatal(err) + } + + // https://developer.amap.com/tools/picker + // https://lbs.qq.com/tool/getpoint/index.html + // if err := simulateLocationSrv.Update(120.116979,30.252876, CoordinateSystemGCJ02); err != nil { + // t.Fatal(err) + // } + + if err := simulateLocationSrv.Update(121.499763, 31.239580); err != nil { + // if err := simulateLocationSrv.Update(121.499763, 31.239580, CoordinateSystemWGS84); err != nil { + t.Fatal(err) + } +} + +func Test_simulateLocation_Recover(t *testing.T) { + setupSimulateLocationSrv(t) + + // if err := dev.SimulateLocationRecover(); err != nil { + if err := simulateLocationSrv.Recover(); err != nil { + t.Fatal(err) + } +} diff --git a/testmanagerd.go b/testmanagerd.go new file mode 100644 index 0000000..d4a34c9 --- /dev/null +++ b/testmanagerd.go @@ -0,0 +1,49 @@ +package giDevice + +import ( + "github.com/electricbubble/gidevice/pkg/libimobiledevice" +) + +var _ Testmanagerd = (*testmanagerd)(nil) + +func newTestmanagerd(client *libimobiledevice.TestmanagerdClient, iOSVersion []int) *testmanagerd { + return &testmanagerd{ + client: client, + iOSVersion: iOSVersion, + } +} + +type testmanagerd struct { + client *libimobiledevice.TestmanagerdClient + iOSVersion []int +} + +func (t *testmanagerd) notifyOfPublishedCapabilities() (err error) { + _, err = t.client.Connection() + return +} + +func (t *testmanagerd) requestChannel(channel string) (id uint32, err error) { + return t.client.MakeChannel(channel) +} + +func (t *testmanagerd) newXCTestManagerDaemon() (xcTestManager XCTestManagerDaemon, err error) { + var channelCode uint32 + if channelCode, err = t.requestChannel("dtxproxy:XCTestManager_IDEInterface:XCTestManager_DaemonConnectionInterface"); err != nil { + return nil, err + } + xcTestManager = newXcTestManagerDaemon(t, channelCode) + return +} + +func (t *testmanagerd) invoke(selector string, args *libimobiledevice.AuxBuffer, channelCode uint32, expectsReply bool) (result *libimobiledevice.DTXMessageResult, err error) { + return t.client.Invoke(selector, args, channelCode, expectsReply) +} + +func (t *testmanagerd) registerCallback(obj string, cb func(m libimobiledevice.DTXMessageResult)) { + t.client.RegisterCallback(obj, cb) +} + +func (t *testmanagerd) close() { + t.client.Close() +} diff --git a/usbmux.go b/usbmux.go new file mode 100644 index 0000000..7f21ab1 --- /dev/null +++ b/usbmux.go @@ -0,0 +1,148 @@ +package giDevice + +import ( + "context" + "github.com/electricbubble/gidevice/pkg/libimobiledevice" +) + +var _ Usbmux = (*usbmux)(nil) + +func NewUsbmux() (Usbmux, error) { + umClient, err := libimobiledevice.NewUsbmuxClient() + if err != nil { + return nil, err + } + return &usbmux{client: umClient}, nil +} + +func newUsbmux(client *libimobiledevice.UsbmuxClient) *usbmux { + return &usbmux{client: client} +} + +type usbmux struct { + client *libimobiledevice.UsbmuxClient +} + +func (um *usbmux) Devices() (devices []Device, err error) { + var pkt libimobiledevice.Packet + if pkt, err = um.client.NewPlistPacket( + um.client.NewBasicRequest(libimobiledevice.MessageTypeDeviceList), + ); err != nil { + return nil, err + } + + if err = um.client.SendPacket(pkt); err != nil { + return nil, err + } + + var respPkt libimobiledevice.Packet + if respPkt, err = um.client.ReceivePacket(); err != nil { + return nil, err + } + + var reply = struct { + DeviceList []libimobiledevice.BaseDevice `plist:"DeviceList"` + }{} + if err = respPkt.Unmarshal(&reply); err != nil { + return nil, err + } + + devices = make([]Device, len(reply.DeviceList)) + for i := range reply.DeviceList { + dev := reply.DeviceList[i] + devices[i] = newDevice(um.client, dev.Properties) + } + + return +} + +func (um *usbmux) ReadBUID() (buid string, err error) { + var pktReadBUID libimobiledevice.Packet + if pktReadBUID, err = um.client.NewPlistPacket( + um.client.NewBasicRequest(libimobiledevice.MessageTypeReadBUID), + ); err != nil { + return "", err + } + + if err = um.client.SendPacket(pktReadBUID); err != nil { + return "", err + } + + respPkt, err := um.client.ReceivePacket() + if err != nil { + return "", err + } + + var reply = struct { + BUID string `plist:"BUID"` + }{} + if err = respPkt.Unmarshal(&reply); err != nil { + return "", err + } + + buid = reply.BUID + + return +} + +func (um *usbmux) Listen(devNotifier chan Device) (context.CancelFunc, error) { + baseDevNotifier := make(chan libimobiledevice.BaseDevice) + ctx, cancelFunc, err := um.listen(baseDevNotifier) + go func(ctx context.Context) { + defer close(devNotifier) + for { + select { + case <-ctx.Done(): + return + case baseDev := <-baseDevNotifier: + if baseDev.MessageType != libimobiledevice.MessageTypeDeviceAdd { + baseDev.Properties.DeviceID = baseDev.DeviceID + } + devNotifier <- newDevice(um.client, baseDev.Properties) + } + } + }(ctx) + return cancelFunc, err +} + +func (um *usbmux) listen(devNotifier chan libimobiledevice.BaseDevice) (ctx context.Context, cancelFunc context.CancelFunc, err error) { + var pkt libimobiledevice.Packet + if pkt, err = um.client.NewPlistPacket( + um.client.NewBasicRequest(libimobiledevice.MessageTypeListen), + ); err != nil { + return nil, nil, err + } + + if err = um.client.SendPacket(pkt); err != nil { + return nil, nil, err + } + + ctx, cancelFunc = context.WithCancel(context.Background()) + + go func(ctx context.Context) { + defer close(devNotifier) + for { + select { + case <-ctx.Done(): + return + default: + var respPkt libimobiledevice.Packet + if respPkt, err = um.client.ReceivePacket(); err != nil { + break + } + + var replyDevice libimobiledevice.BaseDevice + if err = respPkt.Unmarshal(&replyDevice); err != nil { + break + } + if replyDevice.MessageType == libimobiledevice.MessageTypeResult { + break + } + + devNotifier <- replyDevice + } + } + }(ctx) + + return ctx, cancelFunc, nil +} diff --git a/usbmux_test.go b/usbmux_test.go new file mode 100644 index 0000000..7f183f9 --- /dev/null +++ b/usbmux_test.go @@ -0,0 +1,67 @@ +package giDevice + +import ( + "github.com/electricbubble/gidevice/pkg/libimobiledevice" + "testing" + "time" +) + +var um Usbmux + +func setupUsbmux(t *testing.T) { + var err error + um, err = NewUsbmux() + if err != nil { + t.Fatal(err) + } +} + +func Test_usbmux_Devices(t *testing.T) { + setupUsbmux(t) + + devices, err := um.Devices() + if err != nil { + t.Fatal(err) + } + + for _, dev := range devices { + t.Log(dev.Properties().SerialNumber, dev.Properties().ProductID, dev.Properties().DeviceID) + } +} + +func Test_usbmux_ReadBUID(t *testing.T) { + setupUsbmux(t) + + buid, err := um.ReadBUID() + if err != nil { + t.Fatal(err) + } + + t.Log(buid) +} + +func Test_usbmux_Listen(t *testing.T) { + setupUsbmux(t) + + devNotifier := make(chan Device) + cancelFunc, err := um.Listen(devNotifier) + if err != nil { + t.Fatal(err) + } + + go func() { + time.Sleep(20 * time.Second) + cancelFunc() + }() + + for dev := range devNotifier { + if dev.Properties().ConnectionType != "" { + t.Log(dev.Properties().SerialNumber, dev.Properties().ProductID, dev.Properties().DeviceID) + } else { + t.Log(libimobiledevice.MessageTypeDeviceRemove, dev.Properties().DeviceID) + } + } + + time.Sleep(5 * time.Second) + t.Log("Done") +} diff --git a/xctestmanagerdaemon.go b/xctestmanagerdaemon.go new file mode 100644 index 0000000..06dc92b --- /dev/null +++ b/xctestmanagerdaemon.go @@ -0,0 +1,152 @@ +package giDevice + +import ( + "fmt" + "github.com/electricbubble/gidevice/pkg/libimobiledevice" + "github.com/electricbubble/gidevice/pkg/nskeyedarchiver" +) + +var _ XCTestManagerDaemon = (*xcTestManagerDaemon)(nil) + +func newXcTestManagerDaemon(testmanagerd Testmanagerd, channelCode uint32) *xcTestManagerDaemon { + return &xcTestManagerDaemon{ + testmanagerd: testmanagerd, + channelCode: channelCode, + } +} + +type xcTestManagerDaemon struct { + testmanagerd Testmanagerd + channelCode uint32 +} + +func (d *xcTestManagerDaemon) initiateControlSession(XcodeVersion uint64) (err error) { + args := libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(XcodeVersion); err != nil { + return err + } + + selector := "_IDE_initiateControlSessionWithProtocolVersion:" + + var ret *libimobiledevice.DTXMessageResult + if ret, err = d.testmanagerd.invoke(selector, args, d.channelCode, true); err != nil { + return err + } + + if nsErr, ok := ret.Obj.(libimobiledevice.NSError); ok { + return fmt.Errorf("%s", nsErr.NSUserInfo.(map[string]interface{})["NSLocalizedDescription"]) + } + return +} + +func (d *xcTestManagerDaemon) startExecutingTestPlan(XcodeVersion uint64) (err error) { + args := libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(XcodeVersion); err != nil { + return err + } + + selector := "_IDE_startExecutingTestPlanWithProtocolVersion:" + + if _, err = d.testmanagerd.invoke(selector, args, 0xFFFFFFFF, false); err != nil { + return err + } + + return +} + +func (d *xcTestManagerDaemon) initiateSession(XcodeVersion uint64, nsUUID *nskeyedarchiver.NSUUID) (err error) { + args := libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(nsUUID); err != nil { + return err + } + if err = args.AppendObject(nsUUID.String() + "-Go-iDevice"); err != nil { + return err + } + if err = args.AppendObject("/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild"); err != nil { + return err + } + if err = args.AppendObject(XcodeVersion); err != nil { + return err + } + + selector := "_IDE_initiateSessionWithIdentifier:forClient:atPath:protocolVersion:" + + var ret *libimobiledevice.DTXMessageResult + if ret, err = d.testmanagerd.invoke(selector, args, d.channelCode, true); err != nil { + return err + } + + if nsErr, ok := ret.Obj.(libimobiledevice.NSError); ok { + return fmt.Errorf("%s", nsErr.NSUserInfo.(map[string]interface{})["NSLocalizedDescription"]) + } + + return +} + +func (d *xcTestManagerDaemon) authorizeTestSession(pid int) (err error) { + args := libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(pid); err != nil { + return err + } + + selector := "_IDE_authorizeTestSessionWithProcessID:" + + var ret *libimobiledevice.DTXMessageResult + if ret, err = d.testmanagerd.invoke(selector, args, d.channelCode, true); err != nil { + return err + } + + if nsErr, ok := ret.Obj.(libimobiledevice.NSError); ok { + return fmt.Errorf("%s", nsErr.NSUserInfo.(map[string]interface{})["NSLocalizedDescription"]) + } + return +} + +func (d *xcTestManagerDaemon) initiateControlSessionForTestProcessID(pid int) (err error) { + args := libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(pid); err != nil { + return err + } + + selector := "_IDE_initiateControlSessionForTestProcessID:" + + var ret *libimobiledevice.DTXMessageResult + if ret, err = d.testmanagerd.invoke(selector, args, d.channelCode, true); err != nil { + return err + } + + if nsErr, ok := ret.Obj.(libimobiledevice.NSError); ok { + return fmt.Errorf("%s", nsErr.NSUserInfo.(map[string]interface{})["NSLocalizedDescription"]) + } + return +} + +func (d *xcTestManagerDaemon) initiateControlSessionForTestProcessIDProtocolVersion(pid int, XcodeVersion uint64) (err error) { + args := libimobiledevice.NewAuxBuffer() + if err = args.AppendObject(pid); err != nil { + return err + } + if err = args.AppendObject(XcodeVersion); err != nil { + return err + } + + selector := "_IDE_initiateControlSessionForTestProcessID:protocolVersion:" + + var ret *libimobiledevice.DTXMessageResult + if ret, err = d.testmanagerd.invoke(selector, args, d.channelCode, true); err != nil { + return err + } + + if nsErr, ok := ret.Obj.(libimobiledevice.NSError); ok { + return fmt.Errorf("%s", nsErr.NSUserInfo.(map[string]interface{})["NSLocalizedDescription"]) + } + return +} + +func (d *xcTestManagerDaemon) registerCallback(obj string, cb func(m libimobiledevice.DTXMessageResult)) { + d.testmanagerd.registerCallback(obj, cb) +} + +func (d *xcTestManagerDaemon) close() { + d.testmanagerd.close() +}