├── .github └── workflows │ └── build.yml ├── .gitmodules ├── Build.xcconfig ├── CodeSigning.xcconfig.sample ├── Jitterbug.xcodeproj └── project.pbxproj ├── Jitterbug ├── AddressUtils.h ├── AddressUtils.m ├── AppItemView.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon.png │ │ ├── icon_128pt.png │ │ ├── icon_128pt@2x.png │ │ ├── icon_16pt.png │ │ ├── icon_16pt@2x.png │ │ ├── icon_20pt.png │ │ ├── icon_20pt@2x-1.png │ │ ├── icon_20pt@2x.png │ │ ├── icon_20pt@3x.png │ │ ├── icon_256pt.png │ │ ├── icon_256pt@2x.png │ │ ├── icon_29pt.png │ │ ├── icon_29pt@2x-1.png │ │ ├── icon_29pt@2x.png │ │ ├── icon_29pt@3x.png │ │ ├── icon_32pt.png │ │ ├── icon_32pt@2x.png │ │ ├── icon_40pt.png │ │ ├── icon_40pt@2x-1.png │ │ ├── icon_40pt@2x.png │ │ ├── icon_40pt@3x.png │ │ ├── icon_512pt.png │ │ ├── icon_512pt@2x.png │ │ ├── icon_60pt@2x.png │ │ ├── icon_60pt@3x.png │ │ ├── icon_76pt.png │ │ ├── icon_76pt@2x.png │ │ └── icon_83.5@2x.png │ └── Contents.json ├── BusyView.swift ├── CacheStorage.c ├── CacheStorage.h ├── ContentView.swift ├── DeviceDetailsView.swift ├── DeviceListView.swift ├── Extensions.swift ├── FileSelectionView.swift ├── HostFinder.swift ├── HostFinderDelegate.swift ├── Info.plist ├── JBApp.h ├── JBApp.m ├── JBHostDevice.h ├── JBHostDevice.m ├── JBHostDevice.swift ├── JBHostFinder.h ├── JBHostFinder.m ├── JBHostFinderDelegate.h ├── Jitterbug-Bridging-Header.h ├── Jitterbug.entitlements ├── Jitterbug.h ├── JitterbugApp.swift ├── LauncherView.swift ├── Main.swift ├── ManualAddHostView.swift ├── PairingsView.swift ├── Settings.bundle │ ├── Root.plist │ ├── en.lproj │ │ └── Root.strings │ └── zh-Hans.lproj │ │ └── Root.strings ├── SupportFilesView.swift ├── en.lproj │ └── Localizable.strings ├── libusbmuxd-stub.c └── zh-Hans.lproj │ ├── InfoPlist.strings │ └── Localizable.strings ├── JitterbugMac ├── ContentView.swift ├── DeviceDetailsView.swift ├── Info.plist ├── JitterbugMac.entitlements ├── JitterbugMacApp.swift ├── SharingServicePicker.swift ├── en.lproj │ └── Localizable.strings └── zh-Hans.lproj │ ├── InfoPlist.strings │ └── Localizable.strings ├── JitterbugPair └── main.c ├── JitterbugTunnel ├── Info.plist ├── JitterbugTunnel.entitlements ├── PacketTunnelProvider.swift └── zh-Hans.lproj │ └── InfoPlist.strings ├── LICENSE ├── Libraries └── include │ └── config.h ├── Makefile ├── README.md ├── README.zh-Hans.md └── meson.build /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - 'main' 6 | - 'dev' 7 | tags-ignore: 8 | - '**' 9 | paths-ignore: 10 | - 'LICENSE' 11 | - '**.md' 12 | pull_request: 13 | release: 14 | types: [created] 15 | workflow_dispatch: 16 | inputs: 17 | test_release: 18 | description: 'Test release?' 19 | required: true 20 | default: 'false' 21 | 22 | jobs: 23 | build-ios: 24 | name: Jitterbug 25 | runs-on: macos-10.15 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v2 29 | with: 30 | submodules: recursive 31 | - name: Build 32 | run: | 33 | xcodebuild archive -archivePath Jitterbug -scheme Jitterbug -configuration Release CODE_SIGNING_ALLOWED=NO 34 | codesign --force --sign - --timestamp=none --preserve-metadata=identifier,entitlements,flags "Jitterbug.xcarchive/Products/Applications/Jitterbug.app/PlugIns/OpenSSL.framework" 35 | codesign --force --sign - --timestamp=none --preserve-metadata=identifier,entitlements,flags "Jitterbug.xcarchive/Products/Applications/Jitterbug.app/Frameworks/OpenSSL.framework" 36 | codesign --force --sign - --entitlements "JitterbugTunnel/JitterbugTunnel.entitlements" --timestamp=none "Jitterbug.xcarchive/Products/Applications/Jitterbug.app/PlugIns/JitterbugTunnel.appex" 37 | codesign --force --sign - --entitlements "Jitterbug/Jitterbug.entitlements" --timestamp=none "Jitterbug.xcarchive/Products/Applications/Jitterbug.app" 38 | - name: Compress 39 | run: tar cf Jitterbug.xcarchive.tgz Jitterbug.xcarchive 40 | - name: Upload 41 | uses: actions/upload-artifact@v2 42 | with: 43 | name: Jitterbug 44 | path: Jitterbug.xcarchive.tgz 45 | build-ios-lite: 46 | name: Jitterbug Lite 47 | runs-on: macos-10.15 48 | steps: 49 | - name: Checkout 50 | uses: actions/checkout@v2 51 | with: 52 | submodules: recursive 53 | - name: Build 54 | run: xcodebuild archive -archivePath JitterbugLite -scheme JitterbugLite -configuration Release CODE_SIGNING_ALLOWED=NO 55 | - name: Compress 56 | run: tar cf JitterbugLite.xcarchive.tgz JitterbugLite.xcarchive 57 | - name: Upload 58 | uses: actions/upload-artifact@v2 59 | with: 60 | name: JitterbugLite 61 | path: JitterbugLite.xcarchive.tgz 62 | build-macos-ui: 63 | name: Jitterbug (macOS) 64 | runs-on: macos-10.15 65 | steps: 66 | - name: Checkout 67 | uses: actions/checkout@v2 68 | with: 69 | submodules: recursive 70 | - name: Build 71 | run: xcodebuild archive -archivePath JitterbugMac -scheme JitterbugMac -configuration Release 72 | - name: Compress 73 | run: tar cf JitterbugMac.xcarchive.tgz JitterbugMac.xcarchive 74 | - name: Upload 75 | uses: actions/upload-artifact@v2 76 | with: 77 | name: JitterbugMac 78 | path: JitterbugMac.xcarchive.tgz 79 | build-macos: 80 | name: JitterbugPair (macOS) 81 | runs-on: macos-10.15 82 | steps: 83 | - name: Checkout 84 | uses: actions/checkout@v2 85 | with: 86 | submodules: recursive 87 | - name: Dependencies 88 | run: brew install meson openssl@1.1 libusbmuxd libimobiledevice 89 | - name: Build 90 | run: meson --prefix /usr/local --buildtype=release build && cd build && meson compile 91 | env: 92 | PKG_CONFIG_PATH: /usr/local/opt/openssl@1.1/lib/pkgconfig 93 | - name: Upload 94 | uses: actions/upload-artifact@v2 95 | with: 96 | name: jitterbugpair-macos 97 | path: build/jitterbugpair 98 | build-ubuntu: 99 | name: JitterbugPair (Ubuntu) 100 | runs-on: ubuntu-20.04 101 | steps: 102 | - name: Checkout 103 | uses: actions/checkout@v2 104 | with: 105 | submodules: recursive 106 | - name: Dependencies 107 | run: sudo apt install meson libgcrypt-dev libusbmuxd-dev libimobiledevice-dev libunistring-dev 108 | - name: Build 109 | run: meson --prefix /usr --buildtype=release build && cd build && ninja 110 | - name: Upload 111 | uses: actions/upload-artifact@v2 112 | with: 113 | name: jitterbugpair-linux 114 | path: build/jitterbugpair 115 | build-windows: 116 | name: JitterbugPair (Windows) 117 | runs-on: windows-2019 118 | steps: 119 | - uses: msys2/setup-msys2@v2 120 | with: 121 | install: >- 122 | mingw64/mingw-w64-x86_64-gcc 123 | mingw64/mingw-w64-x86_64-pkg-config 124 | mingw64/mingw-w64-x86_64-meson 125 | mingw64/mingw-w64-x86_64-libusbmuxd 126 | mingw64/mingw-w64-x86_64-libimobiledevice 127 | - name: Checkout 128 | uses: actions/checkout@v2 129 | with: 130 | submodules: recursive 131 | - name: Build 132 | run: msys2 -c 'meson --buildtype=release build && cd build && meson compile' 133 | - name: Upload 134 | uses: actions/upload-artifact@v2 135 | with: 136 | name: jitterbugpair-win64 137 | path: | 138 | build/jitterbugpair.exe 139 | build/libwinpthread-1.dll 140 | package-ipa: 141 | name: Package IPA 142 | runs-on: ubuntu-20.04 143 | needs: build-ios 144 | if: github.event_name == 'release' || github.event.inputs.test_release == 'true' 145 | steps: 146 | - name: Download Artifact 147 | uses: actions/download-artifact@v2 148 | with: 149 | name: Jitterbug 150 | - name: Package IPA 151 | run: | 152 | tar xf Jitterbug.xcarchive.tgz 153 | mv Jitterbug.xcarchive/Products/Applications Payload 154 | zip -r Jitterbug.ipa Payload -x "._*" -x ".DS_Store" -x "__MACOSX" 155 | - name: Upload Release Asset 156 | if: github.event_name == 'release' 157 | uses: actions/upload-release-asset@v1.0.2 158 | env: 159 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 160 | with: 161 | upload_url: ${{ github.event.release.upload_url }} 162 | asset_path: Jitterbug.ipa 163 | asset_name: Jitterbug.ipa 164 | asset_content_type: application/octet-stream 165 | - name: Send Dispatch Event 166 | if: github.event_name == 'release' 167 | continue-on-error: true 168 | uses: peter-evans/repository-dispatch@v1 169 | with: 170 | token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 171 | repository: ${{ secrets.DISPATCH_ALTSTORE_REPO_NAME }} 172 | event-type: new-release 173 | package-ipa-lite: 174 | name: Package IPA Lite 175 | runs-on: ubuntu-20.04 176 | needs: build-ios-lite 177 | if: github.event_name == 'release' || github.event.inputs.test_release == 'true' 178 | steps: 179 | - name: Download Artifact 180 | uses: actions/download-artifact@v2 181 | with: 182 | name: JitterbugLite 183 | - name: Package IPA 184 | run: | 185 | tar xf JitterbugLite.xcarchive.tgz 186 | mv JitterbugLite.xcarchive/Products/Applications Payload 187 | zip -r JitterbugLite.ipa Payload -x "._*" -x ".DS_Store" -x "__MACOSX" 188 | - name: Upload Release Asset 189 | if: github.event_name == 'release' 190 | uses: actions/upload-release-asset@v1.0.2 191 | env: 192 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 193 | with: 194 | upload_url: ${{ github.event.release.upload_url }} 195 | asset_path: JitterbugLite.ipa 196 | asset_name: JitterbugLite.ipa 197 | asset_content_type: application/octet-stream 198 | - name: Send Dispatch Event 199 | if: github.event_name == 'release' 200 | continue-on-error: true 201 | uses: peter-evans/repository-dispatch@v1 202 | with: 203 | token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 204 | repository: ${{ secrets.DISPATCH_ALTSTORE_REPO_NAME }} 205 | event-type: new-release 206 | package-dmg: 207 | name: Package DMG 208 | runs-on: macos-10.15 209 | needs: build-macos-ui 210 | if: github.event_name == 'release' || github.event.inputs.test_release == 'true' 211 | steps: 212 | - name: Import signing certificate into keychain 213 | uses: apple-actions/import-codesign-certs@v1 214 | with: 215 | p12-file-base64: ${{ secrets.SIGNING_CERTIFICATE_P12_DATA }} 216 | p12-password: ${{ secrets.SIGNING_CERTIFICATE_PASSWORD }} 217 | - name: Download Artifact 218 | uses: actions/download-artifact@v2 219 | with: 220 | name: JitterbugMac 221 | - name: Sign Binaries 222 | run: | 223 | tar xf JitterbugMac.xcarchive.tgz 224 | codesign --force --sign "Developer ID Application" --timestamp --options runtime "JitterbugMac.xcarchive/Products/Applications/Jitterbug.app/Contents/Frameworks/OpenSSL.framework" 225 | codesign --force --sign "Developer ID Application" --timestamp --options runtime "JitterbugMac.xcarchive/Products/Applications/Jitterbug.app" 226 | - name: Create DMG 227 | run: hdiutil create -fs HFS+ -srcfolder "JitterbugMac.xcarchive/Products/Applications/Jitterbug.app" -volname "Jitterbug" "Jitterbug.dmg" 228 | - name: Notarize app 229 | run: npx notarize-cli --file "Jitterbug.dmg" --bundle-id "com.osy86.Jitterbug" 230 | env: 231 | NOTARIZE_USERNAME: ${{ secrets.SIGNING_USERNAME }} 232 | NOTARIZE_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 233 | - name: Upload Release Asset 234 | if: github.event_name == 'release' 235 | uses: actions/upload-release-asset@v1.0.2 236 | env: 237 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 238 | with: 239 | upload_url: ${{ github.event.release.upload_url }} 240 | asset_path: Jitterbug.dmg 241 | asset_name: Jitterbug.dmg 242 | asset_content_type: application/octet-stream 243 | package-pair: 244 | name: Package JitterbugPair 245 | runs-on: ubuntu-20.04 246 | needs: [build-macos, build-ubuntu, build-windows] 247 | if: github.event_name == 'release' || github.event.inputs.test_release == 'true' 248 | steps: 249 | - name: Download Artifact 250 | uses: actions/download-artifact@v2 251 | - name: Fix Permissions 252 | run: for i in jitterbugpair-*/; do chmod +x $i/* ; zip -r -j "${i%/}.zip" "$i"; done 253 | - name: Upload macOS 254 | if: github.event_name == 'release' 255 | uses: actions/upload-release-asset@v1.0.2 256 | env: 257 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 258 | with: 259 | upload_url: ${{ github.event.release.upload_url }} 260 | asset_path: jitterbugpair-macos.zip 261 | asset_name: jitterbugpair-macos.zip 262 | asset_content_type: application/octet-stream 263 | - name: Upload Linux 264 | if: github.event_name == 'release' 265 | uses: actions/upload-release-asset@v1.0.2 266 | env: 267 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 268 | with: 269 | upload_url: ${{ github.event.release.upload_url }} 270 | asset_path: jitterbugpair-linux.zip 271 | asset_name: jitterbugpair-linux.zip 272 | asset_content_type: application/octet-stream 273 | - name: Upload Windows 274 | if: github.event_name == 'release' 275 | uses: actions/upload-release-asset@v1.0.2 276 | env: 277 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 278 | with: 279 | upload_url: ${{ github.event.release.upload_url }} 280 | asset_path: jitterbugpair-win64.zip 281 | asset_name: jitterbugpair-win64.zip 282 | asset_content_type: application/octet-stream 283 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Libraries/libplist"] 2 | path = Libraries/libplist 3 | url = https://github.com/libimobiledevice/libplist.git 4 | [submodule "Libraries/libusbmuxd"] 5 | path = Libraries/libusbmuxd 6 | url = https://github.com/libimobiledevice/libusbmuxd.git 7 | [submodule "Libraries/libimobiledevice"] 8 | path = Libraries/libimobiledevice 9 | url = https://github.com/libimobiledevice/libimobiledevice.git 10 | [submodule "Libraries/libimobiledevice-glue"] 11 | path = Libraries/libimobiledevice-glue 12 | url = https://github.com/libimobiledevice/libimobiledevice-glue.git 13 | -------------------------------------------------------------------------------- /Build.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | // Configuration settings file format documentation can be found at: 18 | // https://help.apple.com/xcode/#/dev745c5c974 19 | 20 | MARKETING_VERSION = 1.3.1 21 | CURRENT_PROJECT_VERSION = 7 22 | 23 | // Codesigning settings defined optionally, see Documentation/iOSDevelopment.md 24 | #include? "CodeSigning.xcconfig" 25 | -------------------------------------------------------------------------------- /CodeSigning.xcconfig.sample: -------------------------------------------------------------------------------- 1 | // Your Team ID 2 | DEVELOPMENT_TEAM = XYZ0123456 3 | 4 | // Prefix of unique bundle IDs registered to you in Apple Developer Portal. 5 | // You need to register: 6 | // - com.myuniquename.Jitterbug 7 | // - com.myuniquename.JitterbugLite (optional) 8 | // - com.myuniquename.Jitterbug.JitterbugTunnel 9 | // Make sure to include the Network Extensions entitlement unless you are only 10 | // build JitterbugLite. 11 | PRODUCT_BUNDLE_PREFIX = com.myuniquename 12 | -------------------------------------------------------------------------------- /Jitterbug/AddressUtils.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #ifndef AddressUtils_h 18 | #define AddressUtils_h 19 | 20 | #import 21 | 22 | BOOL addressIsLoopback(NSData * _Nonnull data); 23 | NSData * _Nonnull addressIPv4StringToData(NSString * _Nonnull ascii); 24 | NSData * _Nonnull packetReplaceIp(NSData * _Nonnull data, NSString * _Nonnull sourceSearch, NSString * _Nonnull sourceReplace, NSString * _Nonnull destSearch, NSString * _Nonnull destReplace); 25 | 26 | #endif /* AddressUtils_h */ 27 | -------------------------------------------------------------------------------- /Jitterbug/AddressUtils.m: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #include "AddressUtils.h" 18 | #include 19 | 20 | BOOL addressIsLoopback(NSData * _Nonnull data) { 21 | struct sockaddr_storage address = {0}; 22 | struct sockaddr_in *ipv4_addr = (struct sockaddr_in *)&address; 23 | struct sockaddr_in6 *ipv6_addr = (struct sockaddr_in6 *)&address; 24 | 25 | [data getBytes:&address length:(data.length > sizeof(address) ? sizeof(address) : data.length)]; 26 | if (address.ss_family == PF_INET) { 27 | return ipv4_addr->sin_addr.s_addr == htonl(INADDR_LOOPBACK); 28 | } else if (address.ss_family == PF_INET6) { 29 | return IN6_IS_ADDR_LOOPBACK(&ipv6_addr->sin6_addr); 30 | } else { 31 | return NO; 32 | } 33 | } 34 | 35 | NSData * _Nonnull addressIPv4StringToData(NSString * _Nonnull ascii) { 36 | struct sockaddr_in addr = {0}; 37 | addr.sin_len = sizeof(addr); 38 | addr.sin_family = AF_INET; 39 | inet_aton(ascii.UTF8String, &addr.sin_addr); 40 | return [NSData dataWithBytes:&addr length:sizeof(addr)]; 41 | } 42 | 43 | NSData * _Nonnull packetReplaceIp(NSData * _Nonnull data, NSString * _Nonnull sourceSearch, NSString * _Nonnull sourceReplace, NSString * _Nonnull destSearch, NSString * _Nonnull destReplace) { 44 | struct in_addr sourceSearchIp = {0}; 45 | struct in_addr sourceReplaceIp = {0}; 46 | struct in_addr sourcePacketIp = {0}; 47 | struct in_addr destSearchIp = {0}; 48 | struct in_addr destReplaceIp = {0}; 49 | struct in_addr destPacketIp = {0}; 50 | 51 | inet_aton(sourceSearch.UTF8String, &sourceSearchIp); 52 | inet_aton(sourceReplace.UTF8String, &sourceReplaceIp); 53 | inet_aton(destSearch.UTF8String, &destSearchIp); 54 | inet_aton(destReplace.UTF8String, &destReplaceIp); 55 | if (data.length < 20) { 56 | return data; 57 | } 58 | [data getBytes:&sourcePacketIp range:NSMakeRange(12, 4)]; 59 | [data getBytes:&destPacketIp range:NSMakeRange(16, 4)]; 60 | if (sourceSearchIp.s_addr != sourcePacketIp.s_addr && destSearchIp.s_addr != destPacketIp.s_addr) { 61 | return data; 62 | } 63 | NSMutableData *copy = [data mutableCopy]; 64 | if (sourceSearchIp.s_addr == sourcePacketIp.s_addr) { 65 | [copy replaceBytesInRange:NSMakeRange(12, 4) withBytes:&sourceReplaceIp]; 66 | } 67 | if (destSearchIp.s_addr == destPacketIp.s_addr) { 68 | [copy replaceBytesInRange:NSMakeRange(16, 4) withBytes:&destReplaceIp]; 69 | } 70 | return copy; 71 | } 72 | -------------------------------------------------------------------------------- /Jitterbug/AppItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import SwiftUI 18 | 19 | struct AppItemView: View { 20 | @EnvironmentObject private var main: Main 21 | 22 | let app: JBApp 23 | let saved: Bool 24 | let hostIdentifier: String 25 | 26 | var body: some View { 27 | HStack { 28 | Button { 29 | if saved { 30 | main.removeFavorite(appId: app.bundleIdentifier, forHostIdentifier: hostIdentifier) 31 | } else { 32 | main.addFavorite(appId: app.bundleIdentifier, forHostIdentifier: hostIdentifier) 33 | } 34 | } label: { 35 | Label("Save", systemImage: saved ? "star.fill" : "star") 36 | .foregroundColor(.accentColor) 37 | } 38 | IconView(data: app.icon) 39 | Text(app.bundleName) 40 | Spacer() 41 | }.buttonStyle(PlainButtonStyle()) 42 | } 43 | } 44 | 45 | struct IconView: View { 46 | let data: Data 47 | 48 | var body: some View { 49 | #if canImport(UIKit) 50 | if let icon = UIImage(data: data) { 51 | Image(uiImage: icon) 52 | .resizable() 53 | .frame(width: 32, height: 32) 54 | .aspectRatio(contentMode: .fit) 55 | } else { 56 | EmptyView() 57 | .frame(width: 32, height: 32) 58 | } 59 | #elseif canImport(AppKit) 60 | if let icon = NSImage(data: data) { 61 | Image(nsImage: icon) 62 | .resizable() 63 | .frame(width: 32, height: 32) 64 | .aspectRatio(contentMode: .fit) 65 | } else { 66 | EmptyView() 67 | .frame(width: 32, height: 32) 68 | } 69 | #else 70 | #error("Cannot import UIKit or AppKit") 71 | #endif 72 | } 73 | } 74 | 75 | struct AppItemView_Previews: PreviewProvider { 76 | static var previews: some View { 77 | AppItemView(app: JBApp(), saved: true, hostIdentifier: "") 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "extended-srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.063", 9 | "green" : "0.861", 10 | "red" : "0.270" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_20pt@2x-1.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "icon_20pt@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "icon_29pt@2x-1.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "icon_29pt@3x.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "icon_40pt@2x-1.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "icon_40pt@3x.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "icon_60pt@2x.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "icon_60pt@3x.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "icon_20pt.png", 53 | "idiom" : "ipad", 54 | "scale" : "1x", 55 | "size" : "20x20" 56 | }, 57 | { 58 | "filename" : "icon_20pt@2x.png", 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "icon_29pt.png", 65 | "idiom" : "ipad", 66 | "scale" : "1x", 67 | "size" : "29x29" 68 | }, 69 | { 70 | "filename" : "icon_29pt@2x.png", 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "icon_40pt.png", 77 | "idiom" : "ipad", 78 | "scale" : "1x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "icon_40pt@2x.png", 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "icon_76pt.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "icon_76pt@2x.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "icon_83.5@2x.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "Icon.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | }, 111 | { 112 | "filename" : "icon_16pt.png", 113 | "idiom" : "mac", 114 | "scale" : "1x", 115 | "size" : "16x16" 116 | }, 117 | { 118 | "filename" : "icon_16pt@2x.png", 119 | "idiom" : "mac", 120 | "scale" : "2x", 121 | "size" : "16x16" 122 | }, 123 | { 124 | "filename" : "icon_32pt.png", 125 | "idiom" : "mac", 126 | "scale" : "1x", 127 | "size" : "32x32" 128 | }, 129 | { 130 | "filename" : "icon_32pt@2x.png", 131 | "idiom" : "mac", 132 | "scale" : "2x", 133 | "size" : "32x32" 134 | }, 135 | { 136 | "filename" : "icon_128pt.png", 137 | "idiom" : "mac", 138 | "scale" : "1x", 139 | "size" : "128x128" 140 | }, 141 | { 142 | "filename" : "icon_128pt@2x.png", 143 | "idiom" : "mac", 144 | "scale" : "2x", 145 | "size" : "128x128" 146 | }, 147 | { 148 | "filename" : "icon_256pt.png", 149 | "idiom" : "mac", 150 | "scale" : "1x", 151 | "size" : "256x256" 152 | }, 153 | { 154 | "filename" : "icon_256pt@2x.png", 155 | "idiom" : "mac", 156 | "scale" : "2x", 157 | "size" : "256x256" 158 | }, 159 | { 160 | "filename" : "icon_512pt.png", 161 | "idiom" : "mac", 162 | "scale" : "1x", 163 | "size" : "512x512" 164 | }, 165 | { 166 | "filename" : "icon_512pt@2x.png", 167 | "idiom" : "mac", 168 | "scale" : "2x", 169 | "size" : "512x512" 170 | } 171 | ], 172 | "info" : { 173 | "author" : "xcode", 174 | "version" : 1 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/Icon.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_128pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_128pt.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_128pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_128pt@2x.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_16pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_16pt.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_16pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_16pt@2x.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_20pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_20pt.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_256pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_256pt.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_256pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_256pt@2x.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_29pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_29pt.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_32pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_32pt.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_32pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_32pt@2x.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_40pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_40pt.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_512pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_512pt.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_512pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_512pt@2x.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_76pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_76pt.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png -------------------------------------------------------------------------------- /Jitterbug/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Jitterbug/BusyView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import SwiftUI 18 | 19 | struct BusyView: View { 20 | @State var message: String? 21 | 22 | var body: some View { 23 | VStack(alignment: .center) { 24 | BigWhiteSpinner() 25 | if let m = message { 26 | Text(m) 27 | .font(.headline) 28 | .multilineTextAlignment(.center) 29 | } 30 | } 31 | .frame(width: 200, height: 200, alignment: .center) 32 | .foregroundColor(.white) 33 | .background(Color.black.opacity(0.5)) 34 | .clipShape(RoundedRectangle(cornerRadius: 25.0, style: .continuous)) 35 | } 36 | } 37 | 38 | #if os(macOS) 39 | @available(macOS 11, *) 40 | struct BigWhiteSpinner: NSViewRepresentable { 41 | func makeNSView(context: Context) -> NSProgressIndicator { 42 | let view = NSProgressIndicator() 43 | view.style = .spinning 44 | view.startAnimation(self) 45 | return view 46 | } 47 | 48 | func updateNSView(_ nsView: NSProgressIndicator, context: Context) { 49 | } 50 | } 51 | #else // iOS 52 | @available(iOS 14, *) 53 | struct BigWhiteSpinner: UIViewRepresentable { 54 | func makeUIView(context: Context) -> UIActivityIndicatorView { 55 | let view = UIActivityIndicatorView(style: .large) 56 | view.color = .white 57 | view.startAnimating() 58 | return view 59 | } 60 | 61 | func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) { 62 | } 63 | } 64 | #endif 65 | 66 | struct BusyView_Previews: PreviewProvider { 67 | static var previews: some View { 68 | BusyView(message: "Hello World") 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Jitterbug/CacheStorage.c: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #include "CacheStorage.h" 18 | #include "Jitterbug.h" 19 | #include 20 | 21 | typedef struct { 22 | char *udid; 23 | CFDataRef address; 24 | CFDataRef data; 25 | } pairing_t; 26 | 27 | static struct collection g_pairing_cache = {0}; 28 | 29 | int cachePairingAdd(const char *udid, CFDataRef address, CFDataRef data) { 30 | pairing_t *pairing = NULL; 31 | 32 | if (g_pairing_cache.capacity == 0) { 33 | collection_init(&g_pairing_cache); 34 | } 35 | pairing = calloc(sizeof(pairing_t), 1); 36 | pairing->udid = strdup(udid); 37 | pairing->address = CFRetain(address); 38 | pairing->data = CFRetain(data); 39 | collection_add(&g_pairing_cache, pairing); 40 | return 1; 41 | } 42 | 43 | int cachePairingUpdateAddress(const char *udid, CFDataRef address) { 44 | FOREACH(pairing_t *pairing, &g_pairing_cache) { 45 | if (pairing && strcmp(pairing->udid, udid) == 0) { 46 | CFRelease(pairing->address); 47 | pairing->address = CFRetain(address); 48 | return 1; 49 | } 50 | } ENDFOREACH 51 | return 0; 52 | } 53 | 54 | int cachePairingUpdateData(const char *udid, CFDataRef data) { 55 | FOREACH(pairing_t *pairing, &g_pairing_cache) { 56 | if (pairing && strcmp(pairing->udid, udid) == 0) { 57 | CFRelease(pairing->data); 58 | pairing->data = CFRetain(data); 59 | return 1; 60 | } 61 | } ENDFOREACH 62 | return 0; 63 | } 64 | 65 | int cachePairingRemove(const char *udid) { 66 | int ret = 0; 67 | FOREACH(pairing_t *pairing, &g_pairing_cache) { 68 | if (pairing && strcmp(pairing->udid, udid) == 0) { 69 | collection_remove(&g_pairing_cache, pairing); 70 | free(pairing->udid); 71 | CFRelease(pairing->address); 72 | CFRelease(pairing->data); 73 | free(pairing); 74 | ret = 1; 75 | } 76 | } ENDFOREACH 77 | return ret; 78 | } 79 | 80 | int cachePairingGetAddress(const char *udid, char address[static 200]) { 81 | FOREACH(pairing_t *pairing, &g_pairing_cache) { 82 | if (pairing && strcmp(pairing->udid, udid) == 0) { 83 | CFIndex len = CFDataGetLength(pairing->address); 84 | CFDataGetBytes(pairing->address, CFRangeMake(0, len > 200 ? 200 : len), (void *)address); 85 | return 1; 86 | } 87 | } ENDFOREACH 88 | return 0; 89 | } 90 | 91 | int cachePairingGetData(const char *udid, void **data, size_t *len) { 92 | FOREACH(pairing_t *pairing, &g_pairing_cache) { 93 | if (pairing && strcmp(pairing->udid, udid) == 0) { 94 | *len = CFDataGetLength(pairing->data); 95 | *data = malloc(*len); 96 | CFDataGetBytes(pairing->data, CFRangeMake(0, *len), *data); 97 | return 1; 98 | } 99 | } ENDFOREACH 100 | return 0; 101 | } 102 | -------------------------------------------------------------------------------- /Jitterbug/CacheStorage.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #ifndef CacheStorage_h 18 | #define CacheStorage_h 19 | 20 | #include 21 | 22 | int cachePairingAdd(const char *udid, CFDataRef address, CFDataRef data); 23 | int cachePairingUpdateAddress(const char *udid, CFDataRef address); 24 | int cachePairingUpdateData(const char *udid, CFDataRef data); 25 | int cachePairingRemove(const char *udid); 26 | int cachePairingGetAddress(const char *udid, char address[static 200]); 27 | int cachePairingGetData(const char *udid, void **data, size_t *len); 28 | 29 | #endif /* CacheStorage_h */ 30 | -------------------------------------------------------------------------------- /Jitterbug/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import SwiftUI 18 | 19 | struct ContentView: View { 20 | @EnvironmentObject private var main: Main 21 | 22 | var body: some View { 23 | ZStack { 24 | TabView { 25 | LauncherView() 26 | .tabItem { 27 | Label("Launcher", systemImage: "ant") 28 | } 29 | PairingsView() 30 | .tabItem { 31 | Label("Pairings", systemImage: "key") 32 | } 33 | SupportFilesView() 34 | .tabItem { 35 | Label("Support Files", systemImage: "doc.zipper") 36 | } 37 | } 38 | if main.busy { 39 | BusyView(message: main.busyMessage) 40 | } 41 | }.alert(item: $main.alertMessage) { message in 42 | Alert(title: Text(message)) 43 | }.onOpenURL { url in 44 | guard url.scheme == "file" 45 | else { 46 | return // ignore jitterbug urls 47 | } 48 | let type = url.pathExtension 49 | 50 | if type == "dmg" || type == "signature" { 51 | main.backgroundTask(message: NSLocalizedString("Importing support file...", comment: "ContentView")) { 52 | try main.importSupportImage(url) 53 | } 54 | } else if type == "mobiledevicepairing"{ 55 | main.backgroundTask(message: NSLocalizedString("Importing pairing...", comment: "ContentView")) { 56 | try main.importPairing(url) 57 | Thread.sleep(forTimeInterval: 1) 58 | } 59 | } 60 | 61 | 62 | 63 | 64 | 65 | 66 | }.disabled(main.busy) 67 | } 68 | } 69 | 70 | struct ContentView_Previews: PreviewProvider { 71 | static var previews: some View { 72 | ContentView() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Jitterbug/DeviceDetailsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import SwiftUI 18 | 19 | fileprivate enum FileType: Int, Identifiable { 20 | var id: Int { 21 | self.rawValue 22 | } 23 | 24 | case pairing 25 | case supportImage 26 | case supportImageSignature 27 | } 28 | 29 | struct DeviceDetailsView: View { 30 | @EnvironmentObject private var main: Main 31 | @State private var fileSelectType: FileType? 32 | @State private var selectedPairing: URL? 33 | @State private var selectedSupportImage: URL? 34 | @State private var selectedSupportImageSignature: URL? 35 | @State private var apps: [JBApp] = [] 36 | @State private var appToLaunchAfterMount: JBApp? 37 | 38 | let host: JBHostDevice 39 | 40 | private var favoriteApps: [JBApp] { 41 | let favorites = main.getFavorites(forHostIdentifier: host.identifier) 42 | return apps.filter { app in 43 | favorites.contains { favorite in 44 | app.bundleIdentifier == favorite 45 | } 46 | }.sorted { $0.bundleName < $1.bundleName } 47 | } 48 | 49 | private var notFavoriteApps: [JBApp] { 50 | let favorites = main.getFavorites(forHostIdentifier: host.identifier) 51 | return apps.filter { app in 52 | !favorites.contains { favorite in 53 | app.bundleIdentifier == favorite 54 | } 55 | }.sorted { $0.bundleName < $1.bundleName } 56 | } 57 | 58 | var body: some View { 59 | Group { 60 | if host == main.localHost && !main.hasLocalDeviceSupport { 61 | Text("Local device not supported.") 62 | .font(.headline) 63 | } else if !host.isConnected { 64 | Text("Not paired.") 65 | .font(.headline) 66 | } else if apps.isEmpty { 67 | Text("No apps found on device.") 68 | .font(.headline) 69 | } else { 70 | List { 71 | if !main.getFavorites(forHostIdentifier: host.identifier).isEmpty { 72 | Section(header: Text("Favorites")) { 73 | ForEach(favoriteApps) { app in 74 | Button { 75 | launchApplication(app) 76 | } label: { 77 | AppItemView(app: app, saved: true, hostIdentifier: host.identifier) 78 | }.appContextMenu(host: host, app: app) 79 | } 80 | } 81 | } 82 | Section(header: Text("Installed")) { 83 | ForEach(notFavoriteApps) { app in 84 | Button { 85 | launchApplication(app) 86 | } label: { 87 | AppItemView(app: app, saved: false, hostIdentifier: host.identifier) 88 | }.appContextMenu(host: host, app: app) 89 | } 90 | } 91 | } 92 | } 93 | }.navigationTitle(host.name) 94 | .listStyle(PlainListStyle()) 95 | .sheet(item: $fileSelectType) { type in 96 | switch type { 97 | case .pairing: 98 | FileSelectionView(urls: main.pairings, selectedUrl: $selectedPairing, title: Text("Select Pairing")) 99 | case .supportImage: 100 | FileSelectionView(urls: main.supportImages, selectedUrl: $selectedSupportImage, title: Text("Select Image")) 101 | case .supportImageSignature: 102 | FileSelectionView(urls: main.supportImages, selectedUrl: $selectedSupportImageSignature, title: Text("Select Signature")) 103 | } 104 | }.toolbar { 105 | HStack { 106 | Button { 107 | fileSelectType = .pairing 108 | } label: { 109 | Text("Pair") 110 | } 111 | Button { 112 | fileSelectType = .supportImage 113 | } label: { 114 | Text("Mount") 115 | }.disabled(!host.isConnected) 116 | } 117 | }.onAppear { 118 | #if WITH_VPN 119 | let supportVPN = main.localHost == host && main.hasLocalDeviceSupport && !main.isTunnelStarted 120 | #else 121 | let supportVPN = false 122 | #endif 123 | if supportVPN { 124 | main.startTunnel() 125 | } else { 126 | // BUG: sometimes SwiftUI doesn't like this... 127 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1)) { 128 | loadDefaults() 129 | } 130 | } 131 | }.onChange(of: selectedPairing) { url in 132 | guard let selected = url else { 133 | return 134 | } 135 | loadPairing(for: selected) 136 | }.onChange(of: selectedSupportImage) { url in 137 | guard let supportImage = url else { 138 | return 139 | } 140 | let maybeSig = supportImage.appendingPathExtension("signature") 141 | if selectedSupportImageSignature == nil { 142 | if FileManager.default.fileExists(atPath: maybeSig.path) { 143 | selectedSupportImageSignature = maybeSig 144 | } else { 145 | fileSelectType = .supportImageSignature 146 | } 147 | } 148 | }.onChange(of :selectedSupportImageSignature) { url in 149 | guard let supportImage = selectedSupportImage else { 150 | return 151 | } 152 | guard let supportImageSignature = url else { 153 | return 154 | } 155 | mountImage(supportImage, signature: supportImageSignature) 156 | }.onChange(of: main.isTunnelStarted) { started in 157 | resetConnection { 158 | if started { 159 | // BUG: sometimes SwiftUI doesn't like this... 160 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1)) { 161 | loadDefaults() 162 | } 163 | } 164 | } 165 | }.onChange(of: main.selectedLaunchAppId) { appId in 166 | guard apps.count > 0 else { 167 | return 168 | } 169 | guard let autoLaunchApp = try? main.processAutoLaunch(withApps: apps) else { 170 | return 171 | } 172 | launchApplication(autoLaunchApp) 173 | } 174 | } 175 | 176 | private func loadDefaults() { 177 | selectedPairing = main.loadPairing(forHostIdentifier: host.identifier) 178 | selectedSupportImage = main.loadDiskImage(forHostIdentifier: host.identifier) 179 | selectedSupportImageSignature = main.loadDiskImageSignature(forHostIdentifier: host.identifier) 180 | if selectedPairing == nil { 181 | fileSelectType = .pairing 182 | } 183 | } 184 | 185 | private func resetConnection(onComplete: @escaping () -> Void) { 186 | main.backgroundTask(message: NSLocalizedString("Disconnecting...", comment: "DeviceDetailsView")) { 187 | host.stopLockdown() 188 | } onComplete: { 189 | onComplete() 190 | } 191 | } 192 | 193 | private func loadPairing(for selected: URL) { 194 | var success = false 195 | main.backgroundTask(message: NSLocalizedString("Loading pairing data...", comment: "DeviceDetailsView")) { 196 | main.savePairing(nil, forHostIdentifier: host.identifier) 197 | try host.startLockdown(withPairingUrl: selected) 198 | try host.updateInfo() 199 | success = true 200 | } onComplete: { 201 | selectedPairing = nil 202 | if success { 203 | refreshAppsList { 204 | main.savePairing(selected, forHostIdentifier: host.identifier) 205 | } 206 | } 207 | } 208 | } 209 | 210 | private func refreshAppsList(onSuccess: @escaping () -> Void) { 211 | var autoLaunchApp: JBApp? 212 | main.backgroundTask(message: NSLocalizedString("Querying installed apps...", comment: "DeviceDetailsView")) { 213 | try host.updateInfo() 214 | apps = try host.installedApps() 215 | main.archiveSavedHosts() 216 | autoLaunchApp = try main.processAutoLaunch(withApps: apps) 217 | onSuccess() 218 | } onComplete: { 219 | if let app = autoLaunchApp { 220 | launchApplication(app) 221 | } 222 | } 223 | } 224 | 225 | private func mountImage(_ supportImage: URL, signature supportImageSignature: URL) { 226 | main.backgroundTask(message: NSLocalizedString("Mounting disk image...", comment: "DeviceDetailsView")) { 227 | main.saveDiskImage(nil, signature: nil, forHostIdentifier: host.identifier) 228 | try host.mountImage(for: supportImage, signatureUrl: supportImageSignature) 229 | main.saveDiskImage(supportImage, signature: supportImageSignature, forHostIdentifier: host.identifier) 230 | } onComplete: { 231 | selectedSupportImage = nil 232 | selectedSupportImageSignature = nil 233 | if let app = appToLaunchAfterMount { 234 | appToLaunchAfterMount = nil 235 | launchApplication(app) 236 | } 237 | } 238 | } 239 | 240 | private func launchApplication(_ app: JBApp) { 241 | var imageNotMounted = false 242 | main.backgroundTask(message: NSLocalizedString("Launching...", comment: "DeviceDetailsView")) { 243 | do { 244 | try host.launchApplication(app) 245 | } catch { 246 | let code = (error as NSError).code 247 | if code == kJBHostImageNotMounted { 248 | imageNotMounted = true 249 | } else { 250 | throw error 251 | } 252 | } 253 | } onComplete: { 254 | // BUG: SwiftUI shows .disabled() even after it's already done 255 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1)) { 256 | if imageNotMounted { 257 | self.handleImageNotMounted(app: app) 258 | } 259 | } 260 | } 261 | } 262 | 263 | private func handleImageNotMounted(app: JBApp) { 264 | if main.supportImages.isEmpty { 265 | main.alertMessage = NSLocalizedString("Developer image is not mounted. You need DeveloperDiskImage.dmg and DeveloperDiskImage.dmg.signature imported in Support Files.", comment: "DeviceDetailsView") 266 | } else { 267 | fileSelectType = .supportImage 268 | appToLaunchAfterMount = app 269 | } 270 | } 271 | } 272 | 273 | struct AppContextMenuViewModifier: ViewModifier { 274 | @EnvironmentObject private var main: Main 275 | let host: JBHostDevice 276 | let app: JBApp 277 | 278 | func body(content: Content) -> some View { 279 | content.contextMenu { 280 | Button { 281 | UIPasteboard.general.url = main.encodeURL(forHost: host, launchingApp: app) 282 | } label: { 283 | Label("Copy Shortcut URL", systemImage: "link") 284 | .labelStyle(DefaultLabelStyle()) 285 | } 286 | } 287 | } 288 | } 289 | 290 | extension View { 291 | func appContextMenu(host: JBHostDevice, app: JBApp) -> some View { 292 | self.modifier(AppContextMenuViewModifier(host: host, app: app)) 293 | } 294 | } 295 | 296 | struct DeviceDetailsView_Previews: PreviewProvider { 297 | static var previews: some View { 298 | DeviceDetailsView(host: JBHostDevice(hostname: "", address: Data())) 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /Jitterbug/DeviceListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import SwiftUI 18 | 19 | struct DeviceListView: View { 20 | @EnvironmentObject private var main: Main 21 | @State private var showIpAlert: Bool = false 22 | @State private var manualAddHostPresented: Bool = false 23 | 24 | var body: some View { 25 | List { 26 | if !main.savedHosts.isEmpty { 27 | Section(header: Text("Saved")) { 28 | ForEach(main.savedHosts) { host in 29 | NavigationLink(destination: DeviceDetailsView(host: host), tag: host.identifier, selection: $main.selectedHostId) { 30 | HostView(host: host, saved: true) 31 | .foregroundColor(host.discovered ? .primary : .secondary) 32 | }.deviceListContextMenu(host: host) 33 | } 34 | } 35 | } 36 | Section(header: Text("Discovered")) { 37 | ForEach(main.foundHosts) { host in 38 | NavigationLink(destination: DeviceDetailsView(host: host), tag: host.identifier, selection: $main.selectedHostId) { 39 | HostView(host: host, saved: false) 40 | }.deviceListContextMenu(host: host) 41 | } 42 | } 43 | }.navigationTitle("Devices") 44 | .toolbar { 45 | Button { 46 | manualAddHostPresented.toggle() 47 | } label: { 48 | Label("Add", systemImage: "plus") 49 | } 50 | } 51 | .onAppear { 52 | main.startScanning() 53 | } 54 | .onDisappear { 55 | main.stopScanning() 56 | } 57 | .popover(isPresented: $manualAddHostPresented, arrowEdge: .trailing) { 58 | // BUG: SwiftUI won't let us put this on the navbar or it won't close properly 59 | ManualAddHostView() 60 | } 61 | } 62 | } 63 | 64 | struct HostView: View { 65 | @EnvironmentObject private var main: Main 66 | 67 | let host: JBHostDevice 68 | let saved: Bool 69 | 70 | var body: some View { 71 | HStack { 72 | Button { 73 | if saved { 74 | main.removeSavedHost(host) 75 | } else { 76 | main.saveHost(host) 77 | } 78 | } label: { 79 | Label("Save", systemImage: saved ? "star.fill" : "star") 80 | .foregroundColor(.accentColor) 81 | } 82 | switch (host.hostDeviceType) { 83 | case .typeUnknown: 84 | Label("Unknown", systemImage: "questionmark") 85 | case .typeiPhone: 86 | Label("iPhone", systemImage: "apps.iphone") 87 | case .typeiPad: 88 | Label("iPhone", systemImage: "apps.ipad") 89 | @unknown default: 90 | Label("Unknown", systemImage: "questionmark") 91 | } 92 | Text(host.name) 93 | Spacer() 94 | }.buttonStyle(PlainButtonStyle()) 95 | } 96 | } 97 | 98 | struct ContextMenuViewModifier: ViewModifier { 99 | @EnvironmentObject private var main: Main 100 | let host: JBHostDevice 101 | 102 | func body(content: Content) -> some View { 103 | content.contextMenu { 104 | #if os(iOS) 105 | Button { 106 | UIPasteboard.general.url = main.encodeURL(forHost: host) 107 | } label: { 108 | Label("Copy Shortcut URL", systemImage: "link") 109 | .labelStyle(DefaultLabelStyle()) 110 | } 111 | #endif 112 | Button { 113 | main.savePairing(nil, forHostIdentifier: host.identifier) 114 | main.saveDiskImage(nil, signature: nil, forHostIdentifier: host.identifier) 115 | #if os(macOS) 116 | main.backgroundTask(message: NSLocalizedString("Unpairing...", comment: "DeviceListView")) { 117 | if !host.isConnected { 118 | try host.startLockdown() 119 | } 120 | try host.resetPairing() 121 | } 122 | #endif 123 | } label: { 124 | Label("Clear Pairing", systemImage: "xmark.circle") 125 | .labelStyle(DefaultLabelStyle()) 126 | } 127 | } 128 | } 129 | } 130 | 131 | extension View { 132 | func deviceListContextMenu(host: JBHostDevice) -> some View { 133 | self.modifier(ContextMenuViewModifier(host: host)) 134 | } 135 | } 136 | 137 | struct DeviceListView_Previews: PreviewProvider { 138 | static var previews: some View { 139 | DeviceListView() 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Jitterbug/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UniformTypeIdentifiers 18 | 19 | extension URL: Identifiable { 20 | public var id: URL { 21 | self 22 | } 23 | } 24 | 25 | extension String: Identifiable { 26 | public var id: String { 27 | self 28 | } 29 | } 30 | 31 | extension String: LocalizedError { 32 | public var errorDescription: String? { 33 | self 34 | } 35 | } 36 | 37 | extension UTType { 38 | public static let mobileDevicePairing = UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)! 39 | public static let dmg = UTType(filenameExtension: "dmg", conformingTo: .data)! 40 | public static let signature = UTType(filenameExtension: "signature", conformingTo: .data)! 41 | } 42 | -------------------------------------------------------------------------------- /Jitterbug/FileSelectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import SwiftUI 18 | 19 | struct FileSelectionView: View { 20 | let urls: [URL] 21 | @Binding var selectedUrl: URL? 22 | let title: Text 23 | @Environment(\.presentationMode) private var presentationMode: Binding 24 | 25 | var body: some View { 26 | NavigationView { 27 | Group { 28 | if urls.isEmpty { 29 | Text("No files found.") 30 | .font(.headline) 31 | } else { 32 | List { 33 | ForEach(urls) { url in 34 | Button { 35 | presentationMode.wrappedValue.dismiss() 36 | selectedUrl = url 37 | } label: { 38 | Text(url.lastPathComponent) 39 | .lineLimit(1) 40 | } 41 | } 42 | } 43 | } 44 | }.navigationTitle(title) 45 | .navigationViewStyle(StackNavigationViewStyle()) 46 | .listStyle(PlainListStyle()) 47 | .toolbar { 48 | Button { 49 | presentationMode.wrappedValue.dismiss() 50 | selectedUrl = nil 51 | } label: { 52 | Text("Cancel") 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | struct FileSelectionView_Previews: PreviewProvider { 60 | static var previews: some View { 61 | FileSelectionView(urls: [ 62 | .init(fileURLWithPath: "/test1"), 63 | .init(fileURLWithPath: "/test2"), 64 | .init(fileURLWithPath: "/test3") 65 | ], selectedUrl: .constant(nil), title: Text("Hi")) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Jitterbug/HostFinder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | 19 | class HostFinder: NSObject { 20 | private let browser: NetServiceBrowser 21 | private let resolveTimeout = TimeInterval(30) 22 | private var resolving = Set() 23 | private var started = false 24 | 25 | public weak var delegate: HostFinderDelegate? 26 | 27 | override init() { 28 | self.browser = NetServiceBrowser() 29 | super.init() 30 | self.browser.includesPeerToPeer = true 31 | self.browser.delegate = self 32 | } 33 | 34 | func startSearch() { 35 | if !started { 36 | browser.searchForServices(ofType: "_apple-mobdev2._tcp.", inDomain: "local.") 37 | } 38 | started = true 39 | } 40 | 41 | func stopSearch() { 42 | browser.stop() 43 | started = false 44 | } 45 | } 46 | 47 | extension HostFinder: NetServiceBrowserDelegate { 48 | func netServiceBrowser(_ browser: NetServiceBrowser, didFind: NetService, moreComing: Bool) { 49 | NSLog("[HostFinder] resolving %@", didFind.name) 50 | didFind.delegate = self 51 | didFind.resolve(withTimeout: resolveTimeout) 52 | resolving.insert(didFind) 53 | } 54 | 55 | func netServiceBrowser(_ browser: NetServiceBrowser, didRemove: NetService, moreComing: Bool) { 56 | NSLog("[HostFinder] removing %@", didRemove.name) 57 | delegate?.hostFinderRemoveHost(didRemove.name) 58 | } 59 | 60 | func netServiceBrowserWillSearch(_ browser: NetServiceBrowser) { 61 | NSLog("[HostFinder] starting search") 62 | delegate?.hostFinderWillStart() 63 | } 64 | 65 | func netServiceBrowserDidStopSearch(_ browser: NetServiceBrowser) { 66 | NSLog("[HostFinder] stopping search") 67 | delegate?.hostFinderDidStop() 68 | } 69 | } 70 | 71 | extension HostFinder: NetServiceDelegate { 72 | func netServiceDidResolveAddress(_ sender: NetService) { 73 | NSLog("[HostFinder] resolved %@ to %@", sender.name, sender.hostName ?? "(unknown)") 74 | sender.stop() 75 | resolving.remove(sender) 76 | guard let addresses = sender.addresses, addresses.count > 0 else { 77 | delegate?.hostFinderError(NSLocalizedString("Failed to resolve \(sender.name)", comment: "HostFinder")) 78 | return 79 | } 80 | for address in addresses { 81 | // make sure we always return loopback if its available 82 | if addressIsLoopback(address) { 83 | delegate?.hostFinderNewHost(sender.name, name: sender.hostName, address: address) 84 | return 85 | } 86 | } 87 | delegate?.hostFinderNewHost(sender.name, name: sender.hostName, address: addresses[0]) 88 | } 89 | 90 | func netService(_ sender: NetService, didNotResolve errorDict: [String : NSNumber]) { 91 | NSLog("[HostFinder] resolve failed for %@", sender.name) 92 | resolving.remove(sender) 93 | let errorCode = errorDict[NetService.errorCode]! 94 | let errorDomain = errorDict[NetService.errorDomain]! 95 | let error = NSLocalizedString("Resolving \(sender.name) failed with the error domain \(errorDomain), code \(errorCode)", comment: "HostFinder") 96 | delegate?.hostFinderError(error) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Jitterbug/HostFinderDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | protocol HostFinderDelegate : AnyObject { 18 | func hostFinderWillStart() 19 | func hostFinderDidStop() 20 | func hostFinderError(_ error: String) 21 | func hostFinderNewHost(_ host: String, name: String?, address: Data) 22 | func hostFinderRemoveHost(_ host: String) 23 | } 24 | -------------------------------------------------------------------------------- /Jitterbug/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDocumentTypes 8 | 9 | 10 | CFBundleTypeName 11 | Mobile device pairing 12 | LSHandlerRank 13 | Owner 14 | LSItemContentTypes 15 | 16 | com.osy86.mobiledevicepairing 17 | 18 | 19 | 20 | CFBundleTypeName 21 | Developer Image 22 | LSHandlerRank 23 | Default 24 | LSItemContentTypes 25 | 26 | com.apple.disk-image 27 | 28 | 29 | 30 | CFBundleTypeName 31 | Developer Image Signature 32 | LSHandlerRank 33 | Owner 34 | LSItemContentTypes 35 | 36 | com.osy86.developerimagesignature 37 | 38 | 39 | 40 | CFBundleExecutable 41 | $(EXECUTABLE_NAME) 42 | CFBundleIdentifier 43 | $(PRODUCT_BUNDLE_IDENTIFIER) 44 | CFBundleInfoDictionaryVersion 45 | 6.0 46 | CFBundleName 47 | $(PRODUCT_NAME) 48 | CFBundlePackageType 49 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 50 | CFBundleShortVersionString 51 | $(MARKETING_VERSION) 52 | CFBundleURLTypes 53 | 54 | 55 | CFBundleURLName 56 | com.osy86.Jitterbug 57 | CFBundleURLSchemes 58 | 59 | jitterbug 60 | 61 | 62 | 63 | 64 | CFBundleVersion 65 | $(CURRENT_PROJECT_VERSION) 66 | LSRequiresIPhoneOS 67 | 68 | NSBonjourServices 69 | 70 | _apple-mobdev2._tcp 71 | 72 | NSLocalNetworkUsageDescription 73 | Discover iOS devices on the local network. 74 | UIApplicationSceneManifest 75 | 76 | UIApplicationSupportsMultipleScenes 77 | 78 | 79 | UIApplicationSupportsIndirectInputEvents 80 | 81 | UILaunchScreen 82 | 83 | UIRequiredDeviceCapabilities 84 | 85 | armv7 86 | 87 | UISupportedInterfaceOrientations 88 | 89 | UIInterfaceOrientationPortrait 90 | UIInterfaceOrientationLandscapeLeft 91 | UIInterfaceOrientationLandscapeRight 92 | 93 | UISupportedInterfaceOrientations~ipad 94 | 95 | UIInterfaceOrientationPortrait 96 | UIInterfaceOrientationPortraitUpsideDown 97 | UIInterfaceOrientationLandscapeLeft 98 | UIInterfaceOrientationLandscapeRight 99 | 100 | UTExportedTypeDeclarations 101 | 102 | 103 | UTTypeConformsTo 104 | 105 | com.apple.property-list 106 | 107 | UTTypeDescription 108 | Mobile device pairing 109 | UTTypeIconFiles 110 | 111 | UTTypeIdentifier 112 | com.osy86.mobiledevicepairing 113 | UTTypeTagSpecification 114 | 115 | public.filename-extension 116 | 117 | mobiledevicepairing 118 | 119 | 120 | 121 | 122 | UTTypeConformsTo 123 | 124 | com.apple.property-list 125 | 126 | UTTypeDescription 127 | Developer Image 128 | UTTypeIconFiles 129 | 130 | UTTypeIdentifier 131 | com.apple.disk-image 132 | UTTypeTagSpecification 133 | 134 | public.filename-extension 135 | 136 | dmg 137 | 138 | 139 | 140 | 141 | UTTypeConformsTo 142 | 143 | com.apple.property-list 144 | 145 | UTTypeDescription 146 | Developer Image Signature 147 | UTTypeIconFiles 148 | 149 | UTTypeIdentifier 150 | com.osy86.developerimagesignature 151 | UTTypeTagSpecification 152 | 153 | public.filename-extension 154 | 155 | signature 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /Jitterbug/JBApp.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #import 18 | 19 | NS_ASSUME_NONNULL_BEGIN 20 | 21 | @interface JBApp : NSObject 22 | 23 | @property (nonatomic) NSString *bundleName; 24 | @property (nonatomic) NSString *bundleIdentifier; 25 | @property (nonatomic) NSString *bundleExecutable; 26 | @property (nonatomic) NSString *container; 27 | @property (nonatomic) NSString *path; 28 | @property (nonatomic) NSData *icon; 29 | @property (nonatomic, readonly) NSString *executablePath; 30 | 31 | @end 32 | 33 | NS_ASSUME_NONNULL_END 34 | -------------------------------------------------------------------------------- /Jitterbug/JBApp.m: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #import "JBApp.h" 18 | 19 | @implementation JBApp 20 | 21 | - (NSString *)executablePath { 22 | return [self.path stringByAppendingPathComponent:self.bundleExecutable]; 23 | } 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /Jitterbug/JBHostDevice.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #import 18 | #import "JBApp.h" 19 | 20 | typedef NS_ENUM(NSInteger, JBHostDeviceType) { 21 | JBHostDeviceTypeUnknown, 22 | JBHostDeviceTypeiPhone, 23 | JBHostDeviceTypeiPad 24 | }; 25 | 26 | const NSInteger kJBHostImageNotMounted; 27 | 28 | NS_ASSUME_NONNULL_BEGIN 29 | 30 | @interface JBHostDevice : NSObject 31 | 32 | @property (nonatomic) NSString *name; 33 | @property (nonatomic, readonly) BOOL isConnected; 34 | @property (nonatomic, readonly) BOOL isUsbDevice; 35 | @property (nonatomic, readonly) NSString *identifier; 36 | @property (nonatomic, readonly) NSString *hostname; 37 | @property (nonatomic, readonly) NSData *address; 38 | @property (nonatomic) JBHostDeviceType hostDeviceType; 39 | @property (nonatomic) BOOL discovered; 40 | @property (nonatomic, readonly) NSString *udid; 41 | 42 | - (instancetype)init NS_UNAVAILABLE; 43 | - (instancetype)initWithHostname:(NSString *)hostname address:(NSData *)address NS_DESIGNATED_INITIALIZER; 44 | - (instancetype)initWithUdid:(NSString *)udid address:(nullable NSData *)address NS_DESIGNATED_INITIALIZER; 45 | 46 | - (BOOL)startLockdownWithPairingUrl:(NSURL *)url error:(NSError **)error; 47 | - (BOOL)startLockdownWithError:(NSError **)error; 48 | - (void)stopLockdown; 49 | - (void)updateAddress:(NSData *)address; 50 | 51 | - (BOOL)updateDeviceInfoWithError:(NSError **)error; 52 | - (nullable NSArray *)installedAppsWithError:(NSError **)error; 53 | - (BOOL)mountImageForUrl:(NSURL *)url signatureUrl:(NSURL *)signatureUrl error:(NSError **)error; 54 | - (BOOL)launchApplication:(JBApp *)application error:(NSError **)error; 55 | 56 | - (BOOL)resetPairingWithError:(NSError **)error; 57 | - (nullable NSData *)exportPairingWithError:(NSError **)error; 58 | 59 | @end 60 | 61 | NS_ASSUME_NONNULL_END 62 | -------------------------------------------------------------------------------- /Jitterbug/JBHostDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Combine 18 | 19 | @objc extension JBHostDevice: ObservableObject { 20 | func propertyWillChange() -> Void { 21 | DispatchQueue.main.async { self.objectWillChange.send() } 22 | } 23 | } 24 | 25 | @objc extension JBHostDevice: Identifiable { 26 | public var id: String { 27 | self.identifier 28 | } 29 | } 30 | 31 | @objc extension JBApp: Identifiable { 32 | public var id: String { 33 | self.bundleIdentifier 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Jitterbug/JBHostFinder.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #import 18 | #import "JBHostFinderDelegate.h" 19 | 20 | NS_ASSUME_NONNULL_BEGIN 21 | 22 | NS_SWIFT_NAME(HostFinder) 23 | @interface JBHostFinder : NSObject 24 | 25 | @property (nonatomic, weak) id delegate; 26 | 27 | - (void)startSearch; 28 | - (void)stopSearch; 29 | 30 | @end 31 | 32 | NS_ASSUME_NONNULL_END 33 | -------------------------------------------------------------------------------- /Jitterbug/JBHostFinder.m: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #import "JBHostFinder.h" 18 | #import "Jitterbug.h" 19 | #include 20 | 21 | #define TOOL_NAME "jitterbugmac" 22 | 23 | @implementation JBHostFinder 24 | 25 | static void new_device(const idevice_event_t *event, void *user_data) { 26 | JBHostFinder *self = (__bridge JBHostFinder *)user_data; 27 | NSString *udidString = [NSString stringWithUTF8String:event->udid]; 28 | if (event->event == IDEVICE_DEVICE_ADD) { 29 | if (event->conn_type == CONNECTION_NETWORK) { 30 | idevice_info_t *devices; 31 | int i, count; 32 | idevice_get_device_list_extended(&devices, &count); 33 | for (i = 0; i < count; i++) { 34 | idevice_info_t device = devices[i]; 35 | if (device->conn_type == CONNECTION_NETWORK && strcmp(device->udid, event->udid) == 0) { 36 | size_t len = ((uint8_t*)device->conn_data)[0]; 37 | NSData *address = [NSData dataWithBytes:device->conn_data length:len]; 38 | [self.delegate hostFinderNewUdid:udidString address:address]; 39 | break; 40 | } 41 | } 42 | if (i == count) { 43 | DEBUG_PRINT("Failed to find wireless device %s", event->udid); 44 | [self.delegate hostFinderError:[NSString stringWithFormat:NSLocalizedString(@"Failed to get address for wireless device %@", @"JBLocalHostFinder"), udidString]]; 45 | } 46 | idevice_device_list_extended_free(devices); 47 | } else { 48 | [self.delegate hostFinderNewUdid:udidString address:nil]; 49 | } 50 | } else if (event->event == IDEVICE_DEVICE_REMOVE) { 51 | [self.delegate hostFinderRemoveUdid:udidString]; 52 | } 53 | } 54 | 55 | - (void)startSearch { 56 | [self.delegate hostFinderWillStart]; 57 | idevice_event_subscribe(new_device, (__bridge void *)self); 58 | } 59 | 60 | - (void)stopSearch { 61 | idevice_event_unsubscribe(); 62 | [self.delegate hostFinderDidStop]; 63 | } 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /Jitterbug/JBHostFinderDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #import 18 | 19 | NS_ASSUME_NONNULL_BEGIN 20 | 21 | NS_SWIFT_NAME(HostFinderDelegate) 22 | @protocol JBHostFinderDelegate 23 | 24 | - (void)hostFinderWillStart; 25 | - (void)hostFinderDidStop; 26 | - (void)hostFinderError:(NSString *)error; 27 | - (void)hostFinderNewUdid:(NSString *)udid address:(nullable NSData *)address; 28 | - (void)hostFinderRemoveUdid:(NSString *)udid; 29 | 30 | @end 31 | 32 | NS_ASSUME_NONNULL_END 33 | -------------------------------------------------------------------------------- /Jitterbug/Jitterbug-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #ifndef Jitterbug_Bridging_Header_h 18 | #define Jitterbug_Bridging_Header_h 19 | 20 | #import 21 | #import "JBHostDevice.h" 22 | #if TARGET_OS_OSX 23 | #import "JBHostFinder.h" 24 | #import "JBHostFinderDelegate.h" 25 | #endif 26 | #import "AddressUtils.h" 27 | 28 | #endif /* Jitterbug_Bridging_Header_h */ 29 | -------------------------------------------------------------------------------- /Jitterbug/Jitterbug.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.networking.networkextension 6 | 7 | packet-tunnel-provider 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Jitterbug/Jitterbug.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #ifndef Jitterbug_h 18 | #define Jitterbug_h 19 | 20 | #include 21 | 22 | #define DEBUG_PRINT(...) do { \ 23 | fprintf(stderr, "[%s:%d] ", __FUNCTION__, __LINE__); \ 24 | fprintf(stderr, __VA_ARGS__); \ 25 | fprintf(stderr, "\n"); \ 26 | } while (0) 27 | 28 | #endif /* Jitterbug_h */ 29 | -------------------------------------------------------------------------------- /Jitterbug/JitterbugApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import SwiftUI 18 | 19 | private var shortcutHostId: String? 20 | 21 | @main 22 | struct JitterbugApp: App { 23 | @Environment(\.scenePhase) private var scenePhase 24 | @StateObject var main = Main() 25 | #if os(iOS) 26 | @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate 27 | #endif 28 | 29 | var body: some Scene { 30 | WindowGroup { 31 | ContentView() 32 | .environmentObject(main) 33 | .onOpenURL { url in 34 | main.loadURL(url) 35 | } 36 | } 37 | .onChange(of: scenePhase) { newScenePhase in 38 | if newScenePhase == .active { 39 | if shortcutHostId != nil { 40 | main.selectedHostId = shortcutHostId 41 | shortcutHostId = nil 42 | } 43 | } else { 44 | main.archiveSavedHosts() 45 | #if os(iOS) 46 | self.updateQuickActions() 47 | #endif 48 | } 49 | } 50 | } 51 | 52 | #if os(iOS) 53 | private func updateQuickActions() { 54 | let application = UIApplication.shared 55 | application.shortcutItems = main.savedHosts.map({ device -> UIApplicationShortcutItem in 56 | var icon: UIApplicationShortcutIcon? 57 | switch device.hostDeviceType { 58 | case .typeiPhone: 59 | icon = UIApplicationShortcutIcon(systemImageName: "iphone") 60 | case .typeiPad: 61 | icon = UIApplicationShortcutIcon(systemImageName: "ipad") 62 | default: 63 | icon = nil 64 | } 65 | let userInfo: [String: NSSecureCoding] = [ 66 | "identifier": device.identifier as NSSecureCoding, 67 | ] 68 | return UIApplicationShortcutItem(type: "connectHost", localizedTitle: device.name, localizedSubtitle: nil, icon: icon, userInfo: userInfo) 69 | }) 70 | } 71 | 72 | class AppDelegate: NSObject, UIApplicationDelegate { 73 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 74 | let config = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) 75 | config.delegateClass = SceneDelegate.self 76 | return config 77 | } 78 | } 79 | 80 | class SceneDelegate: NSObject, UIWindowSceneDelegate { 81 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 82 | if let shortcutItem = connectionOptions.shortcutItem { 83 | shortcutHostId = shortcutItem.userInfo?["identifier"] as? String 84 | } 85 | } 86 | 87 | func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { 88 | shortcutHostId = shortcutItem.userInfo?["identifier"] as? String 89 | } 90 | } 91 | #endif 92 | } 93 | -------------------------------------------------------------------------------- /Jitterbug/LauncherView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import SwiftUI 18 | 19 | struct LauncherView: View { 20 | @EnvironmentObject private var main: Main 21 | 22 | var body: some View { 23 | NavigationView { 24 | DeviceListView() 25 | .listStyle(PlainListStyle()) 26 | .navigationBarItems(leading: Group { 27 | if main.scanning { 28 | Spinner() 29 | } 30 | }) 31 | PlaceholderView() 32 | }.labelStyle(IconOnlyLabelStyle()) 33 | } 34 | } 35 | 36 | struct PlaceholderView: View { 37 | @EnvironmentObject private var main: Main 38 | 39 | var isPortraitPad: Bool { 40 | let device = UIDevice.current 41 | return device.userInterfaceIdiom == .pad && device.orientation.isPortrait 42 | } 43 | 44 | var body: some View { 45 | if isPortraitPad, let selectedHostId = main.selectedHostId { 46 | if let host = main.savedHosts.first(where: { host in host.identifier == selectedHostId }) { 47 | DeviceDetailsView(host: host) 48 | } else if let host = main.foundHosts.first(where: { host in host.identifier == selectedHostId }) { 49 | DeviceDetailsView(host: host) 50 | } else { 51 | Text("Host not found.") 52 | .font(.headline) 53 | } 54 | } else { 55 | Text("Select a device.") 56 | .font(.headline) 57 | } 58 | } 59 | } 60 | 61 | struct Spinner: UIViewRepresentable { 62 | func makeUIView(context: Context) -> UIActivityIndicatorView { 63 | let view = UIActivityIndicatorView(style: .medium) 64 | view.color = .label 65 | view.startAnimating() 66 | return view 67 | } 68 | 69 | func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) { 70 | } 71 | } 72 | 73 | struct LauncherView_Previews: PreviewProvider { 74 | static var previews: some View { 75 | LauncherView() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Jitterbug/Main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Combine 18 | import NetworkExtension 19 | #if canImport(UIKit) 20 | import UIKit 21 | #endif 22 | 23 | class Main: NSObject, ObservableObject { 24 | @Published var alertMessage: String? 25 | @Published var busy: Bool = false 26 | @Published var busyMessage: String? 27 | 28 | @Published var scanning: Bool = false 29 | @Published var savedHosts: [JBHostDevice] = [] 30 | @Published var foundHosts: [JBHostDevice] = [] 31 | @Published var selectedHostId: String? 32 | @Published var selectedLaunchAppId: String? 33 | 34 | @Published var pairings: [URL] = [] 35 | @Published var supportImages: [URL] = [] 36 | 37 | private let hostFinder = HostFinder() 38 | 39 | @Published var hasLocalDeviceSupport = false 40 | @Published var localHost: JBHostDevice? 41 | @Published var isTunnelStarted: Bool = false 42 | private var vpnObserver: NSObjectProtocol? 43 | private var vpnManager: NETunnelProviderManager! 44 | 45 | private var storage: UserDefaults { 46 | UserDefaults.standard 47 | } 48 | 49 | private var fileManager: FileManager { 50 | FileManager.default 51 | } 52 | 53 | private var documentsURL: URL { 54 | fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0] 55 | } 56 | 57 | private var pairingsURL: URL { 58 | documentsURL.appendingPathComponent("Pairings", isDirectory: true) 59 | } 60 | 61 | private var supportImagesURL: URL { 62 | documentsURL.appendingPathComponent("SupportImages", isDirectory: true) 63 | } 64 | 65 | var tunnelDeviceIp: String { 66 | UserDefaults.standard.string(forKey: "TunnelDeviceIP") ?? "10.8.0.1" 67 | } 68 | 69 | var tunnelFakeIp: String { 70 | UserDefaults.standard.string(forKey: "TunnelFakeIP") ?? "10.8.0.2" 71 | } 72 | 73 | var tunnelSubnetMask: String { 74 | UserDefaults.standard.string(forKey: "TunnelSubnetMask") ?? "255.255.255.0" 75 | } 76 | 77 | var tunnelBundleId: String { 78 | Bundle.main.bundleIdentifier!.appending(".JitterbugTunnel") 79 | } 80 | 81 | override init() { 82 | super.init() 83 | hostFinder.delegate = self 84 | refreshPairings() 85 | refreshSupportImages() 86 | unarchiveSavedHosts() 87 | #if WITH_VPN 88 | initTunnel() 89 | #endif 90 | } 91 | 92 | func backgroundTask(message: String?, task: @escaping () throws -> Void, onComplete: @escaping () -> Void = {}) { 93 | DispatchQueue.main.async { 94 | self.busy = true 95 | self.busyMessage = message 96 | DispatchQueue.global(qos: .background).async { 97 | #if canImport(UIKit) 98 | let app = UIApplication.shared 99 | let bgtask = app.beginBackgroundTask() 100 | #endif 101 | defer { 102 | #if canImport(UIKit) 103 | app.endBackgroundTask(bgtask) 104 | #endif 105 | DispatchQueue.main.async { 106 | self.busy = false 107 | self.busyMessage = nil 108 | onComplete() 109 | } 110 | } 111 | do { 112 | try task() 113 | } catch { 114 | DispatchQueue.main.async { 115 | self.alertMessage = error.localizedDescription 116 | } 117 | } 118 | } 119 | } 120 | } 121 | 122 | // MARK: - File management 123 | 124 | private func importFile(_ file: URL, toDirectory: URL, onComplete: @escaping () -> Void) throws { 125 | let name = file.lastPathComponent 126 | guard name.count > 0 else { 127 | throw NSLocalizedString("Invalid filename.", comment: "Main") 128 | } 129 | let dest = toDirectory.appendingPathComponent(name) 130 | _ = file.startAccessingSecurityScopedResource() 131 | defer { 132 | file.stopAccessingSecurityScopedResource() 133 | } 134 | if !self.fileManager.fileExists(atPath: toDirectory.path) { 135 | try self.fileManager.createDirectory(at: toDirectory, withIntermediateDirectories: false) 136 | } 137 | if self.fileManager.fileExists(atPath: dest.path) { 138 | try self.fileManager.removeItem(at: dest) 139 | } 140 | try self.fileManager.copyItem(at: file, to: dest) 141 | onComplete() 142 | } 143 | 144 | func importPairing(_ pairing: URL) throws { 145 | try importFile(pairing, toDirectory: pairingsURL) { 146 | DispatchQueue.main.async { 147 | self.refreshPairings() 148 | } 149 | } 150 | } 151 | 152 | func importSupportImage(_ support: URL) throws { 153 | try importFile(support, toDirectory: supportImagesURL) { 154 | DispatchQueue.main.async { 155 | self.refreshSupportImages() 156 | } 157 | } 158 | } 159 | 160 | private func refresh(directory: URL, list: inout [URL]) { 161 | guard let contents = try? fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil) else { 162 | return 163 | } 164 | let filtered = contents.filter { newFile in 165 | !list.contains(newFile) 166 | } 167 | if !filtered.isEmpty { 168 | list = contents 169 | } 170 | } 171 | 172 | func refreshPairings() { 173 | refresh(directory: pairingsURL, list: &pairings) 174 | } 175 | 176 | func refreshSupportImages() { 177 | refresh(directory: supportImagesURL, list: &supportImages) 178 | } 179 | 180 | func deletePairing(_ pairing: URL) throws { 181 | try self.fileManager.removeItem(at: pairing) 182 | } 183 | 184 | func deleteSupportImage(_ supportImage: URL) throws { 185 | try self.fileManager.removeItem(at: supportImage) 186 | } 187 | 188 | // MARK: - Save and restore 189 | func archiveSavedHosts() { 190 | guard let data = try? NSKeyedArchiver.archivedData(withRootObject: savedHosts, requiringSecureCoding: false) else { 191 | NSLog("Error archiving hosts") 192 | return 193 | } 194 | storage.set(data, forKey: "SavedHosts") 195 | } 196 | 197 | func unarchiveSavedHosts() { 198 | guard let data = storage.data(forKey: "SavedHosts") else { 199 | return 200 | } 201 | guard let hosts = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [JBHostDevice] else { 202 | NSLog("Error unarchiving hosts") 203 | return 204 | } 205 | savedHosts = hosts 206 | } 207 | 208 | private func saveValue(_ value: Any, forKey key: String, forHostIdentifier hostIdentifier: String) { 209 | var database = storage.dictionary(forKey: "Hosts") ?? [:] 210 | var hostEntry: [String : Any] = database[hostIdentifier] as? [String : Any] ?? [:] 211 | hostEntry[key] = value 212 | database[hostIdentifier] = hostEntry 213 | storage.set(database, forKey: "Hosts") 214 | } 215 | 216 | private func loadValue(forKey key: String, forHostIdentifier hostIdentifier: String) -> Any? { 217 | guard let database = storage.dictionary(forKey: "Hosts") else { 218 | return nil 219 | } 220 | guard let hostEntry = database[hostIdentifier] as? [String : Any] else { 221 | return nil 222 | } 223 | return hostEntry[key] 224 | } 225 | 226 | func savePairing(_ pairing: URL?, forHostIdentifier hostIdentifier: String) { 227 | let file = pairing?.lastPathComponent ?? "" 228 | saveValue(file, forKey: "Pairing", forHostIdentifier: hostIdentifier) 229 | } 230 | 231 | func loadPairing(forHostIdentifier hostIdentifier: String) -> URL? { 232 | guard let file = loadValue(forKey: "Pairing", forHostIdentifier: hostIdentifier) as? String else { 233 | return nil 234 | } 235 | guard file.count > 0 else { 236 | return nil 237 | } 238 | return pairingsURL.appendingPathComponent(file) 239 | } 240 | 241 | func saveDiskImage(_ diskImage: URL?, signature: URL?, forHostIdentifier hostIdentifier: String) { 242 | #if os(macOS) 243 | let diskImageFile = diskImage?.path ?? "" 244 | let diskImageSignatureFile = signature?.path ?? "" 245 | #else 246 | let diskImageFile = diskImage?.lastPathComponent ?? "" 247 | let diskImageSignatureFile = signature?.lastPathComponent ?? "" 248 | #endif 249 | saveValue(diskImageFile, forKey: "DiskImage", forHostIdentifier: hostIdentifier) 250 | saveValue(diskImageSignatureFile, forKey: "DiskImageSignature", forHostIdentifier: hostIdentifier) 251 | } 252 | 253 | func loadDiskImage(forHostIdentifier hostIdentifier: String) -> URL? { 254 | guard let diskImageFile = loadValue(forKey: "DiskImage", forHostIdentifier: hostIdentifier) as? String else { 255 | return nil 256 | } 257 | guard diskImageFile.count > 0 else { 258 | return nil 259 | } 260 | #if os(macOS) 261 | return URL(fileURLWithPath: diskImageFile) 262 | #else 263 | return supportImagesURL.appendingPathComponent(diskImageFile) 264 | #endif 265 | } 266 | 267 | func loadDiskImageSignature(forHostIdentifier hostIdentifier: String) -> URL? { 268 | guard let diskImageSignatureFile = loadValue(forKey: "DiskImageSignature", forHostIdentifier: hostIdentifier) as? String else { 269 | return nil 270 | } 271 | guard diskImageSignatureFile.count > 0 else { 272 | return nil 273 | } 274 | #if os(macOS) 275 | return URL(fileURLWithPath: diskImageSignatureFile) 276 | #else 277 | return supportImagesURL.appendingPathComponent(diskImageSignatureFile) 278 | #endif 279 | } 280 | 281 | func addFavorite(appId: String, forHostIdentifier hostIdentifier: String) { 282 | var favorites = loadValue(forKey: "Favorites", forHostIdentifier: hostIdentifier) as? [String] ?? [] 283 | if !favorites.contains(where: {$0 == appId}) { 284 | favorites.append(appId) 285 | } 286 | saveValue(favorites, forKey: "Favorites", forHostIdentifier: hostIdentifier) 287 | self.objectWillChange.send() 288 | } 289 | 290 | func removeFavorite(appId: String, forHostIdentifier hostIdentifier: String) { 291 | var favorites = loadValue(forKey: "Favorites", forHostIdentifier: hostIdentifier) as? [String] ?? [] 292 | favorites.removeAll(where: {$0 == appId}) 293 | saveValue(favorites, forKey: "Favorites", forHostIdentifier: hostIdentifier) 294 | self.objectWillChange.send() 295 | } 296 | 297 | func getFavorites(forHostIdentifier hostIdentifier: String) -> [String] { 298 | return loadValue(forKey: "Favorites", forHostIdentifier: hostIdentifier) as? [String] ?? [] 299 | } 300 | 301 | // MARK: - Devices 302 | func startScanning() { 303 | hostFinder.startSearch() 304 | } 305 | 306 | func stopScanning() { 307 | hostFinder.stopSearch() 308 | } 309 | 310 | func saveManualHost(identifier: String, address: Data) { 311 | let device = JBHostDevice(hostname: identifier, address: address) 312 | if !savedHosts.contains(where: {$0.identifier == identifier || $0.address == address}) { 313 | saveHost(device) 314 | } 315 | } 316 | 317 | func saveHost(_ host: JBHostDevice) { 318 | savedHosts.append(host) 319 | foundHosts.removeAll(where: {$0.identifier == host.identifier}) 320 | } 321 | 322 | func removeSavedHost(_ host: JBHostDevice) { 323 | savedHosts.removeAll(where: {$0.identifier == host.identifier}) 324 | foundHosts.append(host) 325 | } 326 | } 327 | 328 | extension Main { 329 | func hostFinderWillStart() { 330 | DispatchQueue.main.async { 331 | self.scanning = true 332 | } 333 | } 334 | 335 | func hostFinderDidStop() { 336 | DispatchQueue.main.async { 337 | self.scanning = false 338 | } 339 | } 340 | 341 | func hostFinderError(_ error: String) { 342 | DispatchQueue.main.async { 343 | self.alertMessage = error 344 | } 345 | } 346 | 347 | private func hostFinderNewHost(identifier: String, name: String?, onFound: (JBHostDevice) -> Void) -> Bool { 348 | for hostDevice in self.savedHosts { 349 | if hostDevice.identifier == identifier { 350 | onFound(hostDevice) 351 | if hostDevice.name == identifier, let newName = name { 352 | hostDevice.name = newName 353 | } 354 | hostDevice.discovered = true 355 | self.objectWillChange.send() 356 | return true 357 | } 358 | } 359 | for hostDevice in self.foundHosts { 360 | if hostDevice.identifier == identifier { 361 | onFound(hostDevice) 362 | hostDevice.name = name ?? identifier 363 | hostDevice.discovered = true 364 | self.objectWillChange.send() 365 | return true 366 | } 367 | } 368 | return false 369 | } 370 | 371 | func hostFinderRemove(identifier: String) { 372 | self.savedHosts.filter({$0.identifier == identifier}).forEach { hostDevice in 373 | hostDevice.discovered = false 374 | self.objectWillChange.send() 375 | } 376 | 377 | self.foundHosts.removeAll { hostDevice in 378 | hostDevice.identifier == identifier 379 | } 380 | 381 | } 382 | } 383 | 384 | #if os(macOS) 385 | @objc extension Main: HostFinderDelegate { 386 | func hostFinderNewUdid(_ udid: String, address: Data?) { 387 | DispatchQueue.main.async { 388 | if !self.hostFinderNewHost(identifier: udid, name: nil, onFound: { hostDevice in 389 | if let addr = address { 390 | hostDevice.updateAddress(addr) 391 | } 392 | }) { 393 | let newHost = JBHostDevice(udid: udid, address: address) 394 | newHost.discovered = true 395 | self.foundHosts.append(newHost) 396 | } 397 | } 398 | } 399 | 400 | func hostFinderRemoveUdid(_ udid: String) { 401 | DispatchQueue.main.async { 402 | self.hostFinderRemove(identifier: udid) 403 | } 404 | } 405 | } 406 | #else 407 | extension Main: HostFinderDelegate { 408 | func hostFinderNewHost(_ host: String, name: String?, address: Data) { 409 | DispatchQueue.main.async { 410 | if !self.hostFinderNewHost(identifier: host, name: name, onFound: { hostDevice in 411 | if addressIsLoopback(address) { 412 | self.localHost = hostDevice 413 | } else if hostDevice != self.localHost { 414 | hostDevice.updateAddress(address) 415 | } 416 | }) { 417 | let newHost = JBHostDevice(hostname: host, address: address) 418 | if let newName = name { 419 | newHost.name = newName 420 | } 421 | newHost.discovered = true 422 | if addressIsLoopback(address) { 423 | self.localHost = newHost 424 | } 425 | self.foundHosts.append(newHost) 426 | } 427 | } 428 | } 429 | 430 | func hostFinderRemoveHost(_ host: String) { 431 | DispatchQueue.main.async { 432 | self.hostFinderRemove(identifier: host) 433 | if self.localHost?.identifier == host { 434 | self.localHost = nil 435 | } 436 | } 437 | } 438 | } 439 | #endif 440 | 441 | #if !os(macOS) 442 | // MARK: - VPN Tunnel 443 | extension Main { 444 | private func initTunnel(onSuccess: (() -> ())? = nil) { 445 | NETunnelProviderManager.loadAllFromPreferences { (managers, error) in 446 | if error == nil { 447 | DispatchQueue.main.async { 448 | self.hasLocalDeviceSupport = true 449 | } 450 | } 451 | if !(managers?.isEmpty ?? true), let manager = managers?[0] { 452 | self.vpnManager = manager 453 | onSuccess?() 454 | } 455 | } 456 | } 457 | 458 | private func createAndStartTunnel() { 459 | let manager = NETunnelProviderManager() 460 | manager.localizedDescription = NSLocalizedString("Jitterbug Local Device Tunnel", comment: "Main") 461 | let proto = NETunnelProviderProtocol() 462 | proto.providerBundleIdentifier = tunnelBundleId 463 | proto.serverAddress = "" 464 | manager.protocolConfiguration = proto 465 | manager.isEnabled = true 466 | var success = false 467 | backgroundTask(message: NSLocalizedString("Setting up VPN tunnel...", comment: "Main")) { 468 | let lock = DispatchSemaphore(value: 0) 469 | var error: Error? 470 | manager.saveToPreferences { err in 471 | error = err 472 | lock.signal() 473 | } 474 | lock.wait() 475 | if let err = error { 476 | throw err 477 | } else { 478 | success = true 479 | } 480 | } onComplete: { 481 | if success { 482 | self.initTunnel { 483 | self.startTunnel() 484 | } 485 | } 486 | } 487 | } 488 | 489 | private func startExistingTunnel() { 490 | backgroundTask(message: NSLocalizedString("Starting VPN tunnel...", comment: "Main")) { 491 | guard let manager = self.vpnManager else { 492 | throw NSLocalizedString("No VPN configuration found.", comment: "Main") 493 | } 494 | 495 | if manager.connection.status == .connected { 496 | // Connection already established, nothing to do here 497 | self.setTunnelStarted(true) 498 | return 499 | } 500 | 501 | let lock = DispatchSemaphore(value: 0) 502 | self.vpnObserver = NotificationCenter.default.addObserver(forName: .NEVPNStatusDidChange, object: manager.connection, queue: nil, using: { [weak self] _ in 503 | guard let _self = self else { 504 | return 505 | } 506 | print("[VPN] Connected? \(manager.connection.status == .connected)") 507 | _self.setTunnelStarted(manager.connection.status == .connected, signallingLock: lock) 508 | }) 509 | let options = ["TunnelDeviceIP": self.tunnelDeviceIp as NSObject, 510 | "TunnelFakeIP": self.tunnelFakeIp as NSObject, 511 | "TunnelSubnetMask": self.tunnelSubnetMask as NSObject] 512 | do { 513 | try manager.connection.startVPNTunnel(options: options) 514 | } catch NEVPNError.configurationDisabled { 515 | throw NSLocalizedString("Jitterbug VPN has been disabled in settings or another VPN configuration is selected.", comment: "Main") 516 | } 517 | if lock.wait(timeout: .now() + .seconds(15)) == .timedOut { 518 | throw NSLocalizedString("Failed to start tunnel.", comment: "Main") 519 | } 520 | } 521 | } 522 | 523 | private func setTunnelStarted(_ started: Bool, signallingLock lock: DispatchSemaphore? = nil) { 524 | self.isTunnelStarted = started 525 | 526 | if started { 527 | self.localHost?.updateAddress(addressIPv4StringToData(self.tunnelFakeIp)) 528 | if let lock = lock { 529 | lock.signal() 530 | } 531 | } 532 | } 533 | 534 | func startTunnel() { 535 | if self.vpnManager == nil { 536 | createAndStartTunnel() 537 | } else { 538 | startExistingTunnel() 539 | } 540 | } 541 | 542 | func stopTunnel() { 543 | guard let manager = self.vpnManager else { 544 | return 545 | } 546 | manager.connection.stopVPNTunnel() 547 | } 548 | } 549 | 550 | // MARK: - URL parsing 551 | extension Main { 552 | func encodeURL(forHost host: JBHostDevice, launchingApp app: JBApp? = nil) -> URL { 553 | var components = URLComponents() 554 | components.scheme = "jitterbug" 555 | components.host = host.identifier 556 | if let app = app { 557 | components.queryItems = [ 558 | URLQueryItem(name: "launch", value: app.bundleIdentifier) 559 | ] 560 | } 561 | return components.url! 562 | } 563 | 564 | func loadURL(_ url: URL) { 565 | let components = URLComponents(url: url, resolvingAgainstBaseURL: false)! 566 | guard components.scheme == "jitterbug" else { 567 | if components.scheme != "file" { 568 | print("[URL] unknown scheme \(String(describing: components.scheme))") 569 | } 570 | return 571 | } 572 | selectedHostId = components.host 573 | guard let queryItems = components.queryItems else { 574 | print("[URL] no query to handle") 575 | return 576 | } 577 | for item in queryItems { 578 | if item.name == "launch" { 579 | selectedLaunchAppId = item.value 580 | } 581 | } 582 | } 583 | 584 | func processAutoLaunch(withApps apps: [JBApp]) throws -> JBApp? { 585 | guard let launchId = selectedLaunchAppId else { 586 | return nil 587 | } 588 | DispatchQueue.main.async { 589 | self.selectedLaunchAppId = nil 590 | } 591 | let selectedApp = apps.first { app in 592 | app.bundleIdentifier == launchId 593 | } 594 | if let selectedApp = selectedApp { 595 | return selectedApp 596 | } else { 597 | throw NSLocalizedString("Cannot find \(launchId)", comment: "Main") 598 | } 599 | } 600 | } 601 | #endif 602 | -------------------------------------------------------------------------------- /Jitterbug/ManualAddHostView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import SwiftUI 18 | 19 | struct ManualAddHostView: View { 20 | @EnvironmentObject private var main: Main 21 | @State private var address: String = "" 22 | @Environment(\.presentationMode) private var presentationMode: Binding 23 | 24 | var body: some View { 25 | Form { 26 | List { 27 | #if canImport(UIKit) 28 | TextField("Host", text: $address) 29 | .keyboardType(.asciiCapable) 30 | #else 31 | TextField("Host", text: $address) 32 | #endif 33 | Button { 34 | presentationMode.wrappedValue.dismiss() 35 | addHost(hostString: address) 36 | } label: { 37 | Text("Add") 38 | }.disabled(address.isEmpty) 39 | } 40 | }.frame(minWidth: 250, minHeight: 150) 41 | } 42 | 43 | private func addHost(hostString: String) { 44 | main.backgroundTask(message: NSLocalizedString("Resolving...", comment: "ManualAddHostView")) { 45 | let host = CFHostCreateWithName(kCFAllocatorDefault, hostString as CFString).takeRetainedValue() 46 | CFHostStartInfoResolution(host, .addresses, nil) 47 | var success: DarwinBoolean = false 48 | let addresses = CFHostGetAddressing(host, &success)?.takeUnretainedValue() as NSArray? 49 | if let address = addresses?[0] as? Data { 50 | DispatchQueue.main.async { 51 | main.saveManualHost(identifier: hostString, address: address) 52 | } 53 | } else { 54 | throw NSLocalizedString("Failed to resolve host.", comment: "ManualAddHostView") 55 | } 56 | } 57 | } 58 | } 59 | 60 | struct ManualAddHostView_Previews: PreviewProvider { 61 | static var previews: some View { 62 | ManualAddHostView() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Jitterbug/PairingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import SwiftUI 18 | 19 | struct PairingsView: View { 20 | @EnvironmentObject private var main: Main 21 | @State private var isImporterPresented: Bool = false 22 | 23 | var body: some View { 24 | NavigationView { 25 | Group { 26 | if main.pairings.isEmpty { 27 | VStack { 28 | Text("No pairings found.") 29 | .font(.headline) 30 | Button { 31 | isImporterPresented.toggle() 32 | } label: { 33 | Text("Import Pairings") 34 | .padding() 35 | .foregroundColor(.white) 36 | .background(Color.blue) 37 | .cornerRadius(10) 38 | } 39 | } 40 | } else { 41 | Form { 42 | ForEach(main.pairings) { pairing in 43 | Text(pairing.lastPathComponent) 44 | .lineLimit(1) 45 | }.onDelete { indexSet in 46 | deleteAll(indicies: indexSet) 47 | } 48 | Section { 49 | Button("Import More...") { 50 | isImporterPresented.toggle() 51 | } 52 | .foregroundColor(.blue) 53 | } 54 | } 55 | .listStyle(PlainListStyle()) 56 | } 57 | } 58 | .navigationTitle("Pairings") 59 | .toolbar { 60 | HStack { 61 | Button(action: { isImporterPresented.toggle() }, label: { 62 | Label("Import", systemImage: "square.and.arrow.down") 63 | .labelStyle(IconOnlyLabelStyle()) 64 | }) 65 | if !main.pairings.isEmpty { 66 | EditButton() 67 | } 68 | } 69 | } 70 | .fileImporter(isPresented: $isImporterPresented, allowedContentTypes: [.mobileDevicePairing], onCompletion: importFile) 71 | }.navigationViewStyle(StackNavigationViewStyle()) 72 | } 73 | 74 | private func deleteAll(indicies: IndexSet) { 75 | var toDelete: [URL] = [] 76 | for i in indicies { 77 | toDelete.append(main.pairings[i]) 78 | } 79 | main.backgroundTask(message: NSLocalizedString("Deleting pairing...", comment: "PairingsView")) { 80 | for url in toDelete { 81 | try main.deletePairing(url) 82 | } 83 | } onComplete: { 84 | main.pairings.remove(atOffsets: indicies) 85 | } 86 | } 87 | 88 | private func importFile(result: Result) { 89 | main.backgroundTask(message: NSLocalizedString("Importing pairing...", comment: "PairingsView")) { 90 | let url = try result.get() 91 | try main.importPairing(url) 92 | Thread.sleep(forTimeInterval: 1) 93 | } 94 | } 95 | } 96 | 97 | struct PairingsView_Previews: PreviewProvider { 98 | static var previews: some View { 99 | PairingsView() 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Jitterbug/Settings.bundle/Root.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | StringsTable 6 | Root 7 | PreferenceSpecifiers 8 | 9 | 10 | Type 11 | PSGroupSpecifier 12 | Title 13 | Local Device Tunnel (Advanced) 14 | 15 | 16 | Type 17 | PSTextFieldSpecifier 18 | Title 19 | Tunnel Device IP 20 | Key 21 | TunnelDeviceIP 22 | DefaultValue 23 | 10.8.0.1 24 | IsSecure 25 | 26 | KeyboardType 27 | NumbersAndPunctuation 28 | AutocapitalizationType 29 | None 30 | AutocorrectionType 31 | No 32 | 33 | 34 | Type 35 | PSTextFieldSpecifier 36 | Title 37 | Tunnel Fake Host IP 38 | Key 39 | TunnelFakeIP 40 | DefaultValue 41 | 10.8.0.2 42 | IsSecure 43 | 44 | KeyboardType 45 | NumbersAndPunctuation 46 | AutocapitalizationType 47 | None 48 | AutocorrectionType 49 | No 50 | 51 | 52 | Type 53 | PSTextFieldSpecifier 54 | Title 55 | Tunnel Subnet Mask 56 | Key 57 | TunnelFakeIP 58 | DefaultValue 59 | 255.255.255.0 60 | IsSecure 61 | 62 | KeyboardType 63 | NumbersAndPunctuation 64 | AutocapitalizationType 65 | None 66 | AutocorrectionType 67 | No 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /Jitterbug/Settings.bundle/en.lproj/Root.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osy/Jitterbug/b0449f18c9663c732ee16beefe6df60fa0cea137/Jitterbug/Settings.bundle/en.lproj/Root.strings -------------------------------------------------------------------------------- /Jitterbug/Settings.bundle/zh-Hans.lproj/Root.strings: -------------------------------------------------------------------------------- 1 | /* (No Comment) */ 2 | "Enabled" = "启用"; 3 | 4 | /* A single strings file, whose title is specified in your preferences schema. The strings files provide the localized content to display to the user for each of your preferences. */ 5 | "Group" = "组"; 6 | 7 | /* (No Comment) */ 8 | "Name" = "名称"; 9 | 10 | /* (No Comment) */ 11 | "none given" = "none given"; 12 | 13 | -------------------------------------------------------------------------------- /Jitterbug/SupportFilesView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import SwiftUI 18 | 19 | struct SupportFilesView: View { 20 | @EnvironmentObject private var main: Main 21 | @State private var isImporterPresented: Bool = false 22 | 23 | var body: some View { 24 | NavigationView { 25 | Group { 26 | if main.supportImages.isEmpty { 27 | VStack { 28 | Text("No support files found.") 29 | .font(.headline) 30 | 31 | Button { 32 | isImporterPresented.toggle() 33 | } label: { 34 | Text("Import Support Files") 35 | .padding() 36 | .foregroundColor(.white) 37 | .background(Color.blue) 38 | .cornerRadius(10) 39 | } 40 | } 41 | 42 | } else { 43 | Form { 44 | ForEach(main.supportImages) { pairing in 45 | Text(pairing.lastPathComponent) 46 | .lineLimit(1) 47 | }.onDelete { indexSet in 48 | deleteAll(indicies: indexSet) 49 | } 50 | Section { 51 | Button("Import More...") { 52 | isImporterPresented.toggle() 53 | } 54 | .foregroundColor(.blue) 55 | } 56 | } 57 | .listStyle(PlainListStyle()) 58 | } 59 | } 60 | .navigationTitle("Support Files") 61 | .toolbar { 62 | HStack { 63 | Button(action: { isImporterPresented.toggle() }, label: { 64 | Label("Import", systemImage: "square.and.arrow.down") 65 | .labelStyle(IconOnlyLabelStyle()) 66 | }) 67 | if !main.pairings.isEmpty { 68 | EditButton() 69 | } 70 | } 71 | } 72 | .fileImporter(isPresented: $isImporterPresented, allowedContentTypes: [.dmg, .signature], allowsMultipleSelection: true, onCompletion: importFiles) 73 | }.navigationViewStyle(StackNavigationViewStyle()) 74 | } 75 | 76 | private func deleteAll(indicies: IndexSet) { 77 | var toDelete: [URL] = [] 78 | for i in indicies { 79 | toDelete.append(main.supportImages[i]) 80 | } 81 | main.backgroundTask(message: NSLocalizedString("Deleting support file...", comment: "PairingsView")) { 82 | for url in toDelete { 83 | try main.deleteSupportImage(url) 84 | } 85 | } onComplete: { 86 | main.supportImages.remove(atOffsets: indicies) 87 | } 88 | } 89 | 90 | private func importFiles(result: Result<[URL], Error>) { 91 | main.backgroundTask(message: NSLocalizedString("Importing support file...", comment: "PairingsView")) { 92 | let urls = try result.get() 93 | for url in urls { 94 | try main.importSupportImage(url) 95 | } 96 | Thread.sleep(forTimeInterval: 1) 97 | } 98 | } 99 | } 100 | 101 | struct SupportFilesView_Previews: PreviewProvider { 102 | static var previews: some View { 103 | SupportFilesView() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Jitterbug/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Jitterbug/libusbmuxd-stub.c: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #ifdef HAVE_CONFIG_H 18 | #include 19 | #endif 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #define USBMUXD_API __attribute__((visibility("default"))) 28 | 29 | // usbmuxd public interface 30 | #include "usbmuxd.h" 31 | // usbmuxd protocol 32 | #include "usbmuxd-proto.h" 33 | // custom functions 34 | #include "common/userpref.h" 35 | #include "CacheStorage.h" 36 | #include "Jitterbug.h" 37 | 38 | #pragma mark - Device listing 39 | 40 | USBMUXD_API int usbmuxd_get_device_by_udid(const char *udid, usbmuxd_device_info_t *device) 41 | { 42 | if (!udid) { 43 | DEBUG_PRINT("udid cannot be null!"); 44 | return -EINVAL; 45 | } 46 | if (!device) { 47 | DEBUG_PRINT("device cannot be null!"); 48 | return -EINVAL; 49 | } 50 | if (!cachePairingGetAddress(udid, device->conn_data)) { 51 | DEBUG_PRINT("no cache entry for %s", udid); 52 | return -ENOENT; 53 | } 54 | strcpy(device->udid, udid); 55 | device->conn_type = CONNECTION_TYPE_NETWORK; 56 | return 1; 57 | } 58 | 59 | USBMUXD_API int usbmuxd_get_device(const char *udid, usbmuxd_device_info_t *device, enum usbmux_lookup_options options) 60 | { 61 | if ((options & DEVICE_LOOKUP_USBMUX) != 0) { 62 | DEBUG_PRINT("DEVICE_LOOKUP_USBMUX not supported!"); 63 | return -EINVAL; 64 | } else { 65 | return usbmuxd_get_device_by_udid(udid, device); 66 | } 67 | } 68 | 69 | #pragma mark - Device pairing 70 | 71 | USBMUXD_API int usbmuxd_read_buid(char **buid) 72 | { 73 | // ignore BUID 74 | return -EINVAL; 75 | } 76 | 77 | USBMUXD_API int usbmuxd_read_pair_record(const char* record_id, char **record_data, uint32_t *record_size) 78 | { 79 | void *data; 80 | size_t len; 81 | if (!cachePairingGetData(record_id, &data, &len)) { 82 | DEBUG_PRINT("no cache entry for %s", record_id); 83 | return -ENOENT; 84 | } 85 | *record_data = data; 86 | *record_size = (uint32_t)len; 87 | return 0; 88 | } 89 | 90 | #pragma mark - Unimplemented functions 91 | 92 | USBMUXD_API int usbmuxd_events_subscribe(usbmuxd_subscription_context_t *context, usbmuxd_event_cb_t callback, void *user_data) 93 | { 94 | abort(); 95 | } 96 | 97 | USBMUXD_API int usbmuxd_events_unsubscribe(usbmuxd_subscription_context_t context) 98 | { 99 | abort(); 100 | } 101 | 102 | USBMUXD_API int usbmuxd_get_device_list(usbmuxd_device_info_t **device_list) 103 | { 104 | abort(); 105 | } 106 | 107 | USBMUXD_API int usbmuxd_device_list_free(usbmuxd_device_info_t **device_list) 108 | { 109 | abort(); 110 | } 111 | 112 | USBMUXD_API int usbmuxd_subscribe(usbmuxd_event_cb_t callback, void *user_data) 113 | { 114 | abort(); 115 | } 116 | 117 | USBMUXD_API int usbmuxd_unsubscribe(void) 118 | { 119 | abort(); 120 | } 121 | 122 | USBMUXD_API int usbmuxd_connect(const uint32_t handle, const unsigned short port) 123 | { 124 | abort(); 125 | } 126 | 127 | USBMUXD_API int usbmuxd_disconnect(int sfd) 128 | { 129 | abort(); 130 | } 131 | 132 | USBMUXD_API int usbmuxd_send(int sfd, const char *data, uint32_t len, uint32_t *sent_bytes) 133 | { 134 | abort(); 135 | } 136 | 137 | USBMUXD_API int usbmuxd_recv_timeout(int sfd, char *data, uint32_t len, uint32_t *recv_bytes, unsigned int timeout) 138 | { 139 | abort(); 140 | } 141 | 142 | USBMUXD_API int usbmuxd_recv(int sfd, char *data, uint32_t len, uint32_t *recv_bytes) 143 | { 144 | abort(); 145 | } 146 | 147 | USBMUXD_API int usbmuxd_save_pair_record_with_device_id(const char* record_id, uint32_t device_id, const char *record_data, uint32_t record_size) 148 | { 149 | abort(); 150 | } 151 | 152 | USBMUXD_API int usbmuxd_save_pair_record(const char* record_id, const char *record_data, uint32_t record_size) 153 | { 154 | abort(); 155 | } 156 | 157 | USBMUXD_API int usbmuxd_delete_pair_record(const char* record_id) 158 | { 159 | abort(); 160 | } 161 | 162 | USBMUXD_API void libusbmuxd_set_use_inotify(int set) 163 | { 164 | abort(); 165 | } 166 | 167 | USBMUXD_API void libusbmuxd_set_debug_level(int level) 168 | { 169 | abort(); 170 | } 171 | -------------------------------------------------------------------------------- /Jitterbug/zh-Hans.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Bundle name */ 2 | "CFBundleName" = "Jitterbug"; 3 | 4 | /* (No Comment) */ 5 | "Mobile device pairing" = "设备配对"; 6 | 7 | /* Privacy - Local Network Usage Description */ 8 | "NSLocalNetworkUsageDescription" = "查找局域网内的 iOS 设备"; 9 | 10 | -------------------------------------------------------------------------------- /Jitterbug/zh-Hans.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* No comment provided by engineer. */ 2 | "Add" = "添加"; 3 | 4 | /* No comment provided by engineer. */ 5 | "Cancel" = "取消"; 6 | 7 | /* No comment provided by engineer. */ 8 | "Devices" = "设备列表"; 9 | 10 | /* No comment provided by engineer. */ 11 | "Pairings" = "配对文件"; 12 | 13 | /* No comment provided by engineer. */ 14 | "Support Files" = "支持文件"; 15 | 16 | /* No comment provided by engineer. */ 17 | "Launcher" = "启动器"; 18 | 19 | /* No comment provided by engineer. */ 20 | "ImageMountFailed" = "无法加载镜像"; 21 | 22 | /* No comment provided by engineer. */ 23 | "Cancel" = "取消"; 24 | 25 | /* JBHostDevice */ 26 | "Cannot stat image file!" = "无法加载镜像文件(dmg)"; 27 | 28 | /* JBHostDevice */ 29 | "Cannot stat signature file!" = "无法加载镜像签名文件(signature)"; 30 | 31 | /* JBHostDevice */ 32 | "Could not connect to mobile_image_mounter!" = "无法挂载镜像"; 33 | 34 | /* JBHostDevice */ 35 | "Could not read signature from file." = "无法读取镜像签名文件"; 36 | 37 | /* PairingsView */ 38 | "Deleting pairing..." = "正在删除配对文件..."; 39 | 40 | /* PairingsView */ 41 | "Deleting support file..." = "正在删除支持文件..."; 42 | 43 | /* DeviceDetailsView */ 44 | "Developer image is not mounted. You need DeveloperDiskImage.dmg and DeveloperDiskImage.dmg.signature imported in Support Files." = "开发者镜像未挂载。请在“支持文件”页面导入DeveloperDiskImage.dmg 和 DeveloperDiskImage.dmg.signature。"; 45 | 46 | /* JBHostDevice */ 47 | "Device is locked, can't mount. Unlock device and try again." = "设备未解除锁屏,无法挂载镜像。请解锁设备后再试。"; 48 | 49 | /* DeviceDetailsView */ 50 | "Disconnecting..." = "断开连接中..."; 51 | 52 | /* No comment provided by engineer. */ 53 | "Discovered" = "找到的设备"; 54 | 55 | /* JBHostDevice */ 56 | "Error opening image file." = "无法加载镜像文件。"; 57 | 58 | /* JBHostDevice */ 59 | "Error opening signature file." = "无法加载镜像签名文件。"; 60 | 61 | /* JBHostDevice */ 62 | "Error setting up Wifi debugging." = "无法配置 Wi-Fi 调试。"; 63 | 64 | /* JBHostDevice */ 65 | "Failed cache pairing data." = "缓存配对数据失败。"; 66 | 67 | /* JBHostDevice */ 68 | "Failed to communicate with device. Make sure the device is connected and unlocked and that the pairing is valid." = "无法连接到被调试设备。请确保被调试设备在同一个局域网内,设备已解锁,且配对文件是有效的。"; 69 | 70 | /* JBHostDevice */ 71 | "Failed to communicate with device. Make sure the device is connected, unlocked, and paired." = "无法连接到被调试设备。请确保被调试设备在同一个局域网内,设备已解锁,且设备已匹配。"; 72 | 73 | /* JBHostDevice */ 74 | "Failed to create device." = "创建设备失败。"; 75 | 76 | /* JBHostDevice */ 77 | "Failed to create heartbeat service." = "初始化心跳包服务失败。"; 78 | 79 | /* JBHostDevice */ 80 | "Failed to find pairing record." = "查找配对记录失败。"; 81 | 82 | /* JBHostDevice */ 83 | "Failed to lookup installed apps." = "无法列出已安装 App。"; 84 | 85 | /* JBHostDevice */ 86 | "Failed to read device class." = "无法读取设备类型。"; 87 | 88 | /* JBHostDevice */ 89 | "Failed to read device name." = "无法读取设备名称。"; 90 | 91 | /* JBHostDevice */ 92 | "Failed to reset pairing." = "无法重置配对。"; 93 | 94 | /* HostFinder */ 95 | "Failed to resolve (sender.name)" = "无法解析:(sender.name)。"; 96 | 97 | /* ManualAddHostView */ 98 | "Failed to resolve host." = "无法解析主机。"; 99 | 100 | /* JBHostDevice */ 101 | "Failed to start application." = "无法启动应用。"; 102 | 103 | /* JBHostDevice */ 104 | "Failed to start debugserver. Make sure DeveloperDiskImage.dmg is mounted." = "启动调试服务器失败。请确保DeveloperDiskImage.dmg已挂载。"; 105 | 106 | /* JBHostDevice */ 107 | "Failed to start service on device. Make sure the device is connected to the network and unlocked and that the pairing is valid." = "无法在设备上启动服务。请确保被调试设备在同一个局域网内,设备已解锁,且设备已匹配。"; 108 | 109 | /* Main */ 110 | "Failed to start tunnel." = "无法启动VPN隧道。(请在设置里手动关闭VPN开关再试)"; 111 | 112 | /* No comment provided by engineer. */ 113 | "Favorites" = "收藏"; 114 | 115 | /* No comment provided by engineer. */ 116 | "Hi" = "你好"; 117 | 118 | /* No comment provided by engineer. */ 119 | "Host" = "主机"; 120 | 121 | /* ContentView 122 | PairingsView */ 123 | "Importing pairing..." = "正在导入配对文件..."; 124 | 125 | /* PairingsView */ 126 | "Importing support file..." = "正在导入支持文件..."; 127 | 128 | /* No comment provided by engineer. */ 129 | "Installed" = "已安装"; 130 | 131 | /* Main */ 132 | "Jitterbug Local Device Tunnel" = "Jitterbug 本机调试隧道"; 133 | 134 | /* DeviceDetailsView */ 135 | "Launching..." = "启动中..."; 136 | 137 | /* DeviceDetailsView */ 138 | "Loading pairing data..." = "正在加载配对文件..."; 139 | 140 | /* No comment provided by engineer. */ 141 | "Local device not supported." = "请使用开发者签名激活本机调试功能。"; 142 | 143 | /* No comment provided by engineer. */ 144 | "Mount" = "挂载"; 145 | 146 | /* JBHostDevice */ 147 | "Mount image failed." = "无法挂载镜像。"; 148 | 149 | /* DeviceDetailsView */ 150 | "Mounting disk image..." = "正在挂载镜像..."; 151 | 152 | /* No comment provided by engineer. */ 153 | "No apps found on device." = "未在设备上找到应用程序。"; 154 | 155 | /* No comment provided by engineer. */ 156 | "No files found." = "未找到文件。"; 157 | 158 | /* No comment provided by engineer. */ 159 | "No pairings found." = "未找到配对文件。"; 160 | 161 | /* No comment provided by engineer. */ 162 | "No support files found." = "未找到支持文件。"; 163 | 164 | /* Main */ 165 | "No VPN configuration found." = "未找到 VPN 配置。"; 166 | 167 | /* No comment provided by engineer. */ 168 | "Not paired." = "该设备尚未配对。"; 169 | 170 | /* JBHostDevice */ 171 | "Out of memory!?" = "内存不足!?"; 172 | 173 | /* No comment provided by engineer. */ 174 | "Pair" = "配对"; 175 | 176 | /* JBHostDevice */ 177 | "Pairing data missing key 'UDID'" = "配对文件中缺少UDID信息。"; 178 | 179 | /* DeviceDetailsView */ 180 | "Querying installed apps..." = "正在获取设备已安装应用..."; 181 | 182 | /* HostFinder */ 183 | "Resolving (sender.name) failed with the error domain (errorDomain), code (errorCode)" = "无法解析 (sender.name) 域名: (errorDomain), 错误码: (errorCode)"; 184 | 185 | /* ManualAddHostView */ 186 | "Resolving..." = "正在解析..."; 187 | 188 | /* No comment provided by engineer. */ 189 | "Saved" = "已保存"; 190 | 191 | /* No comment provided by engineer. */ 192 | "Select a device." = "选择一个设备"; 193 | 194 | /* No comment provided by engineer. */ 195 | "Select Image" = "选择镜像(dmg文件)"; 196 | 197 | /* No comment provided by engineer. */ 198 | "Select Pairing" = "选择配对文件"; 199 | 200 | /* No comment provided by engineer. */ 201 | "Select Signature" = "选择镜像签名文件(signature文件)"; 202 | 203 | /* Main */ 204 | "Setting up VPN tunnel..." = "正在启动 VPN 隧道..."; 205 | 206 | /* Main */ 207 | "Starting VPN tunnel..." = "正在启动 VPN 隧道..."; 208 | 209 | /* JBHostDevice */ 210 | "Unknown error occurred, can't mount." = "发生未知错误,无法挂载镜像。(请退出 Jitterbug 后重试。)"; 211 | 212 | /* DeviceListView */ 213 | "Unpairing..." = "取消配对中..."; 214 | 215 | /* JBHostDevice */ 216 | "You must set up a passcode to enable wireless pairing." = "您必须为设备设置锁屏密码才能启用无线调试功能。"; 217 | 218 | "Import More..." = "导入更多..."; 219 | 220 | "Select the file below the list." = "请在列表中选择文件。"; 221 | 222 | "You should select the file that matches the target device." = "您应该选择与目标设备匹配的文件。"; 223 | 224 | "Import Pairings" = "导入配对文件"; 225 | 226 | "Import Support Files" = "导入支持文件"; 227 | 228 | -------------------------------------------------------------------------------- /JitterbugMac/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import SwiftUI 18 | 19 | struct ContentView: View { 20 | @EnvironmentObject private var main: Main 21 | 22 | var body: some View { 23 | ZStack { 24 | NavigationView { 25 | DeviceListView() 26 | .listStyle(SidebarListStyle()) 27 | .frame(minWidth: 200, idealWidth: 200) 28 | Text("Connect an iOS device via USB to start pairing.") 29 | }.labelStyle(IconOnlyLabelStyle()) 30 | if main.busy { 31 | BusyView(message: main.busyMessage) 32 | } 33 | }.frame(minWidth: 800, idealWidth: 800, minHeight: 400, idealHeight: 400) 34 | .alert(item: $main.alertMessage) { message in 35 | Alert(title: Text(message)) 36 | }.onOpenURL { url in 37 | main.backgroundTask(message: NSLocalizedString("Importing pairing...", comment: "ContentView")) { 38 | try main.importPairing(url) 39 | Thread.sleep(forTimeInterval: 1) 40 | } 41 | }.disabled(main.busy) 42 | } 43 | } 44 | 45 | struct ContentView_Previews: PreviewProvider { 46 | static var previews: some View { 47 | ContentView() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /JitterbugMac/DeviceDetailsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import SwiftUI 18 | 19 | struct DeviceDetailsView: View { 20 | @EnvironmentObject private var main: Main 21 | @State private var appsLoaded: Bool = false 22 | @State private var apps: [JBApp] = [] 23 | @State private var fileImporterPresented: Bool = false 24 | @State private var shareFilePresented: Bool = false 25 | @State private var shareFileUrl: URL? 26 | 27 | let host: JBHostDevice 28 | 29 | private var favoriteApps: [JBApp] { 30 | let favorites = main.getFavorites(forHostIdentifier: host.identifier) 31 | return apps.filter { app in 32 | favorites.contains { favorite in 33 | app.bundleIdentifier == favorite 34 | } 35 | } 36 | } 37 | 38 | private var notFavoriteApps: [JBApp] { 39 | let favorites = main.getFavorites(forHostIdentifier: host.identifier) 40 | return apps.filter { app in 41 | !favorites.contains { favorite in 42 | app.bundleIdentifier == favorite 43 | } 44 | } 45 | } 46 | 47 | var body: some View { 48 | Group { 49 | if !host.isConnected { 50 | Text("Not paired.") 51 | .font(.headline) 52 | } else if apps.isEmpty { 53 | Text("No apps found on device.") 54 | .font(.headline) 55 | } else { 56 | List { 57 | if !main.getFavorites(forHostIdentifier: host.identifier).isEmpty { 58 | Section(header: Text("Favorites")) { 59 | ForEach(favoriteApps) { app in 60 | HStack { 61 | AppItemView(app: app, saved: true, hostIdentifier: host.identifier) 62 | Spacer() 63 | Button { 64 | launchApplication(app) 65 | } label: { 66 | Text("Launch") 67 | } 68 | } 69 | } 70 | } 71 | } 72 | Section(header: Text("Installed")) { 73 | ForEach(notFavoriteApps) { app in 74 | HStack { 75 | AppItemView(app: app, saved: false, hostIdentifier: host.identifier) 76 | Spacer() 77 | Button { 78 | launchApplication(app) 79 | } label: { 80 | Text("Launch") 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | }.navigationTitle(host.name) 88 | .toolbar { 89 | ToolbarItemGroup(placement: .primaryAction) { 90 | Button { 91 | refreshAppsList() 92 | } label: { 93 | Label("Refresh", systemImage: "arrow.clockwise") 94 | } 95 | Button { 96 | fileImporterPresented.toggle() 97 | } label: { 98 | Label("Mount Image", systemImage: "externaldrive.badge.plus") 99 | }.disabled(!host.isConnected) 100 | ZStack { 101 | Button { 102 | exportPairing() 103 | } label: { 104 | Label("Export Pairing", systemImage: "square.and.arrow.up") 105 | }.disabled(!host.isConnected) 106 | SharingsPicker(isPresented: $shareFilePresented, sharingItems: [shareFileUrl as Any]) 107 | } 108 | } 109 | }.onAppear { 110 | if !appsLoaded { 111 | appsLoaded = true 112 | refreshAppsList() 113 | } 114 | }.fileImporter(isPresented: $fileImporterPresented, allowedContentTypes: [.dmg]) { result in 115 | if let url = try? result.get() { 116 | mountImage(url) 117 | } 118 | } 119 | } 120 | 121 | private func refreshAppsList() { 122 | main.backgroundTask(message: NSLocalizedString("Querying installed apps...", comment: "DeviceDetailsView")) { 123 | try host.startLockdown() 124 | try host.updateInfo() 125 | apps = try host.installedApps() 126 | main.archiveSavedHosts() 127 | } 128 | } 129 | 130 | private func mountImage(_ supportImage: URL) { 131 | main.backgroundTask(message: NSLocalizedString("Mounting disk image...", comment: "DeviceDetailsView")) { 132 | let supportImageSignature = supportImage.appendingPathExtension("signature") 133 | main.saveDiskImage(nil, signature: nil, forHostIdentifier: host.identifier) 134 | try host.mountImage(for: supportImage, signatureUrl: supportImageSignature) 135 | main.saveDiskImage(supportImage, signature: supportImageSignature, forHostIdentifier: host.identifier) 136 | } 137 | } 138 | 139 | private func launchApplication(_ app: JBApp) { 140 | main.backgroundTask(message: NSLocalizedString("Launching...", comment: "DeviceDetailsView")) { 141 | try host.launchApplication(app) 142 | } 143 | } 144 | 145 | private func exportPairing() { 146 | main.backgroundTask(message: NSLocalizedString("Exporting...", comment: "DeviceDetailsView")) { 147 | let data = try host.exportPairing() 148 | let path = FileManager.default.temporaryDirectory.appendingPathComponent("\(host.udid).mobiledevicepairing") 149 | try data.write(to: path) 150 | DispatchQueue.main.async { 151 | shareFileUrl = path 152 | shareFilePresented.toggle() 153 | } 154 | } 155 | } 156 | } 157 | 158 | struct DeviceDetailsView_Previews: PreviewProvider { 159 | static var previews: some View { 160 | DeviceDetailsView(host: JBHostDevice(hostname: "", address: Data())) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /JitterbugMac/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | LSMinimumSystemVersion 22 | $(MACOSX_DEPLOYMENT_TARGET) 23 | NSHumanReadableCopyright 24 | Copyright © 2021 osy. All rights reserved. 25 | 26 | 27 | -------------------------------------------------------------------------------- /JitterbugMac/JitterbugMac.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /JitterbugMac/JitterbugMacApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import SwiftUI 18 | 19 | @main 20 | struct JitterbugMacApp: App { 21 | @StateObject var main = Main() 22 | 23 | var body: some Scene { 24 | WindowGroup { 25 | ContentView() 26 | .environmentObject(main) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /JitterbugMac/SharingServicePicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2020 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import SwiftUI 18 | 19 | // https://stackoverflow.com/a/60955909/13914748 20 | @available(macOS 11, *) 21 | struct SharingsPicker: NSViewRepresentable { 22 | @Binding var isPresented: Bool 23 | var sharingItems: [Any] = [] 24 | // BUG: really ugly workaround to SwiftUI trying to show this multiple times 25 | static var globalShowOnlyOnce: Bool = false 26 | 27 | func makeNSView(context: Context) -> NSView { 28 | let view = NSView() 29 | return view 30 | } 31 | 32 | func updateNSView(_ nsView: NSView, context: Context) { 33 | if isPresented && !SharingsPicker.globalShowOnlyOnce { 34 | if let _ = nsView.window { 35 | SharingsPicker.globalShowOnlyOnce = true 36 | let picker = NSSharingServicePicker(items: sharingItems) 37 | picker.delegate = context.coordinator 38 | 39 | // !! MUST BE CALLED IN ASYNC, otherwise blocks update 40 | DispatchQueue.main.async { 41 | picker.show(relativeTo: .zero, of: nsView, preferredEdge: .minY) 42 | } 43 | } else { 44 | DispatchQueue.main.async { 45 | isPresented = false 46 | } 47 | } 48 | } 49 | } 50 | 51 | func makeCoordinator() -> Coordinator { 52 | Coordinator(owner: self) 53 | } 54 | 55 | class Coordinator: NSObject, NSSharingServicePickerDelegate { 56 | let owner: SharingsPicker 57 | 58 | init(owner: SharingsPicker) { 59 | self.owner = owner 60 | } 61 | 62 | func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, didChoose service: NSSharingService?) { 63 | 64 | // do here whatever more needed here with selected service 65 | 66 | sharingServicePicker.delegate = nil // << cleanup 67 | self.owner.isPresented = false // << dismiss 68 | SharingsPicker.globalShowOnlyOnce = false 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /JitterbugMac/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /JitterbugMac/zh-Hans.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Bundle name */ 2 | "CFBundleName" = "Jitterbug"; 3 | 4 | /* Copyright (human-readable) */ 5 | "NSHumanReadableCopyright" = "版权所有 © 2021 osy."; 6 | 7 | -------------------------------------------------------------------------------- /JitterbugMac/zh-Hans.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* No comment provided by engineer. */ 2 | "Add" = "添加"; 3 | 4 | /* No comment provided by engineer. */ 5 | "Devices" = "设备列表"; 6 | 7 | /* No comment provided by engineer. */ 8 | "Pairings" = "配对文件"; 9 | 10 | /* No comment provided by engineer. */ 11 | "Support Files" = "支持文件"; 12 | 13 | /* No comment provided by engineer. */ 14 | "Launcher" = "启动器"; 15 | 16 | /* JBHostDevice */ 17 | "Cannot stat image file!" = "无法加载镜像文件(dmg)"; 18 | 19 | /* JBHostDevice */ 20 | "Cannot stat signature file!" = "无法加载镜像签名文件(signature)"; 21 | 22 | /* No comment provided by engineer. */ 23 | "Connect an iOS device via USB to start pairing." = "不支持通过 USB 配对设备。"; 24 | 25 | /* JBHostDevice */ 26 | "Could not connect to mobile_image_mounter!" = "无法挂载镜像"; 27 | 28 | /* JBHostDevice */ 29 | "Could not read signature from file." = "无法读取镜像签名文件"; 30 | 31 | /* JBHostDevice */ 32 | "Device is locked, can't mount. Unlock device and try again." = "设备已锁定,无法挂载镜像。请解锁设备后再试。"; 33 | 34 | /* No comment provided by engineer. */ 35 | "Discovered" = "找到的设备"; 36 | 37 | /* JBHostDevice */ 38 | "Error opening image file." = "无法加载镜像文件。"; 39 | 40 | /* JBHostDevice */ 41 | "Error opening signature file." = "无法加载镜像签名文件。"; 42 | 43 | /* JBHostDevice */ 44 | "Error setting up Wifi debugging." = "无法配置 Wi-Fi 调试。"; 45 | 46 | /* DeviceDetailsView */ 47 | "Exporting..." = "正在导出..."; 48 | 49 | /* JBHostDevice */ 50 | "Failed cache pairing data." = "缓存配对数据失败。"; 51 | 52 | /* JBHostDevice */ 53 | "Failed to communicate with device. Make sure the device is connected and unlocked and that the pairing is valid." = "无法连接到被调试设备。请确保被调试设备在同一个局域网内,设备已解锁,且配对文件是有效的。"; 54 | 55 | /* JBHostDevice */ 56 | "Failed to communicate with device. Make sure the device is connected, unlocked, and paired." = "无法连接到被调试设备。请确保被调试设备在同一个局域网内,设备已解锁,且配对文件是有效的。"; 57 | 58 | /* JBHostDevice */ 59 | "Failed to create device." = "创建设备失败。"; 60 | 61 | /* JBHostDevice */ 62 | "Failed to create heartbeat service." = "初始化心跳包服务失败。"; 63 | 64 | /* JBHostDevice */ 65 | "Failed to find pairing record." = "查找配对记录失败。"; 66 | 67 | /* JBLocalHostFinder */ 68 | "Failed to get address for wireless device %@" = "无法获取 %@ 的地址"; 69 | 70 | /* JBHostDevice */ 71 | "Failed to lookup installed apps." = "无法列出已安装 App。"; 72 | 73 | /* JBHostDevice */ 74 | "Failed to read device class." = "无法读取设备类型。"; 75 | 76 | /* JBHostDevice */ 77 | "Failed to read device name." = "无法读取设备名称。"; 78 | 79 | /* JBHostDevice */ 80 | "Failed to reset pairing." = "无法重置配对。"; 81 | 82 | /* ManualAddHostView */ 83 | "Failed to resolve host." = "无法解析主机。"; 84 | 85 | /* JBHostDevice */ 86 | "Failed to start application." = "无法启动应用。"; 87 | 88 | /* JBHostDevice */ 89 | "Failed to start debugserver. Make sure DeveloperDiskImage.dmg is mounted." = "启动调试服务器失败。请确保DeveloperDiskImage.dmg已挂载。"; 90 | 91 | /* JBHostDevice */ 92 | "Failed to start service on device. Make sure the device is connected to the network and unlocked and that the pairing is valid." = "无法在设备上启动服务。请确保被调试设备在同一个局域网内,设备已解锁,且设备已匹配。"; 93 | 94 | /* Main */ 95 | "Failed to start tunnel." = "无法启动VPN隧道。"; 96 | 97 | /* No comment provided by engineer. */ 98 | "Favorites" = "收藏"; 99 | 100 | /* No comment provided by engineer. */ 101 | "Host" = "主机"; 102 | 103 | /* ContentView */ 104 | "Importing pairing..." = "正在导入配对文件..."; 105 | 106 | /* No comment provided by engineer. */ 107 | "Installed" = "已安装"; 108 | 109 | /* Main */ 110 | "Jitterbug Local Device Tunnel" = "Jitterbug 本机调试隧道"; 111 | 112 | /* No comment provided by engineer. */ 113 | "Launch" = "启动"; 114 | 115 | /* DeviceDetailsView */ 116 | "Launching..." = "启动中..."; 117 | 118 | /* JBHostDevice */ 119 | "Mount image failed." = "无法挂载镜像。"; 120 | 121 | /* DeviceDetailsView */ 122 | "Mounting disk image..." = "正在挂载镜像..."; 123 | 124 | /* No comment provided by engineer. */ 125 | "No apps found on device." = "未在设备上找到应用程序。"; 126 | 127 | /* Main */ 128 | "No VPN configuration found." = "未找到 VPN 配置。"; 129 | 130 | /* No comment provided by engineer. */ 131 | "Not paired." = "该设备尚未配对。"; 132 | 133 | /* JBHostDevice */ 134 | "Out of memory!?" = "内存不足!?"; 135 | 136 | /* JBHostDevice */ 137 | "Pairing data missing key 'UDID'" = "配对文件中缺少UDID信息。"; 138 | 139 | /* DeviceDetailsView */ 140 | "Querying installed apps..." = "正在获取设备已安装应用..."; 141 | 142 | /* ManualAddHostView */ 143 | "Resolving..." = "正在解析..."; 144 | 145 | /* No comment provided by engineer. */ 146 | "Saved" = "已保存"; 147 | 148 | /* Main */ 149 | "Setting up VPN tunnel..." = "正在启动 VPN 隧道..."; 150 | 151 | /* Main */ 152 | "Starting VPN tunnel..." = "正在启动 VPN 隧道..."; 153 | 154 | /* JBHostDevice */ 155 | "Unknown error occurred, can't mount." = "发生未知错误,无法挂载镜像。"; 156 | 157 | /* DeviceListView */ 158 | "Unpairing..." = "取消配对中..."; 159 | 160 | /* JBHostDevice */ 161 | "You must set up a passcode to enable wireless pairing." = "您必须为设备设置锁屏密码才能启用无线调试功能。"; 162 | 163 | -------------------------------------------------------------------------------- /JitterbugPair/main.c: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include "common/userpref.h" 23 | 24 | #include 25 | #include 26 | 27 | #define TOOL_NAME "jitterbugpair" 28 | 29 | static void print_error_message(lockdownd_error_t err, const char *udid) 30 | { 31 | switch (err) { 32 | case LOCKDOWN_E_PASSWORD_PROTECTED: 33 | fprintf(stderr, "ERROR: Could not validate with device %s because a passcode is set. Please enter the passcode on the device and retry.\n", udid); 34 | break; 35 | case LOCKDOWN_E_INVALID_CONF: 36 | case LOCKDOWN_E_INVALID_HOST_ID: 37 | fprintf(stderr, "ERROR: Device %s is not paired with this host\n", udid); 38 | break; 39 | case LOCKDOWN_E_PAIRING_DIALOG_RESPONSE_PENDING: 40 | fprintf(stderr, "ERROR: Please accept the trust dialog on the screen of device %s, then attempt to pair again.\n", udid); 41 | break; 42 | case LOCKDOWN_E_USER_DENIED_PAIRING: 43 | fprintf(stderr, "ERROR: Device %s said that the user denied the trust dialog.\n", udid); 44 | break; 45 | default: 46 | fprintf(stderr, "ERROR: Device %s returned unhandled error code %d\n", udid, err); 47 | break; 48 | } 49 | } 50 | 51 | int print_udids(void) { 52 | unsigned int i; 53 | char **udids = NULL; 54 | unsigned int count = 0; 55 | userpref_get_paired_udids(&udids, &count); 56 | for (i = 0; i < count; i++) { 57 | printf("%s\n", udids[i]); 58 | free(udids[i]); 59 | } 60 | free(udids); 61 | return EXIT_SUCCESS; 62 | } 63 | 64 | int print_help(void) { 65 | fprintf(stderr, "usage: jitterbugpair [OPTIONS]\n"); 66 | fprintf(stderr, "\n"); 67 | fprintf(stderr, "dumps the pairing for the first connected device (unless -u specified)\n"); 68 | fprintf(stderr, "\n"); 69 | fprintf(stderr, "options:\n"); 70 | fprintf(stderr, " -h show this help message\n"); 71 | fprintf(stderr, " -l list UDIDs of all connected devices\n"); 72 | fprintf(stderr, " -u UDID dump connected device with UDID (first device if unspecified)\n"); 73 | fprintf(stderr, " -c dump to stdout instead of file\n"); 74 | fprintf(stderr, "\n"); 75 | return EXIT_FAILURE; 76 | } 77 | 78 | int main(int argc, const char * argv[]) { 79 | int c = 0; 80 | char *path = NULL; 81 | char *udid = NULL; 82 | lockdownd_client_t client = NULL; 83 | idevice_t device = NULL; 84 | idevice_error_t ret = IDEVICE_E_UNKNOWN_ERROR; 85 | lockdownd_error_t lerr; 86 | int result; 87 | char *type = NULL; 88 | plist_t pair_record = NULL; 89 | char *host_id = NULL; 90 | char *session_id = NULL; 91 | 92 | while ((c = getopt(argc, argv, "lu:c")) != -1) { 93 | switch (c) { 94 | case 'l': { 95 | return print_udids(); 96 | } 97 | case 'u': { 98 | udid = strdup(optarg); 99 | break; 100 | } 101 | case 'c': { 102 | path = strdup("/dev/stdout"); 103 | break; 104 | } 105 | case '?': 106 | default: { 107 | return print_help(); 108 | } 109 | } 110 | } 111 | 112 | ret = idevice_new(&device, udid); 113 | if (ret != IDEVICE_E_SUCCESS) { 114 | if (udid) { 115 | fprintf(stderr, "No device found with udid %s.\n", udid); 116 | } else { 117 | fprintf(stderr, "No device found.\n"); 118 | } 119 | result = EXIT_FAILURE; 120 | goto leave; 121 | } 122 | if (!udid) { 123 | ret = idevice_get_udid(device, &udid); 124 | if (ret != IDEVICE_E_SUCCESS) { 125 | fprintf(stderr, "ERROR: Could not get device udid, error code %d\n", ret); 126 | result = EXIT_FAILURE; 127 | goto leave; 128 | } 129 | } 130 | if (!path) { 131 | asprintf(&path, "%s.mobiledevicepairing", udid); 132 | } 133 | 134 | lerr = lockdownd_client_new(device, &client, TOOL_NAME); 135 | if (lerr != LOCKDOWN_E_SUCCESS) { 136 | fprintf(stderr, "ERROR: Could not connect to lockdownd, error code %d\n", lerr); 137 | result = EXIT_FAILURE; 138 | goto leave; 139 | } 140 | 141 | result = EXIT_SUCCESS; 142 | 143 | lerr = lockdownd_query_type(client, &type); 144 | if (lerr != LOCKDOWN_E_SUCCESS) { 145 | fprintf(stderr, "QueryType failed, error code %d\n", lerr); 146 | result = EXIT_FAILURE; 147 | goto leave; 148 | } else { 149 | if (strcmp("com.apple.mobile.lockdown", type)) { 150 | fprintf(stderr, "WARNING: QueryType request returned '%s'\n", type); 151 | } 152 | free(type); 153 | } 154 | 155 | lerr = lockdownd_pair(client, NULL); 156 | if (lerr != LOCKDOWN_E_SUCCESS) { 157 | result = EXIT_FAILURE; 158 | print_error_message(lerr, udid); 159 | goto leave; 160 | } 161 | 162 | userpref_read_pair_record(udid, &pair_record); 163 | plist_dict_set_item(pair_record, "UDID", plist_new_string(udid)); 164 | pair_record_get_host_id(pair_record, &host_id); 165 | 166 | lerr = lockdownd_start_session(client, host_id, &session_id, NULL); 167 | if (lerr != LOCKDOWN_E_SUCCESS) { 168 | result = EXIT_FAILURE; 169 | print_error_message(lerr, udid); 170 | goto leave; 171 | } 172 | 173 | lerr = lockdownd_set_value(client, "com.apple.mobile.wireless_lockdown", "EnableWifiDebugging", plist_new_bool(1)); 174 | if (lerr != LOCKDOWN_E_SUCCESS) { 175 | result = EXIT_FAILURE; 176 | if (lerr == LOCKDOWN_E_UNKNOWN_ERROR) { 177 | fprintf(stderr, "ERROR: You must set up a passcode to enable wireless pairing.\n"); 178 | } else { 179 | print_error_message(lerr, udid); 180 | } 181 | goto leave; 182 | } 183 | 184 | if (!plist_write_to_filename(pair_record, path, PLIST_FORMAT_XML)) { 185 | result = EXIT_FAILURE; 186 | goto leave; 187 | } 188 | plist_free(pair_record); 189 | if (strcmp(path, "/dev/stdout") != 0) { 190 | fprintf(stderr, "SUCCESS: wrote to %s\n", path); 191 | } 192 | 193 | leave: 194 | if (session_id) { 195 | lockdownd_stop_session(client, session_id); 196 | free(session_id); 197 | } 198 | if (host_id) { 199 | free(host_id); 200 | } 201 | lockdownd_client_free(client); 202 | idevice_free(device); 203 | free(udid); 204 | free(path); 205 | 206 | return result; 207 | } 208 | -------------------------------------------------------------------------------- /JitterbugTunnel/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | JitterbugTunnel 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSExtension 24 | 25 | NSExtensionPointIdentifier 26 | com.apple.networkextension.packet-tunnel 27 | NSExtensionPrincipalClass 28 | $(PRODUCT_MODULE_NAME).PacketTunnelProvider 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /JitterbugTunnel/JitterbugTunnel.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.networking.networkextension 6 | 7 | packet-tunnel-provider 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /JitterbugTunnel/PacketTunnelProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2021 osy. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import NetworkExtension 18 | 19 | class PacketTunnelProvider: NEPacketTunnelProvider { 20 | var tunnelDeviceIp: String = "10.8.0.1" 21 | var tunnelFakeIp: String = "10.8.0.2" 22 | var tunnelSubnetMask: String = "255.255.255.0" 23 | 24 | override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { 25 | if let deviceIp = options?["TunnelDeviceIP"] as? String { 26 | tunnelDeviceIp = deviceIp 27 | } 28 | if let fakeIp = options?["TunnelFakeIP"] as? String { 29 | tunnelFakeIp = fakeIp 30 | } 31 | let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: tunnelDeviceIp) 32 | let ipv4 = NEIPv4Settings(addresses: [tunnelDeviceIp], subnetMasks: [tunnelSubnetMask]) 33 | ipv4.includedRoutes = [NEIPv4Route(destinationAddress: tunnelDeviceIp, subnetMask: tunnelSubnetMask)] 34 | ipv4.excludedRoutes = [.default()] 35 | settings.ipv4Settings = ipv4 36 | setTunnelNetworkSettings(settings) { error in 37 | if error == nil { 38 | self.readPackets() 39 | } 40 | completionHandler(error) 41 | } 42 | } 43 | 44 | override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { 45 | // Add code here to start the process of stopping the tunnel. 46 | completionHandler() 47 | } 48 | 49 | override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { 50 | // Add code here to handle the message. 51 | if let handler = completionHandler { 52 | handler(messageData) 53 | } 54 | } 55 | 56 | override func sleep(completionHandler: @escaping () -> Void) { 57 | // Add code here to get ready to sleep. 58 | completionHandler() 59 | } 60 | 61 | override func wake() { 62 | // Add code here to wake up. 63 | } 64 | 65 | private func readPackets() { 66 | packetFlow.readPackets { packets, protocols in 67 | var output: [Data] = [] 68 | for (i, packet) in packets.enumerated() { 69 | let replace: Data 70 | if protocols[i].int32Value == AF_INET { 71 | replace = packetReplaceIp(packet, self.tunnelDeviceIp, self.tunnelFakeIp, self.tunnelFakeIp, self.tunnelDeviceIp) 72 | } else { 73 | replace = packet 74 | } 75 | output.append(replace) 76 | } 77 | self.packetFlow.writePackets(output, withProtocols: protocols) 78 | self.readPackets() 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /JitterbugTunnel/zh-Hans.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Bundle display name */ 2 | "CFBundleDisplayName" = "JitterBug 隧道"; 3 | 4 | /* Bundle name */ 5 | "CFBundleName" = "JitterBug 隧道"; 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Libraries/include/config.h: -------------------------------------------------------------------------------- 1 | #define HAVE_ASPRINTF 1 2 | #define HAVE_DIRENT_D_TYPE 1 3 | #define HAVE_DLFCN_H 1 4 | #define HAVE_FVISIBILITY 1 5 | #define HAVE_GCRYPT_H 1 6 | #define HAVE_GETIFADDRS 1 7 | #define HAVE_GMTIME_R 1 8 | #define HAVE_INTTYPES_H 1 9 | #define HAVE_LOCALTIME_R 1 10 | #define HAVE_MALLOC 1 11 | #define HAVE_MEMMEM 1 12 | #define HAVE_MEMORY_H 1 13 | #define HAVE_OPENSSL 1 14 | #define HAVE_PTHREAD 1 15 | #define HAVE_PTHREAD_CANCEL 1 16 | #define HAVE_PTHREAD_PRIO_INHERIT 1 17 | #define HAVE_REALLOC 1 18 | #define HAVE_SLEEP 1 19 | #define HAVE_STDINT_H 1 20 | #define HAVE_STDLIB_H 1 21 | #define HAVE_STPCPY 1 22 | #define HAVE_STPNCPY 1 23 | #define HAVE_STRCASECMP 1 24 | #define HAVE_STRDUP 1 25 | #define HAVE_STRERROR 1 26 | #define HAVE_STRINGS_H 1 27 | #define HAVE_STRING_H 1 28 | #define HAVE_STRNDUP 1 29 | #define HAVE_STRPTIME 1 30 | #define HAVE_SYS_STAT_H 1 31 | #define HAVE_SYS_TYPES_H 1 32 | #define HAVE_TIMEGM 1 33 | #define HAVE_TM_TM_GMTOFF 1 34 | #define HAVE_TM_TM_ZONE 1 35 | #define HAVE_UNISTD_H 1 36 | #define HAVE_VASPRINTF 1 37 | #define PACKAGE_STRING "jitterbug 1.0" 38 | #define __BIG_ENDIAN 4321 39 | #define __BYTE_ORDER 1234 40 | #define __LITTLE_ENDIAN 1234 41 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | LIBUSBMUXD_CFLAGS := $(shell pkg-config --cflags libusbmuxd-2.0) 2 | LIBUSBMUXD_LDFLAGS := $(shell pkg-config --libs libusbmuxd-2.0) 3 | LIBIMOBILEDEVICE_CFLAGS := $(shell pkg-config --cflags libimobiledevice-1.0) 4 | LIBIMOBILEDEVICE_LDFLAGS := $(shell pkg-config --libs libimobiledevice-1.0) 5 | OPENSSL_CFLAGS := $(shell pkg-config --cflags openssl) 6 | OPENSSL_LDFLAGS := $(shell pkg-config --libs openssl) 7 | 8 | CC := gcc 9 | LD := gcc 10 | CFLAGS := -DHAVE_CONFIG_H -ILibraries/include -ILibraries/libimobiledevice -ILibraries/libimobiledevice/common -ILibraries/libimobiledevice/include $(LIBUSBMUXD_CFLAGS) $(LIBIMOBILEDEVICE_CFLAGS) $(OPENSSL_CLFAGS) 11 | LDFLAGS := $(LIBUSBMUXD_LDFLAGS) $(LIBIMOBILEDEVICE_LDFLAGS) $(OPENSSL_LDFLAGS) 12 | 13 | # path macros 14 | BUILD_PATH := build 15 | 16 | # compile macros 17 | TARGET_NAME := jitterbugpair 18 | ifeq ($(OS),Windows_NT) 19 | TARGET_NAME := $(addsuffix .exe,$(TARGET_NAME)) 20 | endif 21 | TARGET := $(BUILD_PATH)/$(TARGET_NAME) 22 | 23 | # src files & obj files 24 | SRC := JitterbugPair/main.c Libraries/libimobiledevice/common/debug.c Libraries/libimobiledevice/common/userpref.c Libraries/libimobiledevice/common/utils.c 25 | OBJ := $(addprefix $(BUILD_PATH)/, $(addsuffix .o, $(notdir $(basename $(SRC))))) 26 | 27 | # default rule 28 | default: all 29 | 30 | # non-phony targets 31 | $(TARGET): $(OBJ) 32 | $(LD) $(CFLAGS) $(LDFLAGS) -o $@ $^ 33 | 34 | $(BUILD_PATH)/%.o: $(BUILD_PATH)/%.c* 35 | $(CC) $(CFLAGS) -c -o $@ $< 36 | 37 | $(BUILD_PATH)/%.c: $(SRC) 38 | mkdir -p $(BUILD_PATH) || true 39 | cp $^ $(BUILD_PATH)/ 40 | 41 | # phony rules 42 | .PHONY: all 43 | all: $(TARGET) 44 | 45 | .PHONY: clean 46 | clean: 47 | @echo CLEAN $(CLEAN_LIST) 48 | @rm -rf $(BUILD_PATH) 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Jitterbug 2 | ========= 3 | [![Build](https://github.com/osy/Jitterbug/workflows/Build/badge.svg?branch=main&event=push)][1] 4 | 5 | This app uses [libimobiledevice][2] and WiFi pairing to use one iOS device to launch apps with the debugger on another iOS device. This "tethered" launch allows JIT to work on the second iOS device. It can also create a VPN tunnel to allow a device to debug itself. 6 | 7 | ## Download 8 | 9 | [Go to Releases](https://github.com/osy/Jitterbug/releases) 10 | 11 | ## Installation 12 | 13 | [AltStore][6] is the preferred way to install Jitterbug. You can also sideload the IPA through other means or install it on a jailbroken device using [AppSync Unified][7]. 14 | 15 | ## Pairing 16 | 17 | Install Jitterbug on your *primary* device and the app you wish to launch on your *secondary* device. 18 | 19 | On macOS and Windows, make sure you have iTunes installed. On Linux, make sure `usbmuxd` is installed (`sudo apt install usbmuxd`). 20 | 21 | Run `jitterbugpair` with your secondary device plugged in to generate `YOUR-UDID.mobiledevicepairing`. You need to have a passcode enabled and the device should be unlocked. The first time you run the tool, you will get a prompt for your passcode. Type it in and keep the screen on and unlocked and run the tool again to generate the pairing. 22 | 23 | ## Running 24 | 25 | Use AirDrop, email, or another means to copy the `.mobiledevicepairing` to your primary iOS device. When you open it, it should launch Jitterbug and import automatically. 26 | 27 | Download the [Developer Image][3] for the iOS version running on your *secondary* iOS device (or the closest version if your version is not listed) and copy both `DeveloperDiskImage.dmg` and `DeveloperDiskImage.dmg.signature` to your *primary* iOS device. Open Jitterbug, go to "Support Files" and import both files. 28 | 29 | Open Jitterbug, go to "Launcher", and look for your secondary iOS device to show up. If it is not showing up, make sure both devices are connected to the same network (tethering from primary to secondary OR secondary to primary is OK). Select the device and choose the pairing file. Then choose the app to launch and choose the `DeveloperDiskImage.dmg` file. 30 | 31 | ## Development 32 | 33 | ### Building Jitterbug 34 | 35 | 1. Make sure you cloned all submodules with `git submodule update --init --recursive` 36 | 2. Rename `CodeSigning.xcconfig.sample` to `CodeSigning.xcconfig` and edit it, filling in the information detailed in the next section. 37 | 3. Open `Jitterbug.xcodeproj`. 38 | 4. Build and run "Jitterbug" or "JitterbugLite" on your iOS device. 39 | 40 | #### Entitlements 41 | 42 | Jitterbug can create a VPN tunnel so a device can debug itself. This requires the "Network Extensions" entitlement which is not available to free developer accounts. If you have a free account, you will get a code signing error when trying to build. Jitterbug Lite is a build target that does not require Network Extensions and can be built with a free account. 43 | 44 | Because Apple requires unique bundle IDs for developers, you need to choose a unique bundle ID prefix and fill it in `CodeSigning.xcconfig`. 45 | 46 | If you have a paid account, you can find `DEVELOPMENT_TEAM` by going [here](https://developer.apple.com/account/#!/membership). If you have a free account, you can find your Team ID by creating a new Xcode project for iOS, selecting your team under Signing & Capabilities, and looking for the value of `DEVELOPMENT_TEAM` build setting. 47 | 48 | ### Building JitterbugPair 49 | 50 | The software to generate a pairing token is available for macOS, Linux, and Windows. 51 | 52 | #### macOS 53 | 54 | 1. Make sure you cloned all submodules with `git submodule update --init --recursive` 55 | 2. Install [Homebrew][4] if you have not already. 56 | 3. Install dependencies: `brew install meson openssl@1.1 libusbmuxd libimobiledevice pkg-config` 57 | 4. Build with `PKG_CONFIG_PATH="/usr/local/opt/openssl@1.1/lib/pkgconfig" meson build && cd build && meson compile` 58 | 4. The built executable is in `build/jitterbugpair`. You can install it with `meson install`. 59 | 60 | #### Ubuntu 20.04 61 | 62 | 1. Make sure you cloned all submodules with `git submodule update --init --recursive` 63 | 2. Install dependencies: `sudo apt install meson libgcrypt-dev libusbmuxd-dev libimobiledevice-dev libunistring-dev` 64 | 3. Build with `meson build && cd build && ninja` 65 | 4. The built executable is in `build/jitterbugpair`. You can install it with `sudo ninja install`. 66 | 67 | #### Windows 68 | 69 | 1. Install [MSYS][5] and open MSYS shell. 70 | 2. Install dependencies: `pacman -Syy git mingw64/mingw-w64-x86_64-gcc mingw64/mingw-w64-x86_64-pkg-config mingw64/mingw-w64-x86_64-meson mingw64/mingw-w64-x86_64-libusbmuxd mingw64/mingw-w64-x86_64-libimobiledevice` 71 | 3. Clone the repository: `git clone --recursive https://github.com/osy/Jitterbug.git` 72 | 4. Close the MSYS shell and open the MingW64 shell. 73 | 5. Open the cloned repository: `cd Jitterbug` 74 | 6. Build with `meson build && cd build && meson compile` 75 | 7. The built executable is `build/jitterbugpair.exe` and `build/libwinpthread-1.dll`. Both files needs to be in the same directory to run. 76 | 77 | ## Troubleshooting 78 | 79 | ### Mount fails with "ImageMountFailed" 80 | 81 | You may have already mounted the developer image. Try launching an app. 82 | 83 | ### Launch fails with "Failed to get the task for process xxx" 84 | 85 | This application does not have the `get-task-allow` entitlement. Or this is not an development app. 86 | 87 | [1]: https://github.com/osy/Jitterbug/actions/workflows/build.yml?query=event%3Apush 88 | [2]: https://libimobiledevice.org 89 | [3]: https://github.com/xushuduo/Xcode-iOS-Developer-Disk-Image/releases 90 | [4]: https://brew.sh 91 | [5]: https://www.msys2.org 92 | [6]: https://altstore.io 93 | [7]: https://cydia.akemi.ai/?page/net.angelxwind.appsyncunified 94 | -------------------------------------------------------------------------------- /README.zh-Hans.md: -------------------------------------------------------------------------------- 1 | Jitterbug 2 | ========= 3 | [![Build](https://github.com/osy/Jitterbug/workflows/Build/badge.svg?branch=main&event=push)][1] 4 | 5 | 此应用程序使用 [libimobiledevice][2] 和 WiFi 配对来使用一台 iOS 设备在另一台 iOS 设备上通过调试器启动应用程序。 这种“系留”启动允许 JIT 在第二个 iOS 设备上工作。 6 | 7 | ## 下载 8 | 9 | [前往GitHub Releases](https://github.com/osy/Jitterbug/releases) 10 | 11 | ## 编译安装(Building) Jitterbug 12 | 13 | 1. 确保你克隆了所有子模块 (Make sure you cloned all submodules with) `git submodule update --init --recursive` 14 | 2. 打开`Jitterbug.xcodeproj` 并将bundle id 更改为注册到您的Apple Developer 帐户的唯一值。 15 | 3. 在您的 iOS 设备上编译安装并运行“Jitterbug”。 16 | 17 | ## 构建 JitterbugPair 18 | 19 | 生成配对令牌的软件适用于 macOS、Linux 和 Windows。 (The software to generate a pairing token is available for macOS, Linux, and Windows.) 20 | 21 | ### macOS 22 | 23 | 1. 确保您使用 `git submodule update --init --recursive` 克隆了所有子模块 24 | 2. 如果您还没有安装 [Homebrew][4],请安装。 25 | 3. 安装依赖项:`brew install meson openssl@1.1 libusbmuxd libimobiledevice` 26 | 4. 使用`PKG_CONFIG_PATH="/usr/local/opt/openssl@1.1/lib/pkgconfig" meson build && cd build && meson compile `构建 (Build with `PKG_CONFIG_PATH="/usr/local/opt/openssl@1.1/lib/pkgconfig" meson build && cd build && meson compile`) 27 | 4. 构建的可执行文件在 `build/jitterbugpair` 中。 您可以使用“介子安装(meson install)”来安装它。 28 | 29 | ### Ubuntu 20.04 30 | 1. 确保您使用 `git submodule update --init --recursive` 克隆了所有子模块 31 | 2. 安装依赖库: `sudo apt install meson libgcrypt-dev libusbmuxd-dev libimobiledevice-dev libunistring-dev` 32 | 3. 使用`meson build && cd build && ninja`编译安装 33 | 4. 构建的可执行文件在 `build/jitterbugpair` 中。 您可以使用 `sudo ninja install` 安装它。 34 | 35 | ### Windows 36 | 37 | 1. 安装 [MSYS][5] 并打开 MSYS shell。 38 | 2. 安装依赖:`pacman -Syy git mingw64/mingw-w64-x86_64-gcc mingw64/mingw-w64-x86_64-pkg-config mingw64/mingw-w64-x86_64-meson mingw64/mingw-w64-xlibingw64 w64-x86_64-libimobiledevice` 39 | 3. 克隆存储库:`git clone --recursive https://github.com/osy/Jitterbug.git` 40 | 4. 关闭 MSYS 外壳并打开 MingW64 shell。 41 | 5. 打开克隆的存储库:`cd Jitterbug` 42 | 6. 使用 `meson build && cd build && meson compile` 构建 43 | 7. 构建的可执行文件是`build/jitterbugpair.exe` 和`build/libwinpthread-1.dll`。 这两个文件需要在同一目录中才能运行。 44 | 45 | ## 配对 46 | 47 | 在 macOS 和 Windows 上,请确保已安装 iTunes。 在 Linux 上,确保安装了 `usbmuxd`(`sudo apt install usbmuxd`)。 48 | 49 | 在插入设备的情况下运行`jitterbugpair`以生成`YOUR-UDID.mobiledevicepairing`。 您需要启用密码并且设备应该被解锁。 第一次运行该工具时,您将收到输入密码的提示。 输入并保持屏幕开启并解锁,然后再次运行该工具以生成配对。 50 | 51 | ## 开发者形象 (Developer Image) 52 | 53 | 前往 [此处][3] 并下载与目标设备最接近的 iOS 版本对应的 ZIP文件。 解压下载,你应该得到 `DeveloperDiskImage.dmg` 和 `DeveloperDiskImage.dmg.signature`。 54 | 55 | ##运行 56 | 57 | 1. AirDrop(或其他方式)传送`YOUR-UDID.mobiledevicepairing`, `DeveloperDiskImage.dmg`,和`DeveloperDiskImage.dmg.signature` 到安装了 Jitterbug 的设备。 58 | 2. 打开 Jitterbug 并在“配对(Pairings)”下导入您的`.mobiledevicepairing`文件。 在“支持文件(Support Files)”下导入`.dmg`和`.dmg.signature`。 59 | 3. 在“启动器(Launcher")”中,等待目标设备出现。 确保目标设备连接到同一个 WiFi 并已解锁。 60 | 4. 选择目标设备并选择您的`.mobiledevicepairing`文件。 (如果选择器没有自动出现,请点击“配对”。) 61 | 5. 点击您希望在启用 JIT 的情况下启动的应用程序。 62 | 6. 如果这是第一次在此设备上以这种方法启动应用程序,请选择您的`DeveloperDiskImage.dmg`文件。 (如果选择器(Mount)没有自动出现,请点击“安装(install)”。) 63 | 64 | ## 故障排除 65 | 66 | ### 挂载文件失败并显示“ImageMountFailed” 67 | 68 | 您可能已经安装了开发者映像(developer image)。 尝试启动应用程序。 69 | 70 | ### 启动失败并显示 "Failed to get the task for process xxx" 71 | 72 | 此应用程序没有`get-task-allow`权限。 或者这不是一个开发应用程序。 73 | 74 | [1]: https://github.com/osy/Jitterbug/actions/workflows/build.yml?query=event%3Apush 75 | [2]: https://libimobiledevice.org 76 | [3]: https://github.com/xushuduo/Xcode-iOS-Developer-Disk-Image/releases 77 | [4]: https://brew.sh 78 | [5]: https://www.msys2.org -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('jitterbugpair', 'c') 2 | 3 | sources = ['JitterbugPair/main.c'] 4 | incdir = include_directories(['Libraries/include', 5 | 'Libraries/libimobiledevice', 6 | 'Libraries/libimobiledevice/common', 7 | 'Libraries/libimobiledevice/include', 8 | 'Libraries/libimobiledevice-glue/include']) 9 | 10 | os = host_machine.system() 11 | cflags = [] 12 | ldflags = [] 13 | 14 | if os == 'linux' 15 | crypto = dependency('libgcrypt', static: true) 16 | libusbmuxd = dependency('libusbmuxd', static: true) 17 | cflags += ['-DHAVE_ASPRINTF', '-DHAVE_VASPRINTF'] 18 | else 19 | crypto = dependency('openssl', version: ['>= 0.9.8'], static: true) 20 | libusbmuxd = dependency('libusbmuxd-2.0', static: true) 21 | cflags += ['-DHAVE_CONFIG_H'] 22 | if os == 'windows' 23 | ldflags += ['-lIphlpapi'] 24 | endif 25 | endif 26 | 27 | if os == 'windows' 28 | winpthread_path = run_command('which', 'libwinpthread-1.dll').stdout() 29 | winpthread = custom_target('winpthread', 30 | output: 'libwinpthread-1.dll', 31 | command: ['cp', winpthread_path, '@OUTPUT@'], 32 | install: false, 33 | build_by_default: true) 34 | endif 35 | 36 | libimobiledevice = dependency('libimobiledevice-1.0', static: true) 37 | dependencies = [crypto, libusbmuxd, libimobiledevice] 38 | executable('jitterbugpair', 39 | sources, 40 | include_directories: incdir, 41 | dependencies: dependencies, 42 | c_args: cflags, 43 | link_args: ldflags, 44 | install: true) 45 | --------------------------------------------------------------------------------