Compare commits

...

222 Commits
1.3b3 ... 1.4.3

Author SHA1 Message Date
Riley Testut
2354f85998 [Apps] Updates AltStore to 1.4.3 2021-02-02 13:53:20 -06:00
Riley Testut
e1a6bd3d53 [AltServer] Updates app version to 1.5b3 2021-02-02 12:30:49 -06:00
Riley Testut
1420cbd86e Updates app version to 1.4.3b2 2021-02-02 12:28:15 -06:00
Riley Testut
a3a69b5cbd [Apps] Updates AltStore beta to 1.4.3b2 2021-02-02 12:26:43 -06:00
Riley Testut
b2ee7cfa2c [Apps] Updates Delta beta to 1.3b3 2021-02-02 12:25:51 -06:00
Riley Testut
84869af81a Fixes apps crashing for some Apple IDs
Dynamically chooses whether to use new or old WWDR certificate when signing apps.
2021-02-01 20:45:09 -06:00
Riley Testut
352fb1be73 Fixes apps crashing on iOS 13
AltSign’s updated apple.pem did not contain the Apple Root CA certificate, which caused apps to crash on iOS 13. Now both the Root CA and updated WWDR certificates are included with AltSign.
2021-02-01 16:59:18 -06:00
Riley Testut
7cfcab312c Fixes “App Group does not exist” error 2021-02-01 16:53:51 -06:00
Riley Testut
8889923111 [AltServer] Changes AltStore download URL for BETA builds 2021-02-01 13:52:23 -06:00
Riley Testut
49f5f96097 [AltServer] Updates AltPlugin to 1.3 2021-01-30 13:17:12 -06:00
Riley Testut
37aaf2cdb6 [AltPlugin] Updates version to 1.3 2021-01-30 13:04:22 -06:00
Riley Testut
e632ad0d84 [AltPlugin] Supports macOS 11.2 2021-01-30 13:01:37 -06:00
Riley Testut
fc49bc25f3 Fixes apps crashing due to outdated WWDR intermediate certificate
As of January 28, 2021, Apple began signing provisioning profiles with a new WWDR intermediate certificate. This broke all apps installed with AltStore after that date, but updating our local certificate to match Apple’s fixes the issue.
2021-01-29 15:29:10 -06:00
Riley Testut
95eeafa06b [AltServer] Fixes missing embedded certificate when using cached certificate 2020-12-17 14:45:03 -06:00
Riley Testut
a2f531a460 Adds Clip 1.1a1 to apps-alpha.json 2020-12-08 12:29:14 -06:00
Riley Testut
689d61d7d1 Fixes widget not updating after refreshing AltStore 2020-12-07 16:13:10 -06:00
Riley Testut
1f6edd778b Updates pods, fixes Sparkle on Apple Silicon Macs 2020-12-03 16:33:40 -06:00
Riley Testut
4abd4c2f7f Merge branch 'develop' of github.com:rileytestut/AltStore into develop 2020-12-03 16:25:32 -06:00
Riley Testut
3ad3fe5cce [AltServer] Works without Mail plug-in if SIP and AMFI are both disabled 2020-12-03 16:24:43 -06:00
Riley Testut
6c4931b0ba [AltXPC] Initial version
AltXPC uses the private com.apple.authkit.client.internal entitlement to retrieve anisette data.
2020-12-03 16:24:43 -06:00
Riley Testut
fc75ed730d [AltPlugin] Refactors anisette data retrieval into public method 2020-12-03 16:24:43 -06:00
Riley Testut
a767762f49 [AltServer] Updates AltPlugin to 1.2 2020-12-03 16:24:43 -06:00
Riley Testut
699632caa7 [AltPlugin] Updates version to 1.2 2020-12-03 16:24:42 -06:00
Riley Testut
e2ce2b3776 Updates apps.json and apps-alpha.json 2020-12-03 16:19:07 -06:00
Riley Testut
aedb3012a4 Prefers paid developer teams over free teams 2020-12-03 16:06:04 -06:00
Riley Testut
915eed3a69 [AltServer] Prefers paid developer teams over free teams 2020-12-03 16:06:04 -06:00
Riley Testut
f7a2c9f9f0 [AltServer] Supports multiple devices with same Apple ID
AltServer now caches certificates for each Apple ID used to install AltStore, and will re-use them for future installations rather than revoke + create new ones each time (if possible).
2020-12-03 16:06:04 -06:00
Riley Testut
f8f26bfb40 [AltServer] Fixes “RSTPlaceholderView.nib couldn’t be saved” error 2020-12-03 16:06:04 -06:00
Riley Testut
2b53e3483a Updates apps.json with Delta 1.3b1 2020-12-03 16:06:04 -06:00
Riley Testut
1ce9731465 [AltServer] Supports sideloading apps to Apple TV 2020-12-03 16:06:04 -06:00
Riley Testut
3b45ab7f62 Updates AltSign dependency 2020-12-03 16:06:03 -06:00
Riley Testut
1948894502 [AltPlugin] Supports macOS 11.1 2020-12-02 14:29:22 -06:00
Riley Testut
bb3b039672 [AltServer] Supports sideloading .ipa files directly to iOS devices 2020-11-11 17:40:28 -08:00
Riley Testut
66ef234f02 [AltServer] Fixes wireless devices not appearing in devices list 2020-11-11 16:38:45 -08:00
Riley Testut
a94a6b3f4b Resolves conflicts with master branch
# Conflicts:
#	AltStore/Model/DatabaseManager.swift
#	Shared/Extensions/Bundle+AltStore.swift
2020-11-11 13:05:16 -08:00
Riley Testut
0d06e028cd Updates app version to 1.4.2 2020-11-11 12:53:31 -08:00
Riley Testut
5d441fd23a Updates apps.json with AltStore 1.4.2 2020-11-11 12:52:36 -08:00
Riley Testut
21a731987e Updates apps.json with AltStore 1.4.2b1 2020-11-11 11:41:27 -08:00
Riley Testut
831b8cab4d Updates apps-alpha.json with AltStore 1.4.2a1 2020-11-05 10:59:08 -08:00
Riley Testut
80f00e8927 Merge branch 'develop' of https://github.com/rileytestut/AltStore into develop 2020-11-03 14:10:09 -08:00
Riley Testut
5afffb38aa Fixes JIT on iOS 14.2+
Updates code signature version to 0x20400 which allows apps to use JIT on iOS 14.2 and later.
2020-11-03 14:10:03 -08:00
osy86
67da21ccfc Hide private entitlements on >= iOS 13.5 (#415)
iOS 13.5 fixes the psychic paper hack so showing the private entitlement
warning popup is confusing to the user. Additionally iOS 14 checks the
entitlements on installation, so we should not copy the private entitlements
on iOS 14.

Depends on https://github.com/rileytestut/AltSign/pull/15

Co-authored-by: osy <osy86@users.noreply.github.com>
2020-11-03 14:02:19 -08:00
Riley Testut
f63e88d081 Updates apps-alpha.json 2020-10-26 15:59:19 -07:00
Riley Testut
aa1bc25ac8 Updates apps.json 2020-10-26 14:49:35 -07:00
Riley Testut
291c35c1b3 [AltServer] Updates app version to 1.4.1 2020-10-26 14:48:53 -07:00
Riley Testut
9412f4d24f [AltServer] Fixes keyboard shortcuts in NSAlert text fields
Partially reverts commit cace7576 and adds back the “un-used” app main menu in Main.storyboard, which broke keyboard shortcuts in alerts when removed.
2020-10-26 12:14:42 -07:00
Riley Testut
fb3946aad5 [AltServer] Supports installing apps with app extensions 2020-10-15 11:37:58 -07:00
Riley Testut
fe871e0a30 Fixes iOS 14.2 crash-on-launch due to invalid code signature 2020-10-15 11:11:00 -07:00
Riley Testut
f1349964d4 Removes “Install AltDaemon” option from Settings tab
AltDaemon can now be installed directly from the Dynastic repo via Cydia or Sileo.
2020-10-07 11:32:47 -07:00
Riley Testut
488e589943 Merge branch 'update_plugin' of https://github.com/rileytestut/AltStore into develop 2020-10-06 18:16:09 -07:00
Riley Testut
791cad5e9c [AltDaemon] Updates version to 1.0 2020-10-06 18:14:41 -07:00
Riley Testut
719cee9122 [AltServer] Adds PluginManager to update Mail plug-in to 1.1
AltPlugin 1.1 supports Big Sur on both Intel and Apple Silicon Macs.
2020-10-06 18:11:03 -07:00
Riley Testut
3c350e4671 [AltPlugin] Supports Big Sur on both Intel and Apple Silicon Macs 2020-10-06 18:09:47 -07:00
Riley Testut
2dc872392a Updates app version to 1.4 2020-10-05 14:57:22 -07:00
Riley Testut
1f8e16dce8 Limits adding sources to allowed identifiers in non-BETA builds 2020-10-05 14:48:48 -07:00
Riley Testut
00e8b7c80e Adds @Managed property wrapper
• Keeps strong reference to wrapped managed object’s context.
• Projected value simplifies accessing properties on context’s thread.
2020-10-05 14:23:19 -07:00
Riley Testut
c8b4ce8d38 Limits “Change App Icon” option to BETA builds for now 2020-10-05 13:59:44 -07:00
Riley Testut
5000b43533 [AltServer] Updates LaunchAtLogin dependency to 3.0.2 2020-10-05 13:58:13 -07:00
Riley Testut
9c04ad846a Cleans up AltStore Xcode scheme 2020-10-05 13:56:59 -07:00
Riley Testut
788a77b280 Merge branch 'develop' of https://github.com/rileytestut/AltStore into develop 2020-10-01 14:14:46 -07:00
Riley Testut
8b01a8d67c Migrates from Core Data model v8 to v9 2020-10-01 14:14:17 -07:00
Riley Testut
7a0e9d5835 [Shared] Fixes generic error messages when refreshing 2020-10-01 14:09:46 -07:00
Riley Testut
668ca66a04 Updates KeychainAccess pod 2020-10-01 14:09:46 -07:00
Riley Testut
546db3fa23 Adds ability to change sideloaded app icons 2020-10-01 14:09:45 -07:00
Riley Testut
12f33c355a Adds InstalledApp.needsResign
When true, app will be resigned + reinstalled next refresh rather than just refreshing provisioning profiles.
2020-10-01 11:52:26 -07:00
Riley Testut
707c2db508 [AltDaemon] Updates version to 0.4 2020-09-30 15:05:32 -07:00
Riley Testut
700046e693 [AltDaemon] Fixes XPC service lookup for Odyssey jailbreak 2020-09-30 15:04:44 -07:00
Theodore Dubois
b291f7b606 Add https:// to a source URL if you forget the scheme (#361) 2020-09-27 13:56:54 -07:00
Riley Testut
615d4fb35b Fixes serverNotFound error when refreshing apps due to background fetch
Extends time limit for discovering servers back to 3 seconds, and now accounts for wired and local servers.
2020-09-24 13:01:31 -07:00
Riley Testut
acc2ca7caf [AltServer] Fixes crash when reading some provisioning profiles from device 2020-09-24 13:00:25 -07:00
Riley Testut
cc1ff5b51d Fixes crash when reading some provisioning profiles from device 2020-09-23 11:47:47 -07:00
Riley Testut
724f1fc22d [AltDaemon] Updates version to 0.3 2020-09-22 15:26:20 -07:00
Riley Testut
af7fe484a2 [AltDaemon] Replaces local socket communication with XPC
Allows AltDaemon to be launched on demand + reject any connections not made from AltStore.
2020-09-22 15:12:33 -07:00
Riley Testut
361b84e3a1 Merge branch 'widget' into develop 2020-09-22 14:57:46 -07:00
Riley Testut
226795eafd [AltWidget] Fixes certain app icons not appearing 2020-09-22 10:53:18 -07:00
Riley Testut
de174db1bc [AltWidget] Fixes green tinting for sideloaded apps
Changes fallback tint color from AltStore-green to gray, which is more neutral.
2020-09-22 10:48:43 -07:00
Riley Testut
e54d309f39 [AltWidget] Preserves layout if app icon is missing 2020-09-22 10:45:13 -07:00
Riley Testut
50a5d56856 Migrates database + cached apps from app sandbox to app group 2020-09-16 12:09:12 -07:00
Riley Testut
aaaf6ed38d [AltWidget] Fixes crash when featured app is expired 2020-09-15 15:22:10 -07:00
Riley Testut
8045a23531 [AltWidget] Allows choosing featured app 2020-09-15 15:19:12 -07:00
Riley Testut
5abf7a5a11 [AltWidget] Initial version 2020-09-15 14:27:22 -07:00
Riley Testut
669c6f5bf4 Revises AltSign dependency graph
AltStoreCore now links AltSign-Static, so no need to also link against AltSign-Dynamic from other targets.
2020-09-14 15:57:15 -07:00
Riley Testut
9af9347e0c [AltDaemon] Removes Roxas pod 2020-09-14 15:33:46 -07:00
Riley Testut
25f06cccf1 Provides fallback error when connecting to AltServer
Fixes operation never finishing under certain circumstances.
2020-09-14 14:34:45 -07:00
Riley Testut
b0c36adedb Moves database + cached apps to app group so they can be accessed by extensions 2020-09-14 14:31:46 -07:00
Riley Testut
88c8d5f0f8 [AltStoreCore] Adds Date, FileManager, and UIColor extensions 2020-09-14 14:18:15 -07:00
Riley Testut
26fe9ca72b Updates apps.json and apps-alpha.json 2020-09-14 11:25:41 -07:00
Riley Testut
d1b897e212 [AltStoreCore] Sets APPLICATION_EXTENSION_API_ONLY to YES 2020-09-10 11:45:11 -07:00
Riley Testut
5df4169a1b Refines AppManager Combine pipeline 2020-09-10 11:27:44 -07:00
Riley Testut
80a39889ca Merge branch 'module_refactoring' into develop 2020-09-09 10:41:17 -07:00
Riley Testut
f202e985db Fixes incorrect Background Refresh cell style pre-iOS 14 2020-09-08 17:11:22 -07:00
Riley Testut
bfc2ea2c3a Adds button to add Refresh All Apps shortcut to Siri 2020-09-08 16:44:36 -07:00
Riley Testut
e506ceb25a Fixes opening deep links 2020-09-08 16:42:25 -07:00
Riley Testut
671a12b89c Extends additional intent handling time to 9 seconds 2020-09-08 16:40:07 -07:00
Riley Testut
8021ff8871 Replaces AltStore(Core) Roxas pod with framework
Fixes compilation errors when archiving app.
2020-09-08 13:44:08 -07:00
Riley Testut
fb9b1a5c7d Adds new Core Data model v8
No need for explicit migration/mapping model (yet) because we only added a transient property.
2020-09-08 13:28:59 -07:00
Riley Testut
e70c51e36c Updates UI when refreshing apps with Siri 2020-09-08 13:12:40 -07:00
Riley Testut
8d2e3f92b5 Fixes incorrect app subtitles in Browse tab 2020-09-08 13:00:01 -07:00
Riley Testut
0256079738 Supports refreshing apps with Siri on iOS 14 2020-09-08 12:29:44 -07:00
Theodore Dubois
47d85b7bab Fix file providers (#346)
* Make file providers work at all

NSExtensionFileProviderDocumentGroup must be a valid app group. This
updates it to use the new name of the app group including the team ID.

* Update AltStore/Operations/ResignAppOperation.swift
2020-09-04 16:29:01 -07:00
Theodore Dubois
cace7576e2 Make AltServer menu appear attached to the icon (#55)
* Make AltServer menu appear attached to the icon
* Update AltServer/AppDelegate.swift
2020-09-04 13:22:26 -07:00
Riley Testut
3d9417c071 Switches to UIScene-based lifecycle 2020-09-03 16:58:56 -07:00
Riley Testut
f1a39e1a1f [AltStoreCore] Refactors core AltStore logic into AltStoreCore framework
AltStoreCore will contain all shared AltStore code between AltStore and any app extensions. Initially, it includes all AltStore model logic.
2020-09-03 16:39:08 -07:00
Riley Testut
de925e7fea Replaces AltSign cocoapod with Swift package 2020-09-03 16:02:28 -07:00
Riley Testut
e75d184194 [AltKit] Replaces dedicated AltKit module with shared files across targets
Treating AltKit as a full module resulted in more complexity than necessary, when we really just wanted to share some files between different targets. Now we can share individual files across modules as-needed without AltKit overhead.
2020-09-03 15:35:29 -07:00
osy
3def65f501 Preserve device specific keys in Info.plist
Apple's Info.plist support platform and device specific keys to augment existing
keys. For example `UISupportedInterfaceOrientations~ipad` replaces
`UISupportedInterfaceOrientations` when running on an iPad.

By using Bundle.infoDictionary, Apple will pre-process the Info.plist and replace
any key with its device specific variant. Since AltStore does not support iPad,
this will strip out any iPad specific keys for the installing app.

We add an extension Bundle.completeInfoDictionary that will return the original
de-serialized dictionary including all the device specific keys.

See: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/AboutInformationPropertyListFiles.html#//apple_ref/doc/uid/TP40009254-SW9

# Conflicts:
#	AltKit/Extensions/Bundle+AltStore.swift
#	AltStore/Model/DatabaseManager.swift
2020-08-31 12:47:11 -07:00
Riley Testut
1d160aeeea Updates apps.json 2020-08-31 12:39:41 -07:00
Riley Testut
846b2c16d1 Merge pull request #260 from osy86/master
Preserve device specific keys in Info.plist
2020-08-31 12:37:11 -07:00
Riley Testut
a6c882e282 Migrates from Core Data model v6 to v7 2020-08-28 13:25:20 -07:00
Riley Testut
e03f881f07 Updates apps.json for 1.3.6 2020-08-28 12:58:54 -07:00
Riley Testut
89705469e1 Fixes missing CoreCrypto header errors for AltKit + AltServer 2020-08-28 12:43:47 -07:00
Riley Testut
3817f700b9 Merge branch 'accessibility_improvements' into develop
# Conflicts:
#	AltStore/Sources/SourcesViewController.swift
2020-08-28 12:39:05 -07:00
Riley Testut
70a475ff5f Adds altstore://source?url=[link] deep link to add sources 2020-08-28 12:15:15 -07:00
Riley Testut
4c3d33efdc Shows source errors in SourcesViewController 2020-08-27 16:39:03 -07:00
Riley Testut
b7564207b3 Improves error handling when fetching multiple sources
Fetching sources is no longer all or nothing. Now if a source cannot be fetched, it won’t prevent other sources from being updated.
2020-08-27 16:28:13 -07:00
Riley Testut
43395c4db5 Improves News tab accessibility
Combines News item name + subtitle into single accessibility group.
2020-08-27 15:27:38 -07:00
Riley Testut
012917f938 Improves My Apps tab accessibility 2020-08-27 15:25:52 -07:00
Riley Testut
f02fcad3a0 Announces errors when VoiceOver is enabled 2020-08-27 15:24:26 -07:00
Riley Testut
a3a4af182d Improves AppBannerView accessibility 2020-08-27 15:23:21 -07:00
Riley Testut
49d6e66745 Updates AltSign dependency 2020-08-27 14:40:33 -07:00
Riley Testut
ad33f6e1fb Updates patreon access token 2020-08-14 12:27:13 -07:00
Riley Testut
a0aaa680fd Updates apps.json for AltStore 1.4b4 2020-07-27 13:31:09 -07:00
Riley Testut
67166b4421 Fixes “unsupported code signature version” error on iOS 14 2020-07-24 13:08:58 -07:00
Riley Testut
c0f3bd8bb7 Fixes installing AltStore versions containing app extensions 2020-07-24 13:02:48 -07:00
Riley Testut
7262a6a1a0 [AltServer] Uses actual app bundle ID when installing app 2020-07-24 12:21:42 -07:00
Riley Testut
bcf02a4cfe Updates apps.json for 1.3.5 2020-07-15 14:28:54 -07:00
Riley Testut
cdcc5c941d Merge branch '1.3.5' into develop 2020-07-15 14:28:06 -07:00
Riley Testut
eea409dd03 Updates app version to 1.3.5 2020-07-15 11:58:46 -07:00
Riley Testut
dc1fbe8f63 Fixes Bonjour discovery on iOS 14
iOS 14 requires apps to specify which Bonjour services they support as well as a usage description in order to browse the local network.
2020-07-15 11:55:48 -07:00
Riley Testut
728a4b7123 Fixes Apple ID authentication on iOS 14 and macOS 11 2020-07-15 11:55:39 -07:00
Riley Testut
56cf77be42 [AltDaemon] Changes default build configuration to Release 2020-06-22 16:04:57 -07:00
Riley Testut
4e07831635 Adds 1.4 prerelease versions to apps(-alpha).json 2020-06-22 16:03:49 -07:00
Riley Testut
ad6bee7801 Adds Clip 1.0 to apps.json 2020-06-22 16:03:08 -07:00
Riley Testut
042ad856a9 [AltDaemon] Updates version to 0.2 2020-06-11 17:57:14 -07:00
Riley Testut
7cace2cacb [AltDaemon] Disables tweak injection to improve stability 2020-06-11 16:16:37 -07:00
Riley Testut
2b00ea5107 [AltDaemon] Fixes certificate becoming untrusted after refreshing 2020-06-11 16:15:45 -07:00
osy
43be34fd34 Preserve device specific keys in Info.plist
Apple's Info.plist support platform and device specific keys to augment existing
keys. For example `UISupportedInterfaceOrientations~ipad` replaces
`UISupportedInterfaceOrientations` when running on an iPad.

By using Bundle.infoDictionary, Apple will pre-process the Info.plist and replace
any key with its device specific variant. Since AltStore does not support iPad,
this will strip out any iPad specific keys for the installing app.

We add an extension Bundle.completeInfoDictionary that will return the original
de-serialized dictionary including all the device specific keys.

See: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/AboutInformationPropertyListFiles.html#//apple_ref/doc/uid/TP40009254-SW9
2020-06-10 15:05:31 -07:00
Riley Testut
4d9fad5d53 Merge branch 'jailbreak' into develop 2020-06-08 11:33:57 -07:00
Riley Testut
83622b68dc Merge branch 'backup_apps' into develop 2020-06-08 11:33:26 -07:00
Riley Testut
d6a33176e6 Adds “Install AltDaemon” option to settings (jailbreak only)
Exports AltDaemon that can be installed with Filza or another file/package manager.
2020-06-07 10:02:41 -07:00
Riley Testut
0d37ebd7fd Replaces cached AltStore every launch for DEBUG builds 2020-06-07 09:49:29 -07:00
Riley Testut
5884c78b8e [AltServer] Includes underlying installation error in error response 2020-06-07 09:48:53 -07:00
Riley Testut
bef3eb3964 [AltKit] Gracefully fails if no data is received over network connection 2020-06-05 15:43:05 -07:00
Riley Testut
0be1be5769 Improves error messages when there’s an underlying error 2020-06-05 15:32:10 -07:00
Riley Testut
db87d9ca7b [AltDaemon] Synchronizes AppManager operations
Installing and removing apps is now done on a serial dispatch queue, and installing/removing profiles uses file coordination.
2020-06-05 14:35:05 -07:00
Riley Testut
186ad09ab3 [AltKit] Includes underlying error in error response 2020-06-05 14:19:40 -07:00
Riley Testut
fafec6c904 [AltDaemon] Adds explicit autoreleasepool to main.swift 2020-06-05 14:13:09 -07:00
Riley Testut
496aca642c Supports installing/refreshings apps w/o computer on jailbroken devices
AltStore will use AltDaemon as a local AltServer if it’s installed and running. AltStore remains a regular sandboxed app, but AltDaemon has private entitlements necessary to perform AltServer operations without a computer.
2020-06-04 19:53:10 -07:00
Riley Testut
cb4656722a [AltDaemon] Initial version
AltDaemon allows AltStore to install and refresh apps without a computer on jailbroken devices. AltDaemon has the necessary entitlements to perform the same actions AltServer normally does over WiFi, and uses the same AltServer request logic to handle local requests.
2020-06-04 19:48:02 -07:00
Riley Testut
70f897699c [AltServer] Moves core ConnectionManager logic to AltKit
Refactors ConnectionManager to use arbitrary RequestHandlers and ConnectionHandlers. This allows the core AltServer request logic to be shared across different targets with different connection types.
2020-06-04 19:06:13 -07:00
Riley Testut
0b36214bb5 Updates apps.json for 1.3.4 2020-05-27 10:11:02 -07:00
Riley Testut
f9342acb30 [AltServer] Updates app version to 1.3.2 2020-05-27 10:10:32 -07:00
Noah Keck
f96de8d082 Merge pull request #195 from rileytestut/noah978-add-issue-templates
Create issue templates
2020-05-23 12:22:33 -05:00
Noah Keck
0bef37e91f Add logs to additional context 2020-05-23 12:21:30 -05:00
Noah Keck
a69d15f1b1 Create issue templates 2020-05-22 16:09:02 -05:00
Riley Testut
284f90ccd3 [AltServer] Improves error message when device is untrusted or locked during installation 2020-05-21 22:06:18 -07:00
Riley Testut
2411cca51f [AltServer] Suggests disabling “Offload Unused Apps” in error message
iOS 13.5 counts offloaded apps as active sideloaded apps (for some reason), so improve error messages to mention this.
2020-05-21 22:04:24 -07:00
Riley Testut
64f8983d29 Updates app version to 1.3.4 2020-05-19 20:10:55 -07:00
Riley Testut
540c9cc8af [AltServer] Updates app version to 1.3.1 2020-05-19 20:09:50 -07:00
Riley Testut
f564fc5190 [AltServer] Supports app groups when installing AltStore
Necessary for (de-)activation to work as expected in AltStore 1.3.4.
2020-05-19 18:30:53 -07:00
Riley Testut
fff128e1ce Adds option to explicitly back up installed apps 2020-05-19 11:47:43 -07:00
Riley Testut
da2370d9ac Fixes “invalid entitlements” when refreshing AltStore
Replaces “resigned” app group ID with “base” app group ID before resigning AltStore.
2020-05-18 16:00:08 -07:00
Riley Testut
17594a51d1 Limits new (de-)activation flow to 13.5 or later 2020-05-18 00:04:09 -07:00
Riley Testut
05dc365dff Adds altstore://install?url=[link] deep link to install remote .ipa’s 2020-05-17 23:47:26 -07:00
Riley Testut
39b60a07d9 Removes active app extension limits on 13.5 or later 2020-05-17 23:47:26 -07:00
Riley Testut
e0dea67380 [AltServer] Adds wired connection reading timeout 2020-05-17 23:47:26 -07:00
Riley Testut
8bd4e25b7f Uses real app icon for AltBackup icon 2020-05-17 23:47:26 -07:00
Riley Testut
b3f2474456 [AltBackup] UI reflects whether backup/restore/nothing is happening 2020-05-17 23:47:26 -07:00
Riley Testut
60abb9ee07 Adds option to manually restore backup for active apps that have one 2020-05-17 23:47:26 -07:00
Riley Testut
4a893d3c80 Adds option to export backups for inactive apps 2020-05-17 23:47:26 -07:00
Riley Testut
de34e077ce Activates apps by reinstalling then restoring backup on iOS 13.5+
To activate an inactive app that has been deleted from the phone, AltStore will reinstall the app, as well as restore any app data from when it was deactivated.
2020-05-17 23:47:26 -07:00
Riley Testut
2d87c396f1 Deactivates apps by backing up + deleting them on iOS 13.5+
Deactivating apps by removing their profiles no longer works on iOS 13.5. Instead, AltStore will now back up the app by temporarily replacing it with AltBackup, then remove the app from the phone.
2020-05-17 23:47:26 -07:00
Riley Testut
19bf19350e Supports removing inactive apps from My Apps 2020-05-17 23:47:26 -07:00
Riley Testut
d8f1dcb032 Adds RemoveAppBackupOperation to remove backed up app data 2020-05-17 23:47:26 -07:00
Riley Testut
753fb740fe Adds RemoveAppOperation for removing inactive apps 2020-05-17 23:47:26 -07:00
Riley Testut
1582d1b143 Fixes updating App IDs with no app groups 2020-05-17 23:47:26 -07:00
Riley Testut
c403d7c788 Adds BackupAppOperation to backup and restore app data 2020-05-17 23:47:26 -07:00
Riley Testut
7c9d8bd90d Adds option to not cache downloaded app during installation 2020-05-17 23:47:26 -07:00
Riley Testut
7cbe921020 [AltBackup] Derives backup location from original bundle ID, not resigned one
Allows the backup to be used even if the app is later installed with a different developer team.
2020-05-17 23:47:26 -07:00
Riley Testut
8354794c24 Embeds original bundle ID under ALTBundleIdentifier Info.plist key 2020-05-17 23:47:26 -07:00
Riley Testut
b25a0e46cb [AltBackup] No longer assumes AltStore app group is first in ALTAppGroups 2020-05-17 23:47:26 -07:00
Riley Testut
1b8b043290 Supports resigning apps with multiple app groups 2020-05-17 23:47:24 -07:00
Riley Testut
a4d9188bc7 Fixes missing error descriptions when using NSError.withLocalizedFailure() 2020-05-15 14:54:46 -07:00
Riley Testut
47cf59a1ad Adds initial AltBackup app
When deactivating an app, AltStore will first install AltBackup in its place. This allows AltBackup to access the (soon to be) inactive app’s sandbox, and backup all files to a shared app group with AltStore. Later when activating, AltStore will again install AltBackup and use it to restore files before installing the actual app again.
2020-05-15 14:54:46 -07:00
Riley Testut
b9b2afa200 Replaces ConnectionError.errorDescription with .failureReason
Improves error messages where ConnectionError was the underlying failure, but not the main error.
2020-05-15 14:54:46 -07:00
Riley Testut
ea6861b1eb [AltServer] Uses empty strings in place of nil error messages 2020-05-15 14:54:46 -07:00
Riley Testut
a0b5d6d8ae Adds additional checks before considering apps deleted 2020-05-15 14:54:46 -07:00
Riley Testut
484742885f Supports custom entitlements when fetching provisioning profiles 2020-05-15 14:54:43 -07:00
Riley Testut
2fc19f6741 Fixes RefreshGroup strong reference cycle 2020-05-14 16:31:23 -07:00
Riley Testut
f5fc64be44 [AltServer] Supports “remove app” requests
Improves support for removing apps
2020-05-14 16:31:23 -07:00
Riley Testut
fe62d6f80f [AltServer] Renames NSError+ALTServerError category methods to avoid runtime conflicts 2020-05-11 14:33:53 -07:00
Riley Testut
c5a97f6c25 Updates apps.json and apps-alpha.json 2020-05-09 12:53:12 -07:00
Riley Testut
2ae1ddb2d5 Updates apps.json 2020-05-08 18:27:54 -07:00
Riley Testut
29dda98736 Fixes updating DolphiniOS due to mismatched bundle IDs
Manually sets Dolphin’s CFBundleIdentifier to match the source bundle ID to prevent breaking updates for existing users.
2020-05-08 11:45:23 -07:00
Riley Testut
76008022e7 Redownloads missing cached apps when refreshing or updating 2020-05-08 11:43:34 -07:00
Riley Testut
b4299c71fb Verifies app’s bundle ID matches source’s before installing
Prevents apps with incorrect bundle IDs from being installed and then deleted from disk due to AltStore thinking the apps have been removed.
2020-05-07 13:13:05 -07:00
Riley Testut
25477422a9 Adds print statement when deleting cached apps 2020-05-07 13:10:01 -07:00
Riley Testut
cba98ddf57 Improves error when app being refreshed has been deleted 2020-05-07 13:08:52 -07:00
Riley Testut
0f9df5af8a Treats App ID bundle IDs as case-insensitive
Apple’s servers return an error when registering a bundle ID with different capitalization than an existing one, so we now perform case-insensitive comparisons when determining if we need to register an App ID.
2020-05-07 12:45:09 -07:00
Riley Testut
41b57b7f5e Updates app version to 1.3.2 2020-05-03 14:48:23 -07:00
Riley Testut
bab1fcb7bc Asks user for permission before installing apps with private entitlements 2020-05-02 22:06:57 -07:00
Riley Testut
4f6e194b35 Merge pull request #178 from rileytestut/develop
AltStore 1.3 & 1.3.1
2020-05-01 14:47:44 -07:00
Riley Testut
6cdbe8e9ff Updates app version to 1.3.1 2020-05-01 14:44:15 -07:00
Riley Testut
7b4acc56fc Preserves private entitlements for Psychic Paper usage
Psychic Paper allows apps to use private entitlements without jailbreaking. AltStore now preserves private entitlements and includes them when resigning to allow apps to take advantage of this. For more info, see https://github.com/Siguza/psychicpaper
2020-05-01 10:27:22 -07:00
Riley Testut
fb03cb34aa Merge branch 'feature/active_inactive_apps' into develop 2020-05-01 08:53:21 -07:00
Riley Testut
8ea9c30d7e Updates apps.json & apps-alpha.json 2020-05-01 08:52:51 -07:00
Riley Testut
4bdeb53f9f Updates apps.json for AltStore 1.3 2020-04-10 13:53:30 -07:00
Riley Testut
f1199abd4a Adds AltStore (Stable) to Alpha source 2020-04-10 13:52:30 -07:00
Riley Testut
c3257bfbb8 Updates app version to 1.3 2020-04-09 12:26:41 -07:00
Riley Testut
e0d2bab21e [AltServer] Updates app version to 1.3 2020-04-09 12:25:48 -07:00
Riley Testut
7b7613c331 Adds Alpha source JSON 2020-04-09 12:22:57 -07:00
Riley Testut
274a4aea44 Updates apps.json for AltStore 1.3b3 2020-04-09 12:21:23 -07:00
Riley Testut
98146ca8f3 Uses separate App Center tokens for different build types 2020-04-09 12:18:17 -07:00
Riley Testut
ba842ff718 Merge pull request #100 from rileytestut/develop
AltStore 1.2
2020-02-12 12:03:54 -08:00
Riley Testut
88929a1e98 Merge branch 'master' of https://github.com/rileytestut/AltStore 2020-02-12 12:01:23 -08:00
Riley Testut
ce9c222402 Update main.yml 2020-01-13 10:14:05 -08:00
Riley Testut
f1e598b0b6 Adds GitHub Action to post to Discord 2020-01-08 13:05:22 -08:00
Riley Testut
5c95f7727a Create FUNDING.yml 2019-10-18 02:00:32 -07:00
Riley Testut
fcbfe7d4df Merge pull request #47 from coliff/patch-1
Fix typo
2019-10-14 10:07:27 -07:00
Christian Oliff
a5950617f1 Fix typo 2019-10-12 14:50:43 +09:00
Riley Testut
6f7d230895 Merge pull request #43 from ccheever/master
Add missing step to build instructions for AltServer in README
2019-10-09 16:51:28 -07:00
Charlie Cheever
e7ef101f99 Add missing step to build instructions for AltServer in README
You need to run `carthage update` to build AltServer.
Adding that information to the README.
2019-10-08 01:28:16 -07:00
902 changed files with 20838 additions and 7243 deletions

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.rileytestut.AltStore</string>
</array>
</dict>
</plist>

121
AltBackup/AppDelegate.swift Normal file
View File

@@ -0,0 +1,121 @@
//
// AppDelegate.swift
// AltBackup
//
// Created by Riley Testut on 5/11/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
extension AppDelegate
{
static let startBackupNotification = Notification.Name("io.altstore.StartBackup")
static let startRestoreNotification = Notification.Name("io.altstore.StartRestore")
static let operationDidFinishNotification = Notification.Name("io.altstore.BackupOperationFinished")
static let operationResultKey = "result"
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
private var currentBackupReturnURL: URL?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
// Override point for customization after application launch.
NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.operationDidFinish(_:)), name: AppDelegate.operationDidFinishNotification, object: nil)
let viewController = ViewController()
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window?.rootViewController = viewController
self.window?.makeKeyAndVisible()
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
{
return self.open(url)
}
}
private extension AppDelegate
{
func open(_ url: URL) -> Bool
{
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
guard let command = components.host?.lowercased() else { return false }
switch command
{
case "backup":
guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false }
self.currentBackupReturnURL = returnURL
NotificationCenter.default.post(name: AppDelegate.startBackupNotification, object: nil)
return true
case "restore":
guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false }
self.currentBackupReturnURL = returnURL
NotificationCenter.default.post(name: AppDelegate.startRestoreNotification, object: nil)
return true
default: return false
}
}
@objc func operationDidFinish(_ notification: Notification)
{
defer { self.currentBackupReturnURL = nil }
guard
let returnURL = self.currentBackupReturnURL,
let result = notification.userInfo?[AppDelegate.operationResultKey] as? Result<Void, Error>
else { return }
guard var components = URLComponents(url: returnURL, resolvingAgainstBaseURL: false) else { return }
switch result
{
case .success:
components.path = "/success"
case .failure(let error as NSError):
components.path = "/failure"
components.queryItems = ["errorDomain": error.domain,
"errorCode": String(error.code),
"errorDescription": error.localizedDescription].map { URLQueryItem(name: $0, value: $1) }
}
guard let responseURL = components.url else { return }
DispatchQueue.main.async {
UIApplication.shared.open(responseURL, options: [:]) { (success) in
print("Sent response to app with success:", success)
}
}
}
}

View File

@@ -0,0 +1,98 @@
{
"images" : [
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.518",
"green" : "0.502",
"red" : "0.004"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.404",
"green" : "0.322",
"red" : "0.008"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "0.750",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFF"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,293 @@
//
// BackupController.swift
// AltBackup
//
// Created by Riley Testut on 5/12/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
extension ErrorUserInfoKey
{
static let sourceFile: String = "alt_sourceFile"
static let sourceFileLine: String = "alt_sourceFileLine"
}
extension Error
{
var sourceDescription: String? {
guard let sourceFile = (self as NSError).userInfo[ErrorUserInfoKey.sourceFile] as? String, let sourceFileLine = (self as NSError).userInfo[ErrorUserInfoKey.sourceFileLine] else {
return nil
}
return "(\((sourceFile as NSString).lastPathComponent), Line \(sourceFileLine))"
}
}
struct BackupError: ALTLocalizedError
{
enum Code
{
case invalidBundleID
case appGroupNotFound(String?)
case randomError // Used for debugging.
}
let code: Code
let sourceFile: String
let sourceFileLine: Int
var errorFailure: String?
var failureReason: String? {
switch self.code
{
case .invalidBundleID: return NSLocalizedString("The bundle identifier is invalid.", comment: "")
case .appGroupNotFound(let appGroup):
if let appGroup = appGroup
{
return String(format: NSLocalizedString("The app group “%@” could not be found.", comment: ""), appGroup)
}
else
{
return NSLocalizedString("The AltStore app group could not be found.", comment: "")
}
case .randomError: return NSLocalizedString("A random error occured.", comment: "")
}
}
var errorUserInfo: [String : Any] {
let userInfo: [String: Any?] = [NSLocalizedDescriptionKey: self.errorDescription,
NSLocalizedFailureReasonErrorKey: self.failureReason,
NSLocalizedFailureErrorKey: self.errorFailure,
ErrorUserInfoKey.sourceFile: self.sourceFile,
ErrorUserInfoKey.sourceFileLine: self.sourceFileLine]
return userInfo.compactMapValues { $0 }
}
init(_ code: Code, description: String? = nil, file: String = #file, line: Int = #line)
{
self.code = code
self.errorFailure = description
self.sourceFile = file
self.sourceFileLine = line
}
}
class BackupController: NSObject
{
private let fileCoordinator = NSFileCoordinator(filePresenter: nil)
private let operationQueue = OperationQueue()
override init()
{
self.operationQueue.name = "AltBackup-BackupQueue"
}
func performBackup(completionHandler: @escaping (Result<Void, Error>) -> Void)
{
do
{
guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else {
throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to create backup directory.", comment: ""))
}
guard
let altstoreAppGroup = Bundle.main.altstoreAppGroup,
let sharedDirectoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: altstoreAppGroup)
else { throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to create backup directory.", comment: "")) }
let backupsDirectory = sharedDirectoryURL.appendingPathComponent("Backups")
// Use temporary directory to prevent messing up successful backup with incomplete one.
let temporaryAppBackupDirectory = backupsDirectory.appendingPathComponent("Temp", isDirectory: true).appendingPathComponent(UUID().uuidString)
let appBackupDirectory = backupsDirectory.appendingPathComponent(bundleIdentifier)
let writingIntent = NSFileAccessIntent.writingIntent(with: temporaryAppBackupDirectory, options: [])
let replacementIntent = NSFileAccessIntent.writingIntent(with: appBackupDirectory, options: [.forReplacing])
self.fileCoordinator.coordinate(with: [writingIntent, replacementIntent], queue: self.operationQueue) { (error) in
do
{
if let error = error
{
throw error
}
do
{
let mainGroupBackupDirectory = temporaryAppBackupDirectory.appendingPathComponent("App")
try FileManager.default.createDirectory(at: mainGroupBackupDirectory, withIntermediateDirectories: true, attributes: nil)
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let backupDocumentsDirectory = mainGroupBackupDirectory.appendingPathComponent(documentsDirectory.lastPathComponent)
if FileManager.default.fileExists(atPath: backupDocumentsDirectory.path)
{
try FileManager.default.removeItem(at: backupDocumentsDirectory)
}
if FileManager.default.fileExists(atPath: documentsDirectory.path)
{
try FileManager.default.copyItem(at: documentsDirectory, to: backupDocumentsDirectory)
}
print("Copied Documents directory from \(documentsDirectory) to \(backupDocumentsDirectory)")
let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]
let backupLibraryDirectory = mainGroupBackupDirectory.appendingPathComponent(libraryDirectory.lastPathComponent)
if FileManager.default.fileExists(atPath: backupLibraryDirectory.path)
{
try FileManager.default.removeItem(at: backupLibraryDirectory)
}
if FileManager.default.fileExists(atPath: libraryDirectory.path)
{
try FileManager.default.copyItem(at: libraryDirectory, to: backupLibraryDirectory)
}
print("Copied Library directory from \(libraryDirectory) to \(backupLibraryDirectory)")
}
for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup
{
guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to create app group backup directory.", comment: ""))
}
let backupAppGroupURL = temporaryAppBackupDirectory.appendingPathComponent(appGroup)
// There are several system hidden files that we don't have permission to read, so we just skip all hidden files in app group directories.
try self.copyDirectoryContents(at: appGroupURL, to: backupAppGroupURL, options: [.skipsHiddenFiles])
}
// Replace previous backup with new backup.
_ = try FileManager.default.replaceItemAt(appBackupDirectory, withItemAt: temporaryAppBackupDirectory)
print("Replaced previous backup with new backup:", temporaryAppBackupDirectory)
completionHandler(.success(()))
}
catch
{
do { try FileManager.default.removeItem(at: temporaryAppBackupDirectory) }
catch { print("Failed to remove temporary directory.", error) }
completionHandler(.failure(error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
func restoreBackup(completionHandler: @escaping (Result<Void, Error>) -> Void)
{
do
{
guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else {
throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to access backup.", comment: ""))
}
guard
let altstoreAppGroup = Bundle.main.altstoreAppGroup,
let sharedDirectoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: altstoreAppGroup)
else { throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to access backup.", comment: "")) }
let backupsDirectory = sharedDirectoryURL.appendingPathComponent("Backups")
let appBackupDirectory = backupsDirectory.appendingPathComponent(bundleIdentifier)
let readingIntent = NSFileAccessIntent.readingIntent(with: appBackupDirectory, options: [])
self.fileCoordinator.coordinate(with: [readingIntent], queue: self.operationQueue) { (error) in
do
{
if let error = error
{
throw error
}
let mainGroupBackupDirectory = appBackupDirectory.appendingPathComponent("App")
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let backupDocumentsDirectory = mainGroupBackupDirectory.appendingPathComponent(documentsDirectory.lastPathComponent)
let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]
let backupLibraryDirectory = mainGroupBackupDirectory.appendingPathComponent(libraryDirectory.lastPathComponent)
try self.copyDirectoryContents(at: backupDocumentsDirectory, to: documentsDirectory)
try self.copyDirectoryContents(at: backupLibraryDirectory, to: libraryDirectory)
for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup
{
guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to read app group backup.", comment: ""))
}
let backupAppGroupURL = appBackupDirectory.appendingPathComponent(appGroup)
try self.copyDirectoryContents(at: backupAppGroupURL, to: appGroupURL)
}
completionHandler(.success(()))
}
catch
{
completionHandler(.failure(error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
private extension BackupController
{
func copyDirectoryContents(at sourceDirectoryURL: URL, to destinationDirectoryURL: URL, options: FileManager.DirectoryEnumerationOptions = []) throws
{
guard FileManager.default.fileExists(atPath: sourceDirectoryURL.path) else { return }
if !FileManager.default.fileExists(atPath: destinationDirectoryURL.path)
{
try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil)
}
for fileURL in try FileManager.default.contentsOfDirectory(at: sourceDirectoryURL, includingPropertiesForKeys: [.isDirectoryKey], options: options)
{
let isDirectory = try fileURL.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false
let destinationURL = destinationDirectoryURL.appendingPathComponent(fileURL.lastPathComponent)
if FileManager.default.fileExists(atPath: destinationURL.path)
{
do {
try FileManager.default.removeItem(at: destinationURL)
}
catch CocoaError.fileWriteNoPermission where isDirectory {
try self.copyDirectoryContents(at: fileURL, to: destinationURL, options: options)
continue
}
catch {
print(error)
throw error
}
}
do {
try FileManager.default.copyItem(at: fileURL, to: destinationURL)
print("Copied item from \(fileURL) to \(destinationURL)")
}
catch let error where fileURL.lastPathComponent == "Inbox" && fileURL.deletingLastPathComponent().lastPathComponent == "Documents" {
// Ignore errors for /Documents/Inbox
print("Failed to copy Inbox directory:", error)
}
catch {
print(error)
throw error
}
}
}
}

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" name="Background"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<namedColor name="Background">
<color red="0.0040000001899898052" green="0.50199997425079346" blue="0.51800000667572021" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

66
AltBackup/Info.plist Normal file
View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ALTAppGroups</key>
<array>
<string>group.com.rileytestut.AltStore</string>
</array>
<key>ALTBundleIdentifier</key>
<string>com.rileytestut.AltBackup</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>AltBackup General</string>
<key>CFBundleURLSchemes</key>
<array>
<string>altbackup</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleLightContent</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportsDocumentBrowser</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,15 @@
//
// UIColor+AltBackup.swift
// AltBackup
//
// Created by Riley Testut on 5/11/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
extension UIColor
{
static let altstoreBackground = UIColor(named: "Background")!
static let altstoreText = UIColor(named: "Text")!
}

View File

@@ -0,0 +1,206 @@
//
// ViewController.swift
// AltBackup
//
// Created by Riley Testut on 5/11/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
extension Bundle
{
var appName: String? {
let appName =
Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ??
Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as? String
return appName
}
}
extension ViewController
{
enum BackupOperation
{
case backup
case restore
}
}
class ViewController: UIViewController
{
private let backupController = BackupController()
private var currentOperation: BackupOperation? {
didSet {
DispatchQueue.main.async {
self.update()
}
}
}
private var textLabel: UILabel!
private var detailTextLabel: UILabel!
private var activityIndicatorView: UIActivityIndicatorView!
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
{
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.backup), name: AppDelegate.startBackupNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.restore), name: AppDelegate.startRestoreNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
}
required init?(coder: NSCoder) {
fatalError()
}
override func viewDidLoad()
{
super.viewDidLoad()
self.view.backgroundColor = .altstoreBackground
self.textLabel = UILabel(frame: .zero)
self.textLabel.font = UIFont.preferredFont(forTextStyle: .title2)
self.textLabel.textColor = .altstoreText
self.textLabel.textAlignment = .center
self.textLabel.numberOfLines = 0
self.detailTextLabel = UILabel(frame: .zero)
self.detailTextLabel.font = UIFont.preferredFont(forTextStyle: .body)
self.detailTextLabel.textColor = .altstoreText
self.detailTextLabel.textAlignment = .center
self.detailTextLabel.numberOfLines = 0
self.activityIndicatorView = UIActivityIndicatorView(style: .whiteLarge)
self.activityIndicatorView.color = .altstoreText
self.activityIndicatorView.startAnimating()
#if DEBUG
let button1 = UIButton(type: .system)
button1.setTitle("Backup", for: .normal)
button1.setTitleColor(.white, for: .normal)
button1.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
button1.addTarget(self, action: #selector(ViewController.backup), for: .primaryActionTriggered)
let button2 = UIButton(type: .system)
button2.setTitle("Restore", for: .normal)
button2.setTitleColor(.white, for: .normal)
button2.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
button2.addTarget(self, action: #selector(ViewController.restore), for: .primaryActionTriggered)
let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!, button1, button2]
#else
let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!]
#endif
let stackView = UIStackView(arrangedSubviews: arrangedSubviews)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.spacing = 22
stackView.axis = .vertical
stackView.alignment = .center
self.view.addSubview(stackView)
NSLayoutConstraint.activate([stackView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
stackView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
stackView.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: self.view.safeAreaLayoutGuide.leadingAnchor, multiplier: 1.0),
self.view.safeAreaLayoutGuide.trailingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: stackView.trailingAnchor, multiplier: 1.0)])
self.update()
}
}
private extension ViewController
{
@objc func backup()
{
self.currentOperation = .backup
self.backupController.performBackup { (result) in
let appName = Bundle.main.appName ?? NSLocalizedString("App", comment: "")
let title = String(format: NSLocalizedString("%@ could not be backed up.", comment: ""), appName)
self.process(result, errorTitle: title)
}
}
@objc func restore()
{
self.currentOperation = .restore
self.backupController.restoreBackup { (result) in
let appName = Bundle.main.appName ?? NSLocalizedString("App", comment: "")
let title = String(format: NSLocalizedString("%@ could not be restored.", comment: ""), appName)
self.process(result, errorTitle: title)
}
}
func update()
{
switch self.currentOperation
{
case .backup:
self.textLabel.text = NSLocalizedString("Backing up app data…", comment: "")
self.detailTextLabel.isHidden = true
self.activityIndicatorView.startAnimating()
case .restore:
self.textLabel.text = NSLocalizedString("Restoring app data…", comment: "")
self.detailTextLabel.isHidden = true
self.activityIndicatorView.startAnimating()
case .none:
self.textLabel.text = String(format: NSLocalizedString("%@ is inactive.", comment: ""),
Bundle.main.appName ?? NSLocalizedString("App", comment: ""))
self.detailTextLabel.text = String(format: NSLocalizedString("Refresh %@ in AltStore to continue using it.", comment: ""),
Bundle.main.appName ?? NSLocalizedString("this app", comment: ""))
self.detailTextLabel.isHidden = false
self.activityIndicatorView.stopAnimating()
}
}
}
private extension ViewController
{
func process(_ result: Result<Void, Error>, errorTitle: String)
{
DispatchQueue.main.async {
switch result
{
case .success: break
case .failure(let error as NSError):
let message: String
if let sourceDescription = error.sourceDescription
{
message = error.localizedDescription + "\n\n" + sourceDescription
}
else
{
message = error.localizedDescription
}
let alertController = UIAlertController(title: errorTitle, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
self.present(alertController, animated: true, completion: nil)
}
NotificationCenter.default.post(name: AppDelegate.operationDidFinishNotification, object: nil, userInfo: [AppDelegate.operationResultKey: result])
}
}
@objc func didEnterBackground(_ notification: Notification)
{
// Reset UI once we've left app (but not before).
self.currentOperation = nil
}
}

View File

@@ -0,0 +1,59 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import <Foundation/Foundation.h>
// Shared
#import "ALTConstants.h"
#import "ALTConnection.h"
#import "NSError+ALTServerError.h"
#import "CFNotificationName+AltStore.h"
// libproc
int proc_pidpath(int pid, void * buffer, uint32_t buffersize);
// Security.framework
CF_ENUM(uint32_t) {
kSecCSInternalInformation = 1 << 0,
kSecCSSigningInformation = 1 << 1,
kSecCSRequirementInformation = 1 << 2,
kSecCSDynamicInformation = 1 << 3,
kSecCSContentInformation = 1 << 4,
kSecCSSkipResourceDirectory = 1 << 5,
kSecCSCalculateCMSDigest = 1 << 6,
};
OSStatus SecStaticCodeCreateWithPath(CFURLRef path, uint32_t flags, void ** __nonnull CF_RETURNS_RETAINED staticCode);
OSStatus SecCodeCopySigningInformation(void *code, uint32_t flags, CFDictionaryRef * __nonnull CF_RETURNS_RETAINED information);
NS_ASSUME_NONNULL_BEGIN
@interface AKDevice : NSObject
@property (class, readonly) AKDevice *currentDevice;
@property (strong, readonly) NSString *serialNumber;
@property (strong, readonly) NSString *uniqueDeviceIdentifier;
@property (strong, readonly) NSString *serverFriendlyDescription;
@end
@interface AKAppleIDSession : NSObject
- (instancetype)initWithIdentifier:(NSString *)identifier;
- (NSDictionary<NSString *, NSString *> *)appleIDHeadersForRequest:(NSURLRequest *)request;
@end
@interface LSApplicationWorkspace : NSObject
@property (class, readonly) LSApplicationWorkspace *defaultWorkspace;
- (BOOL)installApplication:(NSURL *)fileURL withOptions:(nullable NSDictionary<NSString *, id> *)options error:(NSError *_Nullable *)error;
- (BOOL)uninstallApplication:(NSString *)bundleIdentifier withOptions:(nullable NSDictionary *)options;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>application-identifier</key>
<string>6XVY5G3U44.com.rileytestut.AltDaemon</string>
<key>get-task-allow</key>
<true/>
<key>platform-application</key>
<true/>
<key>com.apple.authkit.client.private</key>
<true/>
<key>com.apple.private.mobileinstall.allowedSPI</key>
<array>
<string>Install</string>
<string>Uninstall</string>
<string>InstallForLaunchServices</string>
<string>UninstallForLaunchServices</string>
<string>InstallLocalProvisioned</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,65 @@
//
// AnisetteDataManager.swift
// AltDaemon
//
// Created by Riley Testut on 6/1/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltSign
private extension UserDefaults
{
@objc var localUserID: String? {
get { return self.string(forKey: #keyPath(UserDefaults.localUserID)) }
set { self.set(newValue, forKey: #keyPath(UserDefaults.localUserID)) }
}
}
struct AnisetteDataManager
{
static let shared = AnisetteDataManager()
private let dateFormatter = ISO8601DateFormatter()
private init()
{
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW);
}
func requestAnisetteData() throws -> ALTAnisetteData
{
var request = URLRequest(url: URL(string: "https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA")!)
request.httpMethod = "POST"
let akAppleIDSession = unsafeBitCast(NSClassFromString("AKAppleIDSession")!, to: AKAppleIDSession.Type.self)
let akDevice = unsafeBitCast(NSClassFromString("AKDevice")!, to: AKDevice.Type.self)
let session = akAppleIDSession.init(identifier: "com.apple.gs.xcode.auth")
let headers = session.appleIDHeaders(for: request)
let device = akDevice.current
let date = self.dateFormatter.date(from: headers["X-Apple-I-Client-Time"] ?? "") ?? Date()
var localUserID = UserDefaults.standard.localUserID
if localUserID == nil
{
localUserID = UUID().uuidString
UserDefaults.standard.localUserID = localUserID
}
let anisetteData = ALTAnisetteData(machineID: headers["X-Apple-I-MD-M"] ?? "",
oneTimePassword: headers["X-Apple-I-MD"] ?? "",
localUserID: headers["X-Apple-I-MD-LU"] ?? localUserID ?? "",
routingInfo: UInt64(headers["X-Apple-I-MD-RINFO"] ?? "") ?? 0,
deviceUniqueIdentifier: device.uniqueDeviceIdentifier,
deviceSerialNumber: device.serialNumber,
deviceDescription: "<MacBookPro15,1> <Mac OS X;10.15.2;19C57> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>",
date: date,
locale: .current,
timeZone: .current)
return anisetteData
}
}

138
AltDaemon/AppManager.swift Normal file
View File

@@ -0,0 +1,138 @@
//
// AppManager.swift
// AltDaemon
//
// Created by Riley Testut on 6/1/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltSign
private extension URL
{
static let profilesDirectoryURL = URL(fileURLWithPath: "/var/MobileDevice/ProvisioningProfiles", isDirectory: true)
}
private extension CFNotificationName
{
static let updatedProvisioningProfiles = CFNotificationName("MISProvisioningProfileRemoved" as CFString)
}
struct AppManager
{
static let shared = AppManager()
private let appQueue = DispatchQueue(label: "com.rileytestut.AltDaemon.appQueue", qos: .userInitiated)
private let profilesQueue = OperationQueue()
private let fileCoordinator = NSFileCoordinator()
private init()
{
self.profilesQueue.name = "com.rileytestut.AltDaemon.profilesQueue"
self.profilesQueue.qualityOfService = .userInitiated
}
func installApp(at fileURL: URL, bundleIdentifier: String, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
self.appQueue.async {
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
let options = ["CFBundleIdentifier": bundleIdentifier, "AllowInstallLocalProvisioned": NSNumber(value: true)] as [String : Any]
let result = Result { try lsApplicationWorkspace.default.installApplication(fileURL, withOptions: options) }
completionHandler(result)
}
}
func removeApp(forBundleIdentifier bundleIdentifier: String, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
self.appQueue.async {
let lsApplicationWorkspace = unsafeBitCast(NSClassFromString("LSApplicationWorkspace")!, to: LSApplicationWorkspace.Type.self)
lsApplicationWorkspace.default.uninstallApplication(bundleIdentifier, withOptions: nil)
completionHandler(.success(()))
}
}
func install(_ profiles: Set<ALTProvisioningProfile>, activeProfiles: Set<String>?, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in
do
{
if let error = error
{
throw error
}
let installingBundleIDs = Set(profiles.map(\.bundleIdentifier))
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
// Remove all inactive profiles (if active profiles are provided), and the previous profiles.
for fileURL in profileURLs
{
// Use memory mapping to reduce peak memory usage and stay within limit.
guard let profile = try? ALTProvisioningProfile(url: fileURL, options: [.mappedIfSafe]) else { continue }
if installingBundleIDs.contains(profile.bundleIdentifier) || (activeProfiles?.contains(profile.bundleIdentifier) == false && profile.isFreeProvisioningProfile)
{
try FileManager.default.removeItem(at: fileURL)
}
else
{
print("Ignoring:", profile.bundleIdentifier, profile.uuid)
}
}
for profile in profiles
{
let destinationURL = URL.profilesDirectoryURL.appendingPathComponent(profile.uuid.uuidString.lowercased())
try profile.data.write(to: destinationURL, options: .atomic)
}
completionHandler(.success(()))
}
catch
{
completionHandler(.failure(error))
}
// Notify system to prevent accidentally untrusting developer certificate.
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .updatedProvisioningProfiles, nil, nil, true)
}
}
func removeProvisioningProfiles(forBundleIdentifiers bundleIdentifiers: Set<String>, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let intent = NSFileAccessIntent.writingIntent(with: .profilesDirectoryURL, options: [])
self.fileCoordinator.coordinate(with: [intent], queue: self.profilesQueue) { (error) in
do
{
let profileURLs = try FileManager.default.contentsOfDirectory(at: intent.url, includingPropertiesForKeys: nil, options: [])
for fileURL in profileURLs
{
guard let profile = ALTProvisioningProfile(url: fileURL) else { continue }
if bundleIdentifiers.contains(profile.bundleIdentifier)
{
try FileManager.default.removeItem(at: fileURL)
}
}
completionHandler(.success(()))
}
catch
{
completionHandler(.failure(error))
}
// Notify system to prevent accidentally untrusting developer certificate.
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), .updatedProvisioningProfiles, nil, nil, true)
}
}
}

View File

@@ -0,0 +1,123 @@
//
// DaemonRequestHandler.swift
// AltDaemon
//
// Created by Riley Testut on 6/1/20.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
typealias DaemonConnectionManager = ConnectionManager<DaemonRequestHandler>
private let connectionManager = ConnectionManager(requestHandler: DaemonRequestHandler(),
connectionHandlers: [XPCConnectionHandler()])
extension DaemonConnectionManager
{
static var shared: ConnectionManager {
return connectionManager
}
}
struct DaemonRequestHandler: RequestHandler
{
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void)
{
do
{
let anisetteData = try AnisetteDataManager.shared.requestAnisetteData()
let response = AnisetteDataResponse(anisetteData: anisetteData)
completionHandler(.success(response))
}
catch
{
completionHandler(.failure(error))
}
}
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void)
{
guard let fileURL = request.fileURL else { return completionHandler(.failure(ALTServerError(.invalidRequest))) }
print("Awaiting begin installation request...")
connection.receiveRequest() { (result) in
print("Received begin installation request with result:", result)
do
{
guard case .beginInstallation(let request) = try result.get() else { throw ALTServerError(.unknownRequest) }
guard let bundleIdentifier = request.bundleIdentifier else { throw ALTServerError(.invalidRequest) }
AppManager.shared.installApp(at: fileURL, bundleIdentifier: bundleIdentifier, activeProfiles: request.activeProfiles) { (result) in
let result = result.map { InstallationProgressResponse(progress: 1.0) }
print("Installed app with result:", result)
completionHandler(result)
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for connection: Connection,
completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void)
{
AppManager.shared.install(request.provisioningProfiles, activeProfiles: request.activeProfiles) { (result) in
switch result
{
case .failure(let error):
print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", error)
completionHandler(.failure(error))
case .success:
print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier })
let response = InstallProvisioningProfilesResponse()
completionHandler(.success(response))
}
}
}
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for connection: Connection,
completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void)
{
AppManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers) { (result) in
switch result
{
case .failure(let error):
print("Failed to remove profiles \(request.bundleIdentifiers):", error)
completionHandler(.failure(error))
case .success:
print("Removed profiles:", request.bundleIdentifiers)
let response = RemoveProvisioningProfilesResponse()
completionHandler(.success(response))
}
}
}
func handleRemoveAppRequest(_ request: RemoveAppRequest, for connection: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void)
{
AppManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier) { (result) in
switch result
{
case .failure(let error):
print("Failed to remove app \(request.bundleIdentifier):", error)
completionHandler(.failure(error))
case .success:
print("Removed app:", request.bundleIdentifier)
let response = RemoveAppResponse()
completionHandler(.success(response))
}
}
}
}

View File

@@ -0,0 +1,93 @@
//
// XPCConnectionHandler.swift
// AltDaemon
//
// Created by Riley Testut on 9/14/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import Security
class XPCConnectionHandler: NSObject, ConnectionHandler
{
var connectionHandler: ((Connection) -> Void)?
var disconnectionHandler: ((Connection) -> Void)?
private let dispatchQueue = DispatchQueue(label: "io.altstore.XPCConnectionListener", qos: .utility)
private let listeners = XPCConnection.machServiceNames.map { NSXPCListener.makeListener(machServiceName: $0) }
deinit
{
self.stopListening()
}
func startListening()
{
for listener in self.listeners
{
listener.delegate = self
listener.resume()
}
}
func stopListening()
{
self.listeners.forEach { $0.suspend() }
}
}
private extension XPCConnectionHandler
{
func disconnect(_ connection: Connection)
{
connection.disconnect()
self.disconnectionHandler?(connection)
}
}
extension XPCConnectionHandler: NSXPCListenerDelegate
{
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool
{
let maximumPathLength = 4 * UInt32(MAXPATHLEN)
let pathBuffer = UnsafeMutablePointer<CChar>.allocate(capacity: Int(maximumPathLength))
defer { pathBuffer.deallocate() }
proc_pidpath(newConnection.processIdentifier, pathBuffer, maximumPathLength)
let path = String(cString: pathBuffer)
let fileURL = URL(fileURLWithPath: path)
var code: UnsafeMutableRawPointer?
defer { code.map { Unmanaged<AnyObject>.fromOpaque($0).release() } }
var status = SecStaticCodeCreateWithPath(fileURL as CFURL, 0, &code)
guard status == 0 else { return false }
var signingInfo: CFDictionary?
defer { signingInfo.map { Unmanaged<AnyObject>.passUnretained($0).release() } }
status = SecCodeCopySigningInformation(code, kSecCSInternalInformation | kSecCSSigningInformation, &signingInfo)
guard status == 0 else { return false }
// Only accept connections from AltStore.
guard
let codeSigningInfo = signingInfo as? [String: Any],
let bundleIdentifier = codeSigningInfo["identifier"] as? String,
bundleIdentifier.contains("com.rileytestut.AltStore")
else { return false }
let connection = XPCConnection(newConnection)
newConnection.invalidationHandler = { [weak self, weak connection] in
guard let self = self, let connection = connection else { return }
self.disconnect(connection)
}
self.connectionHandler?(connection)
return true
}
}

14
AltDaemon/main.swift Normal file
View File

@@ -0,0 +1,14 @@
//
// main.swift
// AltDaemon
//
// Created by Riley Testut on 6/2/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
autoreleasepool {
DaemonConnectionManager.shared.start()
RunLoop.current.run()
}

View File

@@ -0,0 +1,10 @@
Package: com.rileytestut.altdaemon
Name: AltDaemon
Depends:
Version: 1.0
Architecture: iphoneos-arm
Description: AltDaemon allows AltStore to install and refresh apps without a computer.
Maintainer: Riley Testut
Author: Riley Testut
Homepage: https://altstore.io
Section: System

View File

@@ -0,0 +1,2 @@
#!/bin/sh
launchctl load /Library/LaunchDaemons/com.rileytestut.altdaemon.plist

View File

@@ -0,0 +1,2 @@
#!/bin/sh
launchctl unload /Library/LaunchDaemons/com.rileytestut.altdaemon.plist >> /dev/null 2>&1

2
AltDaemon/package/DEBIAN/prerm Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
launchctl unload /Library/LaunchDaemons/com.rileytestut.altdaemon.plist

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.rileytestut.altdaemon</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/env</string>
<string>_MSSafeMode=1</string>
<string>_SafeMode=1</string>
<string>/usr/bin/AltDaemon</string>
</array>
<key>UserName</key>
<string>mobile</string>
<key>KeepAlive</key>
<false/>
<key>RunAtLoad</key>
<false/>
<key>MachServices</key>
<dict>
<key>cy:io.altstore.altdaemon</key>
<true/>
<key>lh:io.altstore.altdaemon</key>
<true/>
</dict>
</dict>
</plist>

Binary file not shown.

View File

@@ -1,58 +0,0 @@
//
// CodableServerError.swift
// AltKit
//
// Created by Riley Testut on 3/5/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
// Can only automatically conform ALTServerError.Code to Codable, not ALTServerError itself
extension ALTServerError.Code: Codable {}
struct CodableServerError: Codable
{
var error: ALTServerError {
return ALTServerError(self.errorCode, userInfo: self.userInfo ?? [:])
}
private var errorCode: ALTServerError.Code
private var userInfo: [String: String]?
private enum CodingKeys: String, CodingKey
{
case errorCode
case userInfo
}
init(error: ALTServerError)
{
self.errorCode = error.code
let userInfo = error.userInfo.compactMapValues { $0 as? String }
if !userInfo.isEmpty
{
self.userInfo = userInfo
}
}
init(from decoder: Decoder) throws
{
let container = try decoder.container(keyedBy: CodingKeys.self)
let errorCode = try container.decode(Int.self, forKey: .errorCode)
self.errorCode = ALTServerError.Code(rawValue: errorCode) ?? .unknown
let userInfo = try container.decodeIfPresent([String: String].self, forKey: .userInfo)
self.userInfo = userInfo
}
func encode(to encoder: Encoder) throws
{
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.error.code.rawValue, forKey: .errorCode)
try container.encodeIfPresent(self.userInfo, forKey: .userInfo)
}
}

View File

@@ -8,11 +8,15 @@
#import <Foundation/Foundation.h>
@class ALTAnisetteData;
NS_ASSUME_NONNULL_BEGIN
@interface ALTPluginService : NSObject
+ (instancetype)sharedService;
@property (class, nonatomic, readonly) ALTPluginService *sharedService;
- (ALTAnisetteData *)requestAnisetteData;
@end

View File

@@ -67,10 +67,8 @@
[[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(receiveNotification:) name:@"com.rileytestut.AltServer.FetchAnisetteData" object:nil];
}
- (void)receiveNotification:(NSNotification *)notification
- (ALTAnisetteData *)requestAnisetteData
{
NSString *requestUUID = notification.userInfo[@"requestUUID"];
NSMutableURLRequest* req = [[NSMutableURLRequest alloc] initWithURL:[[NSURL alloc] initWithString:@"https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA"]];
[req setHTTPMethod:@"POST"];
@@ -91,6 +89,14 @@
locale:[NSLocale currentLocale]
timeZone:[NSTimeZone localTimeZone]];
return anisetteData;
}
- (void)receiveNotification:(NSNotification *)notification
{
NSString *requestUUID = notification.userInfo[@"requestUUID"];
ALTAnisetteData *anisetteData = [self requestAnisetteData];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:anisetteData requiringSecureCoding:YES error:nil];
[[NSDistributedNotificationCenter defaultCenter] postNotificationName:@"com.rileytestut.AltServer.AnisetteDataResponse" object:nil userInfo:@{@"requestUUID": requestUUID, @"anisetteData": data} deliverImmediately:YES];

View File

@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2019 Riley Testut. All rights reserved.</string>
<key>NSPrincipalClass</key>
@@ -58,5 +58,21 @@
<string># For mail version 13.0 (3594.4.2) on OS X Version 10.15 (build 19A558d)</string>
<string>6EEA38FB-1A0B-469B-BB35-4C2E0EEA9053</string>
</array>
<key>Supported11.0PluginCompatibilityUUIDs</key>
<array>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
</array>
<key>Supported11.1PluginCompatibilityUUIDs</key>
<array>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
</array>
<key>Supported11.2PluginCompatibilityUUIDs</key>
<array>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
</array>
<key>Supported11.3PluginCompatibilityUUIDs</key>
<array>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
</array>
</dict>
</plist>

BIN
AltServer/AltPlugin.zip Normal file

Binary file not shown.

View File

@@ -5,4 +5,10 @@
#import "ALTDeviceManager.h"
#import "ALTWiredConnection.h"
#import "ALTNotificationConnection.h"
#import "AltKit.h"
// Shared
#import "ALTConstants.h"
#import "ALTConnection.h"
#import "AltXPCProtocol.h"
#import "NSError+ALTServerError.h"
#import "CFNotificationName+AltStore.h"

View File

@@ -7,7 +7,28 @@
//
import Foundation
import AltKit
private extension Bundle
{
struct ID
{
static let mail = "com.apple.mail"
static let altXPC = "com.rileytestut.AltXPC"
}
}
private extension ALTAnisetteData
{
func sanitize(byReplacingBundleID bundleID: String)
{
guard let range = self.deviceDescription.lowercased().range(of: "(" + bundleID.lowercased()) else { return }
var adjustedDescription = self.deviceDescription[..<range.lowerBound]
adjustedDescription += "(com.apple.dt.Xcode/3594.4.19)>"
self.deviceDescription = String(adjustedDescription)
}
}
class AnisetteDataManager: NSObject
{
@@ -16,6 +37,13 @@ class AnisetteDataManager: NSObject
private var anisetteDataCompletionHandlers: [String: (Result<ALTAnisetteData, Error>) -> Void] = [:]
private var anisetteDataTimers: [String: Timer] = [:]
private lazy var xpcConnection: NSXPCConnection = {
let connection = NSXPCConnection(serviceName: Bundle.ID.altXPC)
connection.remoteObjectInterface = NSXPCInterface(with: AltXPCProtocol.self)
connection.resume()
return connection
}()
private override init()
{
super.init()
@@ -24,6 +52,55 @@ class AnisetteDataManager: NSObject
}
func requestAnisetteData(_ completion: @escaping (Result<ALTAnisetteData, Error>) -> Void)
{
self.requestAnisetteDataFromXPCService { (result) in
do
{
let anisetteData = try result.get()
completion(.success(anisetteData))
}
catch CocoaError.xpcConnectionInterrupted
{
// SIP and/or AMFI are not disabled, so fall back to Mail plug-in.
self.requestAnisetteDataFromPlugin { (result) in
completion(result)
}
}
catch
{
completion(.failure(error))
}
}
}
func isXPCAvailable(completion: @escaping (Bool) -> Void)
{
guard let proxy = self.xpcConnection.remoteObjectProxyWithErrorHandler({ (error) in
completion(false)
}) as? AltXPCProtocol else { return }
proxy.ping {
completion(true)
}
}
}
private extension AnisetteDataManager
{
func requestAnisetteDataFromXPCService(completion: @escaping (Result<ALTAnisetteData, Error>) -> Void)
{
guard let proxy = self.xpcConnection.remoteObjectProxyWithErrorHandler({ (error) in
print("Anisette XPC Error:", error)
completion(.failure(error))
}) as? AltXPCProtocol else { return }
proxy.requestAnisetteData { (anisetteData, error) in
anisetteData?.sanitize(byReplacingBundleID: Bundle.ID.altXPC)
completion(Result(anisetteData, error))
}
}
func requestAnisetteDataFromPlugin(completion: @escaping (Result<ALTAnisetteData, Error>) -> Void)
{
let requestUUID = UUID().uuidString
self.anisetteDataCompletionHandlers[requestUUID] = completion
@@ -37,10 +114,7 @@ class AnisetteDataManager: NSObject
DistributedNotificationCenter.default().postNotificationName(Notification.Name("com.rileytestut.AltServer.FetchAnisetteData"), object: nil, userInfo: ["requestUUID": requestUUID], options: .deliverImmediately)
}
}
private extension AnisetteDataManager
{
@objc func handleAnisetteDataResponse(_ notification: Notification)
{
guard let userInfo = notification.userInfo, let requestUUID = userInfo["requestUUID"] as? String else { return }
@@ -49,14 +123,7 @@ private extension AnisetteDataManager
let archivedAnisetteData = userInfo["anisetteData"] as? Data,
let anisetteData = try? NSKeyedUnarchiver.unarchivedObject(ofClass: ALTAnisetteData.self, from: archivedAnisetteData)
{
if let range = anisetteData.deviceDescription.lowercased().range(of: "(com.apple.mail")
{
var adjustedDescription = anisetteData.deviceDescription[..<range.lowerBound]
adjustedDescription += "(com.apple.dt.Xcode/3594.4.19)>"
anisetteData.deviceDescription = String(adjustedDescription)
}
anisetteData.sanitize(byReplacingBundleID: Bundle.ID.mail)
self.finishRequest(forUUID: requestUUID, result: .success(anisetteData))
}
else

View File

@@ -12,32 +12,20 @@ import UserNotifications
import AltSign
import LaunchAtLogin
import STPrivilegedTask
private let pluginDirectoryURL = URL(fileURLWithPath: "/Library/Mail/Bundles", isDirectory: true)
private let pluginURL = pluginDirectoryURL.appendingPathComponent("AltPlugin.mailbundle")
enum PluginError: LocalizedError
{
case cancelled
case unknown
case taskError(String)
case taskErrorCode(Int)
var errorDescription: String? {
switch self
{
case .cancelled: return NSLocalizedString("Mail plug-in installation was cancelled.", comment: "")
case .unknown: return NSLocalizedString("Failed to install Mail plug-in.", comment: "")
case .taskError(let output): return output
case .taskErrorCode(let errorCode): return String(format: NSLocalizedString("There was an error installing the Mail plug-in. (Error Code: %@)", comment: ""), NSNumber(value: errorCode))
}
}
}
#if STAGING
private let altstoreAppURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/altstore.ipa")!
#elseif BETA
private let altstoreAppURL = URL(string: "https://f000.backblazeb2.com/file/altstore/altstore-beta.ipa")!
#else
private let altstoreAppURL = URL(string: "https://f000.backblazeb2.com/file/altstore/altstore.ipa")!
#endif
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
private let pluginManager = PluginManager()
private var statusItem: NSStatusItem?
private var connectedDevices = [ALTDevice]()
@@ -46,36 +34,30 @@ class AppDelegate: NSObject, NSApplicationDelegate {
@IBOutlet private var appMenu: NSMenu!
@IBOutlet private var connectedDevicesMenu: NSMenu!
@IBOutlet private var sideloadIPAConnectedDevicesMenu: NSMenu!
@IBOutlet private var launchAtLoginMenuItem: NSMenuItem!
@IBOutlet private var installMailPluginMenuItem: NSMenuItem!
private weak var authenticationAppleIDTextField: NSTextField?
private weak var authenticationPasswordTextField: NSSecureTextField?
private var isMailPluginInstalled: Bool {
let isMailPluginInstalled = FileManager.default.fileExists(atPath: pluginURL.path)
return isMailPluginInstalled
}
func applicationDidFinishLaunching(_ aNotification: Notification)
{
UserDefaults.standard.registerDefaults()
UNUserNotificationCenter.current().delegate = self
ConnectionManager.shared.start()
ServerConnectionManager.shared.start()
ALTDeviceManager.shared.start()
let item = NSStatusBar.system.statusItem(withLength: -1)
guard let button = item.button else { return }
button.image = NSImage(named: "MenuBarIcon")
button.target = self
button.action = #selector(AppDelegate.presentMenu)
item.menu = self.appMenu
item.button?.image = NSImage(named: "MenuBarIcon")
self.statusItem = item
self.appMenu.delegate = self
self.connectedDevicesMenu.delegate = self
self.sideloadIPAConnectedDevicesMenu.delegate = self
UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { (success, error) in
guard success else { return }
@@ -92,6 +74,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
UserDefaults.standard.didPresentInitialNotification = true
}
}
if self.pluginManager.isUpdateAvailable
{
self.installMailPlugin()
}
}
func applicationWillTerminate(_ aNotification: Notification)
@@ -102,44 +89,32 @@ class AppDelegate: NSObject, NSApplicationDelegate {
private extension AppDelegate
{
@objc func presentMenu()
{
guard let button = self.statusItem?.button, let superview = button.superview, let window = button.window else { return }
self.connectedDevices = ALTDeviceManager.shared.availableDevices
self.launchAtLoginMenuItem.state = LaunchAtLogin.isEnabled ? .on : .off
self.launchAtLoginMenuItem.action = #selector(AppDelegate.toggleLaunchAtLogin(_:))
if self.isMailPluginInstalled
{
self.installMailPluginMenuItem.title = NSLocalizedString("Uninstall Mail Plug-in", comment: "")
}
else
{
self.installMailPluginMenuItem.title = NSLocalizedString("Install Mail Plug-in", comment: "")
}
self.installMailPluginMenuItem.target = self
self.installMailPluginMenuItem.action = #selector(AppDelegate.handleInstallMailPluginMenuItem(_:))
let x = button.frame.origin.x
let y = button.frame.origin.y - 5
let location = superview.convert(NSMakePoint(x, y), to: nil)
guard let event = NSEvent.mouseEvent(with: .leftMouseUp, location: location,
modifierFlags: [], timestamp: 0, windowNumber: window.windowNumber, context: nil,
eventNumber: 0, clickCount: 1, pressure: 0)
else { return }
NSMenu.popUpContextMenu(self.appMenu, with: event, for: button)
}
@objc func installAltStore(_ item: NSMenuItem)
{
guard case let index = self.connectedDevicesMenu.index(of: item), index != -1 else { return }
guard let index = item.menu?.index(of: item), index != -1 else { return }
let device = self.connectedDevices[index]
self.installApplication(at: altstoreAppURL, to: device)
}
@objc func sideloadIPA(_ item: NSMenuItem)
{
guard let index = item.menu?.index(of: item), index != -1 else { return }
let device = self.connectedDevices[index]
let openPanel = NSOpenPanel()
openPanel.canChooseDirectories = false
openPanel.allowsMultipleSelection = false
openPanel.allowedFileTypes = ["ipa"]
openPanel.begin { (response) in
guard let fileURL = openPanel.url, response == .OK else { return }
self.installApplication(at: fileURL, to: device)
}
}
func installApplication(at url: URL, to device: ALTDevice)
{
let alert = NSAlert()
alert.messageText = NSLocalizedString("Please enter your Apple ID and password.", comment: "")
alert.informativeText = NSLocalizedString("Your Apple ID and password are not saved and are only sent to Apple for authentication.", comment: "")
@@ -182,18 +157,16 @@ private extension AppDelegate
let username = appleIDTextField.stringValue
let password = passwordTextField.stringValue
let device = self.connectedDevices[index]
func install()
{
ALTDeviceManager.shared.installAltStore(to: device, appleID: username, password: password) { (result) in
ALTDeviceManager.shared.installApplication(at: url, to: device, appleID: username, password: password) { (result) in
switch result
{
case .success:
case .success(let application):
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("Installation Succeeded", comment: "")
content.body = String(format: NSLocalizedString("AltStore was successfully installed on %@.", comment: ""), device.name)
content.body = String(format: NSLocalizedString("%@ was successfully installed on %@.", comment: ""), application.name, device.name)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
@@ -212,6 +185,10 @@ private extension AppDelegate
{
alert.informativeText = underlyingError.localizedDescription
}
else if let recoverySuggestion = error.localizedRecoverySuggestion
{
alert.informativeText = error.localizedDescription + "\n\n" + recoverySuggestion
}
else
{
alert.informativeText = error.localizedDescription
@@ -224,26 +201,25 @@ private extension AppDelegate
}
}
if !self.isMailPluginInstalled
if !self.pluginManager.isMailPluginInstalled || self.pluginManager.isUpdateAvailable
{
self.installMailPlugin { (result) in
DispatchQueue.main.async {
switch result
{
case .failure(PluginError.cancelled): break
case .failure(let error):
let alert = NSAlert()
alert.messageText = NSLocalizedString("Failed to Install Mail Plug-in", comment: "")
alert.informativeText = error.localizedDescription
alert.runModal()
case .success:
let alert = NSAlert()
alert.messageText = NSLocalizedString("Mail Plug-in Installed", comment: "")
alert.informativeText = NSLocalizedString("Please restart Mail and enable AltPlugin in Mail's Preferences. Mail must be running when installing or refreshing apps with AltServer.", comment: "")
alert.runModal()
install()
AnisetteDataManager.shared.isXPCAvailable { (isAvailable) in
if isAvailable
{
// XPC service is available, so we don't need to install/update Mail plug-in.
// Users can still manually do so from the AltServer menu.
install()
}
else
{
DispatchQueue.main.async {
self.installMailPlugin { (result) in
switch result
{
case .failure: break
case .success: install()
}
}
}
}
}
@@ -256,236 +232,109 @@ private extension AppDelegate
@objc func toggleLaunchAtLogin(_ item: NSMenuItem)
{
if item.state == .on
{
item.state = .off
}
else
{
item.state = .on
}
LaunchAtLogin.isEnabled.toggle()
}
@objc func handleInstallMailPluginMenuItem(_ item: NSMenuItem)
{
if self.isMailPluginInstalled
if !self.pluginManager.isMailPluginInstalled || self.pluginManager.isUpdateAvailable
{
self.uninstallMailPlugin { (result) in
DispatchQueue.main.async {
switch result
{
case .failure(PluginError.cancelled): break
case .failure(let error):
let alert = NSAlert()
alert.messageText = NSLocalizedString("Failed to Uninstall Mail Plug-in", comment: "")
alert.informativeText = error.localizedDescription
alert.runModal()
case .success:
let alert = NSAlert()
alert.messageText = NSLocalizedString("Mail Plug-in Uninstalled", comment: "")
alert.informativeText = NSLocalizedString("Please restart Mail for changes to take effect. You will not be able to use AltServer until the plug-in is reinstalled.", comment: "")
alert.runModal()
}
}
}
self.installMailPlugin()
}
else
{
self.installMailPlugin { (result) in
DispatchQueue.main.async {
switch result
{
case .failure(PluginError.cancelled): break
case .failure(let error):
let alert = NSAlert()
alert.messageText = NSLocalizedString("Failed to Install Mail Plug-in", comment: "")
alert.informativeText = error.localizedDescription
alert.runModal()
case .success:
let alert = NSAlert()
alert.messageText = NSLocalizedString("Mail Plug-in Installed", comment: "")
alert.informativeText = NSLocalizedString("Please restart Mail and enable AltPlugin in Mail's Preferences. Mail must be running when installing or refreshing apps with AltServer.", comment: "")
alert.runModal()
}
}
}
self.uninstallMailPlugin()
}
}
func installMailPlugin(completionHandler: @escaping (Result<Void, Error>) -> Void)
private func installMailPlugin(completion: ((Result<Void, Error>) -> Void)? = nil)
{
do
{
let alert = NSAlert()
alert.messageText = NSLocalizedString("Install Mail Plug-in", comment: "")
alert.informativeText = NSLocalizedString("AltServer requires a Mail plug-in in order to retrieve necessary information about your Apple ID. Would you like to install it now?", comment: "")
alert.addButton(withTitle: NSLocalizedString("Install Plug-in", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
let response = alert.runModal()
guard response == .alertFirstButtonReturn else { throw PluginError.cancelled }
self.downloadPlugin { (result) in
do
self.pluginManager.installMailPlugin { (result) in
DispatchQueue.main.async {
switch result
{
let fileURL = try result.get()
defer { try? FileManager.default.removeItem(at: fileURL) }
case .failure(PluginError.cancelled): break
case .failure(let error):
let alert = NSAlert()
alert.messageText = NSLocalizedString("Failed to Install Mail Plug-in", comment: "")
alert.informativeText = error.localizedDescription
alert.runModal()
// Ensure plug-in directory exists.
let authorization = try self.runAndKeepAuthorization("mkdir", arguments: ["-p", pluginDirectoryURL.path])
// Unzip AltPlugin to plug-ins directory.
try self.runAndKeepAuthorization("unzip", arguments: ["-o", fileURL.path, "-d", pluginDirectoryURL.path], authorization: authorization)
guard self.isMailPluginInstalled else { throw PluginError.unknown }
// Enable Mail plug-in preferences.
try self.run("defaults", arguments: ["write", "/Library/Preferences/com.apple.mail", "EnableBundles", "-bool", "YES"], authorization: authorization)
print("Finished installing Mail plug-in!")
completionHandler(.success(()))
}
catch
{
completionHandler(.failure(error))
}
}
}
catch
{
completionHandler(.failure(PluginError.cancelled))
}
}
func downloadPlugin(completionHandler: @escaping (Result<URL, Error>) -> Void)
{
let pluginURL = URL(string: "https://f000.backblazeb2.com/file/altstore/altserver/altplugin/1_0.zip")!
let downloadTask = URLSession.shared.downloadTask(with: pluginURL) { (fileURL, response, error) in
if let fileURL = fileURL
{
print("Downloaded plugin to URL:", fileURL)
completionHandler(.success(fileURL))
}
else
{
completionHandler(.failure(error ?? PluginError.unknown))
}
}
downloadTask.resume()
}
func uninstallMailPlugin(completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let alert = NSAlert()
alert.messageText = NSLocalizedString("Uninstall Mail Plug-in", comment: "")
alert.informativeText = NSLocalizedString("Are you sure you want to uninstall the AltServer Mail plug-in? You will no longer be able to install or refresh apps with AltStore.", comment: "")
alert.addButton(withTitle: NSLocalizedString("Uninstall Plug-in", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
let response = alert.runModal()
guard response == .alertFirstButtonReturn else { return completionHandler(.failure(PluginError.cancelled)) }
DispatchQueue.global().async {
do
{
if FileManager.default.fileExists(atPath: pluginURL.path)
{
// Delete Mail plug-in from privileged directory.
try self.run("rm", arguments: ["-rf", pluginURL.path])
case .success:
let alert = NSAlert()
alert.messageText = NSLocalizedString("Mail Plug-in Installed", comment: "")
alert.informativeText = NSLocalizedString("Please restart Mail and enable AltPlugin in Mail's Preferences. Mail must be running when installing or refreshing apps with AltServer.", comment: "")
alert.runModal()
}
completionHandler(.success(()))
}
catch
{
completionHandler(.failure(error))
completion?(result)
}
}
}
}
private extension AppDelegate
{
func run(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil) throws
{
_ = try self._run(program, arguments: arguments, authorization: authorization, freeAuthorization: true)
}
@discardableResult
func runAndKeepAuthorization(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil) throws -> AuthorizationRef
private func uninstallMailPlugin()
{
return try self._run(program, arguments: arguments, authorization: authorization, freeAuthorization: false)
}
func _run(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil, freeAuthorization: Bool) throws -> AuthorizationRef
{
var launchPath = "/usr/bin/" + program
if !FileManager.default.fileExists(atPath: launchPath)
{
launchPath = "/bin/" + program
}
print("Running program:", launchPath)
let task = STPrivilegedTask()
task.launchPath = launchPath
task.arguments = arguments
task.freeAuthorizationWhenDone = freeAuthorization
let errorCode: OSStatus
if let authorization = authorization
{
errorCode = task.launch(withAuthorization: authorization)
}
else
{
errorCode = task.launch()
}
guard errorCode == 0 else { throw PluginError.taskErrorCode(Int(errorCode)) }
task.waitUntilExit()
print("Exit code:", task.terminationStatus)
guard task.terminationStatus == 0 else {
let outputData = task.outputFileHandle.readDataToEndOfFile()
if let outputString = String(data: outputData, encoding: .utf8), !outputString.isEmpty
{
throw PluginError.taskError(outputString)
self.pluginManager.uninstallMailPlugin { (result) in
DispatchQueue.main.async {
switch result
{
case .failure(PluginError.cancelled): break
case .failure(let error):
let alert = NSAlert()
alert.messageText = NSLocalizedString("Failed to Uninstall Mail Plug-in", comment: "")
alert.informativeText = error.localizedDescription
alert.runModal()
case .success:
let alert = NSAlert()
alert.messageText = NSLocalizedString("Mail Plug-in Uninstalled", comment: "")
alert.informativeText = NSLocalizedString("Please restart Mail for changes to take effect. You will not be able to use AltServer until the plug-in is reinstalled.", comment: "")
alert.runModal()
}
}
throw PluginError.taskErrorCode(Int(task.terminationStatus))
}
guard let authorization = task.authorization else { throw PluginError.unknown }
return authorization
}
}
extension AppDelegate: NSMenuDelegate
{
func menuWillOpen(_ menu: NSMenu)
{
guard menu == self.appMenu else { return }
self.connectedDevices = ALTDeviceManager.shared.availableDevices
self.launchAtLoginMenuItem.target = self
self.launchAtLoginMenuItem.action = #selector(AppDelegate.toggleLaunchAtLogin(_:))
self.launchAtLoginMenuItem.state = LaunchAtLogin.isEnabled ? .on : .off
if self.pluginManager.isUpdateAvailable
{
self.installMailPluginMenuItem.title = NSLocalizedString("Update Mail Plug-in", comment: "")
}
else if self.pluginManager.isMailPluginInstalled
{
self.installMailPluginMenuItem.title = NSLocalizedString("Uninstall Mail Plug-in", comment: "")
}
else
{
self.installMailPluginMenuItem.title = NSLocalizedString("Install Mail Plug-in", comment: "")
}
self.installMailPluginMenuItem.target = self
self.installMailPluginMenuItem.action = #selector(AppDelegate.handleInstallMailPluginMenuItem(_:))
}
func numberOfItems(in menu: NSMenu) -> Int
{
guard menu == self.connectedDevicesMenu || menu == self.sideloadIPAConnectedDevicesMenu else { return -1 }
return self.connectedDevices.isEmpty ? 1 : self.connectedDevices.count
}
func menu(_ menu: NSMenu, update item: NSMenuItem, at index: Int, shouldCancel: Bool) -> Bool
{
guard menu == self.connectedDevicesMenu || menu == self.sideloadIPAConnectedDevicesMenu else { return false }
if self.connectedDevices.isEmpty
{
item.title = NSLocalizedString("No Connected Devices", comment: "")
@@ -499,7 +348,7 @@ extension AppDelegate: NSMenuDelegate
item.title = device.name
item.isEnabled = true
item.target = self
item.action = #selector(AppDelegate.installAltStore)
item.action = (menu == self.connectedDevicesMenu) ? #selector(AppDelegate.installAltStore(_:)) : #selector(AppDelegate.sideloadIPA(_:))
item.tag = index
}

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="15504" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="17503.1" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15504"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17503.1"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@@ -64,6 +64,7 @@
<outlet property="connectedDevicesMenu" destination="KJ9-WY-pW1" id="Mcv-64-iFU"/>
<outlet property="installMailPluginMenuItem" destination="3CM-gV-X2G" id="lio-ha-z0S"/>
<outlet property="launchAtLoginMenuItem" destination="IyR-FQ-upe" id="Fxn-EP-hwH"/>
<outlet property="sideloadIPAConnectedDevicesMenu" destination="IuI-bV-fTY" id="QQw-St-HfG"/>
</connections>
</customObject>
<customObject id="Arf-IC-5eb" customClass="SUUpdater"/>
@@ -97,6 +98,22 @@
</connections>
</menu>
</menuItem>
<menuItem title="Sideload .ipa" id="x0e-zI-0A2" userLabel="Install .ipa">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Sideload .ipa" systemMenu="recentDocuments" id="IuI-bV-fTY">
<items>
<menuItem title="No Connected Devices" id="in5-an-MD0">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="clearRecentDocuments:" target="Ady-hI-5gd" id="aUE-On-axK"/>
</connections>
</menuItem>
</items>
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="N3K-su-XV6"/>
</connections>
</menu>
</menuItem>
<menuItem isSeparatorItem="YES" id="1ZZ-BB-xHy"/>
<menuItem title="Launch at Login" id="IyR-FQ-upe" userLabel="Launch At Login">
<modifierMask key="keyEquivalentModifierMask"/>

View File

@@ -6,7 +6,7 @@
// Copyright © 2020 Riley Testut. All rights reserved.
//
#import <AltSign/AltSign.h>
#import "AltSign.h"
NS_ASSUME_NONNULL_BEGIN

View File

@@ -7,7 +7,8 @@
//
#import "ALTNotificationConnection+Private.h"
#import "AltKit.h"
#import "NSError+ALTServerError.h"
void ALTDeviceReceivedNotification(const char *notification, void *user_data);

View File

@@ -14,6 +14,8 @@ NS_ASSUME_NONNULL_BEGIN
@interface ALTWiredConnection ()
@property (nonatomic, readwrite, getter=isConnected) BOOL connected;
@property (nonatomic, readonly) idevice_connection_t connection;
- (instancetype)initWithDevice:(ALTDevice *)device connection:(idevice_connection_t)connection;

View File

@@ -6,12 +6,16 @@
// Copyright © 2020 Riley Testut. All rights reserved.
//
#import <AltSign/AltSign.h>
#import "AltSign.h"
#import "ALTConnection.h"
NS_ASSUME_NONNULL_BEGIN
NS_SWIFT_NAME(WiredConnection)
@interface ALTWiredConnection : NSObject
@interface ALTWiredConnection : NSObject <ALTConnection>
@property (nonatomic, readonly, getter=isConnected) BOOL connected;
@property (nonatomic, copy, readonly) ALTDevice *device;

View File

@@ -7,7 +7,9 @@
//
#import "ALTWiredConnection+Private.h"
#import "AltKit.h"
#import "ALTConnection.h"
#import "NSError+ALTServerError.h"
@implementation ALTWiredConnection
@@ -30,8 +32,15 @@
- (void)disconnect
{
if (![self isConnected])
{
return;
}
idevice_disconnect(self.connection);
_connection = nil;
self.connected = NO;
}
- (void)sendData:(NSData *)data completionHandler:(void (^)(BOOL, NSError * _Nullable))completionHandler
@@ -85,7 +94,7 @@
uint32_t size = MIN(4096, (uint32_t)expectedSize - (uint32_t)receivedData.length);
uint32_t receivedBytes = 0;
if (idevice_connection_receive_timeout(self.connection, bytes, size, &receivedBytes, 0) != IDEVICE_E_SUCCESS)
if (idevice_connection_receive_timeout(self.connection, bytes, size, &receivedBytes, 10000) != IDEVICE_E_SUCCESS)
{
return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
}
@@ -98,4 +107,11 @@
});
}
#pragma mark - NSObject -
- (NSString *)description
{
return [NSString stringWithFormat:@"%@ (Wired)", self.device.name];
}
@end

View File

@@ -1,231 +0,0 @@
//
// ClientConnection.swift
// AltServer
//
// Created by Riley Testut on 1/9/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import Network
import AltKit
import AltSign
extension ClientConnection
{
enum Connection
{
case wireless(NWConnection)
case wired(WiredConnection)
}
}
class ClientConnection
{
let connection: Connection
init(connection: Connection)
{
self.connection = connection
}
func disconnect()
{
switch self.connection
{
case .wireless(let connection):
switch connection.state
{
case .cancelled, .failed:
print("Disconnecting from \(connection.endpoint)...")
default:
// State update handler might call this method again.
connection.cancel()
}
case .wired(let connection):
connection.disconnect()
}
}
func send<T: Encodable>(_ response: T, shouldDisconnect: Bool = false, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
{
func finish(_ result: Result<Void, ALTServerError>)
{
completionHandler(result)
if shouldDisconnect
{
// Add short delay to prevent us from dropping connection too quickly.
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
self.disconnect()
}
}
}
do
{
let data = try JSONEncoder().encode(response)
let responseSize = withUnsafeBytes(of: Int32(data.count)) { Data($0) }
self.send(responseSize) { (result) in
switch result
{
case .failure: finish(.failure(.init(.lostConnection)))
case .success:
self.send(data) { (result) in
switch result
{
case .failure: finish(.failure(.init(.lostConnection)))
case .success: finish(.success(()))
}
}
}
}
}
catch
{
finish(.failure(.init(.invalidResponse)))
}
}
func receiveRequest(completionHandler: @escaping (Result<ServerRequest, ALTServerError>) -> Void)
{
let size = MemoryLayout<Int32>.size
print("Receiving request size")
self.receiveData(expectedBytes: size) { (result) in
do
{
let data = try result.get()
print("Receiving request...")
let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) })
self.receiveData(expectedBytes: expectedBytes) { (result) in
do
{
let data = try result.get()
let request = try JSONDecoder().decode(ServerRequest.self, from: data)
print("Received installation request:", request)
completionHandler(.success(request))
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
}
func send(_ data: Data, completionHandler: @escaping (Result<Void, Error>) -> Void)
{
switch self.connection
{
case .wireless(let connection):
connection.send(content: data, completion: .contentProcessed { (error) in
if let error = error
{
completionHandler(.failure(error))
}
else
{
completionHandler(.success(()))
}
})
case .wired(let connection):
connection.send(data) { (success, error) in
if !success
{
completionHandler(.failure(ALTServerError(.lostConnection)))
}
else
{
completionHandler(.success(()))
}
}
}
}
func receiveData(expectedBytes: Int, completionHandler: @escaping (Result<Data, Error>) -> Void)
{
func finish(data: Data?, error: Error?)
{
do
{
let data = try self.process(data: data, error: error)
completionHandler(.success(data))
}
catch
{
completionHandler(.failure(ALTServerError(error)))
}
}
switch self.connection
{
case .wireless(let connection):
connection.receive(minimumIncompleteLength: expectedBytes, maximumLength: expectedBytes) { (data, _, _, error) in
finish(data: data, error: error)
}
case .wired(let connection):
connection.receiveData(withExpectedSize: expectedBytes) { (data, error) in
finish(data: data, error: error)
}
}
}
}
extension ClientConnection: CustomStringConvertible
{
var description: String {
switch self.connection
{
case .wireless(let connection): return "\(connection.endpoint) (Wireless)"
case .wired(let connection): return "\(connection.device.name) (Wired)"
}
}
}
private extension ClientConnection
{
func process(data: Data?, error: Error?) throws -> Data
{
do
{
do
{
guard let data = data else { throw error ?? ALTServerError(.unknown) }
return data
}
catch let error as NWError
{
print("Error receiving data from connection \(connection)", error)
throw ALTServerError(.lostConnection)
}
catch
{
throw error
}
}
catch let error as ALTServerError
{
throw error
}
catch
{
preconditionFailure("A non-ALTServerError should never be thrown from this method.")
}
}
}

View File

@@ -1,508 +0,0 @@
//
// ConnectionManager.swift
// AltServer
//
// Created by Riley Testut on 5/23/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import Network
import AppKit
import AltKit
extension ALTServerError
{
init<E: Error>(_ error: E)
{
switch error
{
case let error as ALTServerError: self = error
case is DecodingError: self = ALTServerError(.invalidRequest)
case is EncodingError: self = ALTServerError(.invalidResponse)
case let error as NSError:
self = ALTServerError(.unknown, userInfo: error.userInfo)
}
}
}
extension ConnectionManager
{
enum State
{
case notRunning
case connecting
case running(NWListener.Service)
case failed(Swift.Error)
}
}
class ConnectionManager
{
static let shared = ConnectionManager()
var stateUpdateHandler: ((State) -> Void)?
private(set) var state: State = .notRunning {
didSet {
self.stateUpdateHandler?(self.state)
}
}
private lazy var listener = self.makeListener()
private let dispatchQueue = DispatchQueue(label: "com.rileytestut.AltServer.connections", qos: .utility)
private var connections = [ClientConnection]()
private var notificationConnections = [ALTDevice: NotificationConnection]()
private init()
{
NotificationCenter.default.addObserver(self, selector: #selector(ConnectionManager.deviceDidConnect(_:)), name: .deviceManagerDeviceDidConnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ConnectionManager.deviceDidDisconnect(_:)), name: .deviceManagerDeviceDidDisconnect, object: nil)
}
func start()
{
switch self.state
{
case .notRunning, .failed: self.listener.start(queue: self.dispatchQueue)
default: break
}
}
func stop()
{
switch self.state
{
case .running: self.listener.cancel()
default: break
}
}
func disconnect(_ connection: ClientConnection)
{
connection.disconnect()
if let index = self.connections.firstIndex(where: { $0 === connection })
{
self.connections.remove(at: index)
}
}
}
private extension ConnectionManager
{
func makeListener() -> NWListener
{
let listener = try! NWListener(using: .tcp)
let service: NWListener.Service
if let serverID = UserDefaults.standard.serverID?.data(using: .utf8)
{
let txtDictionary = ["serverID": serverID]
let txtData = NetService.data(fromTXTRecord: txtDictionary)
service = NWListener.Service(name: nil, type: ALTServerServiceType, domain: nil, txtRecord: txtData)
}
else
{
service = NWListener.Service(type: ALTServerServiceType)
}
listener.service = service
listener.serviceRegistrationUpdateHandler = { (serviceChange) in
switch serviceChange
{
case .add(.service(let name, let type, let domain, _)):
let service = NWListener.Service(name: name, type: type, domain: domain, txtRecord: nil)
self.state = .running(service)
default: break
}
}
listener.stateUpdateHandler = { (state) in
switch state
{
case .ready: break
case .waiting, .setup: self.state = .connecting
case .cancelled: self.state = .notRunning
case .failed(let error):
self.state = .failed(error)
self.start()
@unknown default: break
}
}
listener.newConnectionHandler = { [weak self] (connection) in
self?.prepare(connection)
}
return listener
}
func prepare(_ connection: NWConnection)
{
let clientConnection = ClientConnection(connection: .wireless(connection))
guard !self.connections.contains(where: { $0 === clientConnection }) else { return }
self.connections.append(clientConnection)
connection.stateUpdateHandler = { [weak self] (state) in
switch state
{
case .setup, .preparing: break
case .ready:
print("Connected to client:", connection.endpoint)
self?.handleRequest(for: clientConnection)
case .waiting:
print("Waiting for connection...")
case .failed(let error):
print("Failed to connect to service \(connection.endpoint).", error)
self?.disconnect(clientConnection)
case .cancelled:
self?.disconnect(clientConnection)
@unknown default: break
}
}
connection.start(queue: self.dispatchQueue)
}
}
private extension ConnectionManager
{
func startNotificationConnection(to device: ALTDevice)
{
ALTDeviceManager.shared.startNotificationConnection(to: device) { (connection, error) in
guard let connection = connection else { return }
let notifications: [CFNotificationName] = [.wiredServerConnectionAvailableRequest, .wiredServerConnectionStartRequest]
connection.startListening(forNotifications: notifications.map { String($0.rawValue) }) { (success, error) in
guard success else { return }
connection.receivedNotificationHandler = { [weak self, weak connection] (notification) in
guard let self = self, let connection = connection else { return }
self.handle(notification, for: connection)
}
self.notificationConnections[device] = connection
}
}
}
func stopNotificationConnection(to device: ALTDevice)
{
guard let connection = self.notificationConnections[device] else { return }
connection.disconnect()
self.notificationConnections[device] = nil
}
func handle(_ notification: CFNotificationName, for connection: NotificationConnection)
{
switch notification
{
case .wiredServerConnectionAvailableRequest:
connection.sendNotification(.wiredServerConnectionAvailableResponse) { (success, error) in
if let error = error, !success
{
print("Error sending wired server connection response.", error)
}
else
{
print("Sent wired server connection available response!")
}
}
case .wiredServerConnectionStartRequest:
ALTDeviceManager.shared.startWiredConnection(to: connection.device) { (wiredConnection, error) in
if let wiredConnection = wiredConnection
{
print("Started wired server connection!")
let clientConnection = ClientConnection(connection: .wired(wiredConnection))
self.handleRequest(for: clientConnection)
}
else if let error = error
{
print("Error starting wired server connection.", error)
}
}
default: break
}
}
}
private extension ConnectionManager
{
func handleRequest(for connection: ClientConnection)
{
connection.receiveRequest() { (result) in
print("Received initial request with result:", result)
switch result
{
case .failure(let error):
let response = ErrorResponse(error: ALTServerError(error))
connection.send(response, shouldDisconnect: true) { (result) in
print("Sent error response with result:", result)
}
case .success(.anisetteData(let request)):
self.handleAnisetteDataRequest(request, for: connection)
case .success(.prepareApp(let request)):
self.handlePrepareAppRequest(request, for: connection)
case .success(.beginInstallation): break
case .success(.installProvisioningProfiles(let request)):
self.handleInstallProvisioningProfilesRequest(request, for: connection)
case .success(.removeProvisioningProfiles(let request)):
self.handleRemoveProvisioningProfilesRequest(request, for: connection)
case .success(.unknown):
let response = ErrorResponse(error: ALTServerError(.unknownRequest))
connection.send(response, shouldDisconnect: true) { (result) in
print("Sent unknown request response with result:", result)
}
}
}
}
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: ClientConnection)
{
AnisetteDataManager.shared.requestAnisetteData { (result) in
switch result
{
case .failure(let error):
let errorResponse = ErrorResponse(error: ALTServerError(error))
connection.send(errorResponse, shouldDisconnect: true) { (result) in
print("Sent anisette data error response with result:", result)
}
case .success(let anisetteData):
let response = AnisetteDataResponse(anisetteData: anisetteData)
connection.send(response, shouldDisconnect: true) { (result) in
print("Sent anisette data response with result:", result)
}
}
}
}
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: ClientConnection)
{
var temporaryURL: URL?
func finish(_ result: Result<Void, ALTServerError>)
{
if let temporaryURL = temporaryURL
{
do { try FileManager.default.removeItem(at: temporaryURL) }
catch { print("Failed to remove .ipa.", error) }
}
switch result
{
case .failure(let error):
print("Failed to process request from \(connection).", error)
let response = ErrorResponse(error: ALTServerError(error))
connection.send(response, shouldDisconnect: true) { (result) in
print("Sent install app error response to \(connection) with result:", result)
}
case .success:
print("Processed request from \(connection).")
let response = InstallationProgressResponse(progress: 1.0)
connection.send(response, shouldDisconnect: true) { (result) in
print("Sent install app response to \(connection) with result:", result)
}
}
}
self.receiveApp(for: request, from: connection) { (result) in
print("Received app with result:", result)
switch result
{
case .failure(let error): finish(.failure(error))
case .success(let fileURL):
temporaryURL = fileURL
print("Awaiting begin installation request...")
connection.receiveRequest() { (result) in
print("Received begin installation request with result:", result)
switch result
{
case .failure(let error): finish(.failure(error))
case .success(.beginInstallation(let installRequest)):
print("Installing to device \(request.udid)...")
self.installApp(at: fileURL, toDeviceWithUDID: request.udid, activeProvisioningProfiles: installRequest.activeProfiles, connection: connection) { (result) in
print("Installed to device with result:", result)
switch result
{
case .failure(let error): finish(.failure(error))
case .success: finish(.success(()))
}
}
case .success:
let response = ErrorResponse(error: ALTServerError(.unknownRequest))
connection.send(response, shouldDisconnect: true) { (result) in
print("Sent unknown request error response to \(connection) with result:", result)
}
}
}
}
}
}
func receiveApp(for request: PrepareAppRequest, from connection: ClientConnection, completionHandler: @escaping (Result<URL, ALTServerError>) -> Void)
{
connection.receiveData(expectedBytes: request.contentSize) { (result) in
do
{
print("Received app data!")
let data = try result.get()
guard ALTDeviceManager.shared.availableDevices.contains(where: { $0.identifier == request.udid }) else { throw ALTServerError(.deviceNotFound) }
print("Writing app data...")
let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".ipa")
try data.write(to: temporaryURL, options: .atomic)
print("Wrote app to URL:", temporaryURL)
completionHandler(.success(temporaryURL))
}
catch
{
print("Error processing app data:", error)
completionHandler(.failure(ALTServerError(error)))
}
}
}
func installApp(at fileURL: URL, toDeviceWithUDID udid: String, activeProvisioningProfiles: Set<String>?, connection: ClientConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
{
let serialQueue = DispatchQueue(label: "com.altstore.ConnectionManager.installQueue", qos: .default)
var isSending = false
var observation: NSKeyValueObservation?
let progress = ALTDeviceManager.shared.installApp(at: fileURL, toDeviceWithUDID: udid, activeProvisioningProfiles: activeProvisioningProfiles) { (success, error) in
print("Installed app with result:", error == nil ? "Success" : error!.localizedDescription)
if let error = error.map({ $0 as? ALTServerError ?? ALTServerError(.unknown) })
{
completionHandler(.failure(error))
}
else
{
completionHandler(.success(()))
}
observation?.invalidate()
observation = nil
}
observation = progress.observe(\.fractionCompleted, changeHandler: { (progress, change) in
serialQueue.async {
guard !isSending else { return }
isSending = true
print("Progress:", progress.fractionCompleted)
let response = InstallationProgressResponse(progress: progress.fractionCompleted)
connection.send(response) { (result) in
serialQueue.async {
isSending = false
}
}
}
})
}
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for connection: ClientConnection)
{
ALTDeviceManager.shared.installProvisioningProfiles(request.provisioningProfiles, toDeviceWithUDID: request.udid, activeProvisioningProfiles: request.activeProfiles) { (success, error) in
if let error = error, !success
{
print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", error)
let errorResponse = ErrorResponse(error: ALTServerError(error))
connection.send(errorResponse, shouldDisconnect: true) { (result) in
print("Sent install profiles error response with result:", result)
}
}
else
{
print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier })
let response = InstallProvisioningProfilesResponse()
connection.send(response, shouldDisconnect: true) { (result) in
print("Sent install profiles response to \(connection) with result:", result)
}
}
}
}
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for connection: ClientConnection)
{
ALTDeviceManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers, fromDeviceWithUDID: request.udid) { (success, error) in
if let error = error, !success
{
print("Failed to remove profiles \(request.bundleIdentifiers):", error)
let errorResponse = ErrorResponse(error: ALTServerError(error))
connection.send(errorResponse, shouldDisconnect: true) { (result) in
print("Sent remove profiles error response with result:", result)
}
}
else
{
print("Removed profiles:", request.bundleIdentifiers)
let response = RemoveProvisioningProfilesResponse()
connection.send(response, shouldDisconnect: true) { (result) in
print("Sent remove profiles error response to \(connection) with result:", result)
}
}
}
}
}
private extension ConnectionManager
{
@objc func deviceDidConnect(_ notification: Notification)
{
guard let device = notification.object as? ALTDevice else { return }
self.startNotificationConnection(to: device)
}
@objc func deviceDidDisconnect(_ notification: Notification)
{
guard let device = notification.object as? ALTDevice else { return }
self.stopNotificationConnection(to: device)
}
}

View File

@@ -0,0 +1,218 @@
//
// RequestHandler.swift
// AltServer
//
// Created by Riley Testut on 5/23/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
typealias ServerConnectionManager = ConnectionManager<ServerRequestHandler>
private let connectionManager = ConnectionManager(requestHandler: ServerRequestHandler(),
connectionHandlers: [WirelessConnectionHandler(), WiredConnectionHandler()])
extension ServerConnectionManager
{
static var shared: ConnectionManager {
return connectionManager
}
}
struct ServerRequestHandler: RequestHandler
{
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void)
{
AnisetteDataManager.shared.requestAnisetteData { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let anisetteData):
let response = AnisetteDataResponse(anisetteData: anisetteData)
completionHandler(.success(response))
}
}
}
func handlePrepareAppRequest(_ request: PrepareAppRequest, for connection: Connection, completionHandler: @escaping (Result<InstallationProgressResponse, Error>) -> Void)
{
var temporaryURL: URL?
func finish(_ result: Result<InstallationProgressResponse, Error>)
{
if let temporaryURL = temporaryURL
{
do { try FileManager.default.removeItem(at: temporaryURL) }
catch { print("Failed to remove .ipa.", error) }
}
completionHandler(result)
}
self.receiveApp(for: request, from: connection) { (result) in
print("Received app with result:", result)
switch result
{
case .failure(let error): finish(.failure(error))
case .success(let fileURL):
temporaryURL = fileURL
print("Awaiting begin installation request...")
connection.receiveRequest() { (result) in
print("Received begin installation request with result:", result)
switch result
{
case .failure(let error): finish(.failure(error))
case .success(.beginInstallation(let installRequest)):
print("Installing app to device \(request.udid)...")
self.installApp(at: fileURL, toDeviceWithUDID: request.udid, activeProvisioningProfiles: installRequest.activeProfiles, connection: connection) { (result) in
print("Installed app to device with result:", result)
switch result
{
case .failure(let error): finish(.failure(error))
case .success:
let response = InstallationProgressResponse(progress: 1.0)
finish(.success(response))
}
}
case .success: finish(.failure(ALTServerError(.unknownRequest)))
}
}
}
}
}
func handleInstallProvisioningProfilesRequest(_ request: InstallProvisioningProfilesRequest, for connection: Connection,
completionHandler: @escaping (Result<InstallProvisioningProfilesResponse, Error>) -> Void)
{
ALTDeviceManager.shared.installProvisioningProfiles(request.provisioningProfiles, toDeviceWithUDID: request.udid, activeProvisioningProfiles: request.activeProfiles) { (success, error) in
if let error = error, !success
{
print("Failed to install profiles \(request.provisioningProfiles.map { $0.bundleIdentifier }):", error)
completionHandler(.failure(ALTServerError(error)))
}
else
{
print("Installed profiles:", request.provisioningProfiles.map { $0.bundleIdentifier })
let response = InstallProvisioningProfilesResponse()
completionHandler(.success(response))
}
}
}
func handleRemoveProvisioningProfilesRequest(_ request: RemoveProvisioningProfilesRequest, for connection: Connection,
completionHandler: @escaping (Result<RemoveProvisioningProfilesResponse, Error>) -> Void)
{
ALTDeviceManager.shared.removeProvisioningProfiles(forBundleIdentifiers: request.bundleIdentifiers, fromDeviceWithUDID: request.udid) { (success, error) in
if let error = error, !success
{
print("Failed to remove profiles \(request.bundleIdentifiers):", error)
completionHandler(.failure(ALTServerError(error)))
}
else
{
print("Removed profiles:", request.bundleIdentifiers)
let response = RemoveProvisioningProfilesResponse()
completionHandler(.success(response))
}
}
}
func handleRemoveAppRequest(_ request: RemoveAppRequest, for connection: Connection, completionHandler: @escaping (Result<RemoveAppResponse, Error>) -> Void)
{
ALTDeviceManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier, fromDeviceWithUDID: request.udid) { (success, error) in
if let error = error, !success
{
print("Failed to remove app \(request.bundleIdentifier):", error)
completionHandler(.failure(ALTServerError(error)))
}
else
{
print("Removed app:", request.bundleIdentifier)
let response = RemoveAppResponse()
completionHandler(.success(response))
}
}
}
}
private extension RequestHandler
{
func receiveApp(for request: PrepareAppRequest, from connection: Connection, completionHandler: @escaping (Result<URL, ALTServerError>) -> Void)
{
connection.receiveData(expectedSize: request.contentSize) { (result) in
do
{
print("Received app data!")
let data = try result.get()
guard ALTDeviceManager.shared.availableDevices.contains(where: { $0.identifier == request.udid }) else { throw ALTServerError(.deviceNotFound) }
print("Writing app data...")
let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".ipa")
try data.write(to: temporaryURL, options: .atomic)
print("Wrote app to URL:", temporaryURL)
completionHandler(.success(temporaryURL))
}
catch
{
print("Error processing app data:", error)
completionHandler(.failure(ALTServerError(error)))
}
}
}
func installApp(at fileURL: URL, toDeviceWithUDID udid: String, activeProvisioningProfiles: Set<String>?, connection: Connection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
{
let serialQueue = DispatchQueue(label: "com.altstore.ConnectionManager.installQueue", qos: .default)
var isSending = false
var observation: NSKeyValueObservation?
let progress = ALTDeviceManager.shared.installApp(at: fileURL, toDeviceWithUDID: udid, activeProvisioningProfiles: activeProvisioningProfiles) { (success, error) in
print("Installed app with result:", error == nil ? "Success" : error!.localizedDescription)
if let error = error.map({ ALTServerError($0) })
{
completionHandler(.failure(error))
}
else
{
completionHandler(.success(()))
}
observation?.invalidate()
observation = nil
}
observation = progress.observe(\.fractionCompleted, changeHandler: { (progress, change) in
serialQueue.async {
guard !isSending else { return }
isSending = true
print("Progress:", progress.fractionCompleted)
let response = InstallationProgressResponse(progress: progress.fractionCompleted)
connection.send(response) { (result) in
serialQueue.async {
isSending = false
}
}
}
})
}
}

View File

@@ -0,0 +1,115 @@
//
// WiredConnectionHandler.swift
// AltServer
//
// Created by Riley Testut on 6/1/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
class WiredConnectionHandler: ConnectionHandler
{
var connectionHandler: ((Connection) -> Void)?
var disconnectionHandler: ((Connection) -> Void)?
private var notificationConnections = [ALTDevice: NotificationConnection]()
func startListening()
{
NotificationCenter.default.addObserver(self, selector: #selector(WiredConnectionHandler.deviceDidConnect(_:)), name: .deviceManagerDeviceDidConnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(WiredConnectionHandler.deviceDidDisconnect(_:)), name: .deviceManagerDeviceDidDisconnect, object: nil)
}
func stopListening()
{
NotificationCenter.default.removeObserver(self, name: .deviceManagerDeviceDidConnect, object: nil)
NotificationCenter.default.removeObserver(self, name: .deviceManagerDeviceDidDisconnect, object: nil)
}
}
private extension WiredConnectionHandler
{
func startNotificationConnection(to device: ALTDevice)
{
ALTDeviceManager.shared.startNotificationConnection(to: device) { (connection, error) in
guard let connection = connection else { return }
let notifications: [CFNotificationName] = [.wiredServerConnectionAvailableRequest, .wiredServerConnectionStartRequest]
connection.startListening(forNotifications: notifications.map { String($0.rawValue) }) { (success, error) in
guard success else { return }
connection.receivedNotificationHandler = { [weak self, weak connection] (notification) in
guard let self = self, let connection = connection else { return }
self.handle(notification, for: connection)
}
self.notificationConnections[device] = connection
}
}
}
func stopNotificationConnection(to device: ALTDevice)
{
guard let connection = self.notificationConnections[device] else { return }
connection.disconnect()
self.notificationConnections[device] = nil
}
func handle(_ notification: CFNotificationName, for connection: NotificationConnection)
{
switch notification
{
case .wiredServerConnectionAvailableRequest:
connection.sendNotification(.wiredServerConnectionAvailableResponse) { (success, error) in
if let error = error, !success
{
print("Error sending wired server connection response.", error)
}
else
{
print("Sent wired server connection available response!")
}
}
case .wiredServerConnectionStartRequest:
ALTDeviceManager.shared.startWiredConnection(to: connection.device) { (wiredConnection, error) in
if let wiredConnection = wiredConnection
{
print("Started wired server connection!")
self.connectionHandler?(wiredConnection)
var observation: NSKeyValueObservation?
observation = wiredConnection.observe(\.isConnected) { [weak self] (connection, change) in
guard !connection.isConnected else { return }
self?.disconnectionHandler?(connection)
observation?.invalidate()
}
}
else if let error = error
{
print("Error starting wired server connection.", error)
}
}
default: break
}
}
}
private extension WiredConnectionHandler
{
@objc func deviceDidConnect(_ notification: Notification)
{
guard let device = notification.object as? ALTDevice else { return }
self.startNotificationConnection(to: device)
}
@objc func deviceDidDisconnect(_ notification: Notification)
{
guard let device = notification.object as? ALTDevice else { return }
self.stopNotificationConnection(to: device)
}
}

View File

@@ -0,0 +1,148 @@
//
// WirelessConnectionHandler.swift
// AltKit
//
// Created by Riley Testut on 6/1/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import Network
extension WirelessConnectionHandler
{
public enum State
{
case notRunning
case connecting
case running(NWListener.Service)
case failed(Swift.Error)
}
}
public class WirelessConnectionHandler: ConnectionHandler
{
public var connectionHandler: ((Connection) -> Void)?
public var disconnectionHandler: ((Connection) -> Void)?
public var stateUpdateHandler: ((State) -> Void)?
public private(set) var state: State = .notRunning {
didSet {
self.stateUpdateHandler?(self.state)
}
}
private lazy var listener = self.makeListener()
private let dispatchQueue = DispatchQueue(label: "io.altstore.WirelessConnectionListener", qos: .utility)
public func startListening()
{
switch self.state
{
case .notRunning, .failed: self.listener.start(queue: self.dispatchQueue)
default: break
}
}
public func stopListening()
{
switch self.state
{
case .running: self.listener.cancel()
default: break
}
}
}
private extension WirelessConnectionHandler
{
func makeListener() -> NWListener
{
let listener = try! NWListener(using: .tcp)
let service: NWListener.Service
if let serverID = UserDefaults.standard.serverID?.data(using: .utf8)
{
let txtDictionary = ["serverID": serverID]
let txtData = NetService.data(fromTXTRecord: txtDictionary)
service = NWListener.Service(name: nil, type: ALTServerServiceType, domain: nil, txtRecord: txtData)
}
else
{
service = NWListener.Service(type: ALTServerServiceType)
}
listener.service = service
listener.serviceRegistrationUpdateHandler = { (serviceChange) in
switch serviceChange
{
case .add(.service(let name, let type, let domain, _)):
let service = NWListener.Service(name: name, type: type, domain: domain, txtRecord: nil)
self.state = .running(service)
default: break
}
}
listener.stateUpdateHandler = { (state) in
switch state
{
case .ready: break
case .waiting, .setup: self.state = .connecting
case .cancelled: self.state = .notRunning
case .failed(let error): self.state = .failed(error)
@unknown default: break
}
}
listener.newConnectionHandler = { [weak self] (connection) in
self?.prepare(connection)
}
return listener
}
func prepare(_ nwConnection: NWConnection)
{
print("Preparing:", nwConnection)
// Use same instance for all callbacks.
let connection = NetworkConnection(nwConnection)
nwConnection.stateUpdateHandler = { [weak self] (state) in
switch state
{
case .setup, .preparing: break
case .ready:
print("Connected to client:", connection)
self?.connectionHandler?(connection)
case .waiting:
print("Waiting for connection...")
case .failed(let error):
print("Failed to connect to service \(nwConnection.endpoint).", error)
self?.disconnect(connection)
case .cancelled:
self?.disconnect(connection)
@unknown default: break
}
}
nwConnection.start(queue: self.dispatchQueue)
}
func disconnect(_ connection: Connection)
{
connection.disconnect()
self.disconnectionHandler?(connection)
}
}

View File

@@ -10,11 +10,7 @@ import Cocoa
import UserNotifications
import ObjectiveC
#if STAGING
private let appURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/altstore.ipa")!
#else
private let appURL = URL(string: "https://f000.backblazeb2.com/file/altstore/altstore.ipa")!
#endif
private let appGroupsLock = NSLock()
enum InstallError: LocalizedError
{
@@ -36,21 +32,14 @@ enum InstallError: LocalizedError
extension ALTDeviceManager
{
func installAltStore(to device: ALTDevice, appleID: String, password: String, completion: @escaping (Result<Void, Error>) -> Void)
func installApplication(at url: URL, to device: ALTDevice, appleID: String, password: String, completion: @escaping (Result<ALTApplication, Error>) -> Void)
{
let destinationDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
func finish(_ error: Error?, title: String = "")
func finish(_ result: Result<ALTApplication, Error>, title: String = "")
{
DispatchQueue.main.async {
if let error = error
{
completion(.failure(error))
}
else
{
completion(.success(()))
}
completion(result)
}
try? FileManager.default.removeItem(at: destinationDirectoryURL)
@@ -81,14 +70,13 @@ extension ALTDeviceManager
{
let certificate = try result.get()
let content = UNMutableNotificationContent()
content.title = String(format: NSLocalizedString("Installing AltStore to %@...", comment: ""), device.name)
content.body = NSLocalizedString("This may take a few seconds.", comment: "")
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
self.downloadApp { (result) in
if !url.isFileURL
{
// Show alert before downloading remote .ipa.
self.showInstallationAlert(appName: NSLocalizedString("AltStore", comment: ""), deviceName: device.name)
}
self.downloadApp(from: url) { (result) in
do
{
let fileURL = try result.get()
@@ -96,18 +84,14 @@ extension ALTDeviceManager
try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil)
let appBundleURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: destinationDirectoryURL)
do
{
try FileManager.default.removeItem(at: fileURL)
}
catch
{
print("Failed to remove downloaded .ipa.", error)
}
guard let application = ALTApplication(fileURL: appBundleURL) else { throw ALTError(.invalidApp) }
if url.isFileURL
{
// Show alert after "downloading" local .ipa.
self.showInstallationAlert(appName: application.name, deviceName: device.name)
}
// Refresh anisette data to prevent session timeouts.
AnisetteDataManager.shared.requestAnisetteData { (result) in
do
@@ -115,93 +99,79 @@ extension ALTDeviceManager
let anisetteData = try result.get()
session.anisetteData = anisetteData
self.registerAppID(name: "AltStore", identifier: "com.rileytestut.AltStore", team: team, session: session) { (result) in
self.prepareAllProvisioningProfiles(for: application, device: device, team: team, session: session) { (result) in
do
{
let appID = try result.get()
let profiles = try result.get()
self.updateFeatures(for: appID, app: application, team: team, session: session) { (result) in
do
{
let appID = try result.get()
self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in
do
{
let provisioningProfile = try result.get()
self.install(application, to: device, team: team, appID: appID, certificate: certificate, profile: provisioningProfile) { (result) in
finish(result.error, title: "Failed to Install AltStore")
}
}
catch
{
finish(error, title: "Failed to Fetch Provisioning Profile")
}
}
}
catch
{
finish(error, title: "Failed to Update App ID")
}
self.install(application, to: device, team: team, certificate: certificate, profiles: profiles) { (result) in
finish(result.map { application }, title: "Failed to Install AltStore")
}
}
catch
{
finish(error, title: "Failed to Register App")
finish(.failure(error), title: "Failed to Fetch Provisioning Profiles")
}
}
}
catch
{
finish(error, title: "Failed to Refresh Anisette Data")
finish(.failure(error), title: "Failed to Refresh Anisette Data")
}
}
}
catch
{
finish(error, title: "Failed to Download AltStore")
finish(.failure(error), title: "Failed to Download AltStore")
}
}
}
catch
{
finish(error, title: "Failed to Fetch Certificate")
finish(.failure(error), title: "Failed to Fetch Certificate")
}
}
}
catch
{
finish(error, title: "Failed to Register Device")
finish(.failure(error), title: "Failed to Register Device")
}
}
}
catch
{
finish(error, title: "Failed to Fetch Team")
finish(.failure(error), title: "Failed to Fetch Team")
}
}
}
catch
{
finish(error, title: "Failed to Authenticate")
finish(.failure(error), title: "Failed to Authenticate")
}
}
}
catch
{
finish(error, title: "Failed to Fetch Anisette Data")
finish(.failure(error), title: "Failed to Fetch Anisette Data")
}
}
}
func downloadApp(completionHandler: @escaping (Result<URL, Error>) -> Void)
}
private extension ALTDeviceManager
{
func downloadApp(from url: URL, completionHandler: @escaping (Result<URL, Error>) -> Void)
{
let downloadTask = URLSession.shared.downloadTask(with: appURL) { (fileURL, response, error) in
guard !url.isFileURL else { return completionHandler(.success(url)) }
let downloadTask = URLSession.shared.downloadTask(with: url) { (fileURL, response, error) in
do
{
let (fileURL, _) = try Result((fileURL, response), error).get()
completionHandler(.success(fileURL))
do { try FileManager.default.removeItem(at: fileURL) }
catch { print("Failed to remove downloaded .ipa.", error) }
}
catch
{
@@ -264,26 +234,93 @@ extension ALTDeviceManager
func fetchTeam(for account: ALTAccount, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTTeam, Error>) -> Void)
{
func finish(_ result: Result<ALTTeam, Error>)
{
switch result
ALTAppleAPI.shared.fetchTeams(for: account, session: session) { (teams, error) in
do
{
case .failure(let error):
completionHandler(.failure(error))
let teams = try Result(teams, error).get()
case .success(let team):
if let team = teams.first(where: { $0.type == .individual })
{
return completionHandler(.success(team))
}
else if let team = teams.first(where: { $0.type == .free })
{
return completionHandler(.success(team))
}
else if let team = teams.first
{
return completionHandler(.success(team))
}
else
{
throw InstallError.noTeam
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func fetchCertificate(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTCertificate, Error>) -> Void)
{
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
do
{
let certificates = try Result(certificates, error).get()
let applicationSupportDirectoryURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
let altserverDirectoryURL = applicationSupportDirectoryURL.appendingPathComponent("com.rileytestut.AltServer")
let certificatesDirectoryURL = altserverDirectoryURL.appendingPathComponent("Certificates")
try FileManager.default.createDirectory(at: certificatesDirectoryURL, withIntermediateDirectories: true, attributes: nil)
let certificateFileURL = certificatesDirectoryURL.appendingPathComponent(team.identifier + ".p12")
var isCancelled = false
// Check if there is another AltStore certificate, which means AltStore has been installed with this Apple ID before.
if let previousCertificate = certificates.first(where: { $0.machineName?.starts(with: "AltStore") == true })
{
if FileManager.default.fileExists(atPath: certificateFileURL.path),
let data = try? Data(contentsOf: certificateFileURL),
let certificate = ALTCertificate(p12Data: data, password: previousCertificate.machineIdentifier)
{
// Manually set machineIdentifier so we can encrypt + embed certificate if needed.
certificate.machineIdentifier = previousCertificate.machineIdentifier
return completionHandler(.success(certificate))
}
DispatchQueue.main.sync {
let alert = NSAlert()
alert.messageText = NSLocalizedString("Multiple AltServers Not Supported", comment: "")
alert.informativeText = NSLocalizedString("Please use the same AltServer you previously used with this Apple ID, or else apps installed with other AltServers will stop working.\n\nAre you sure you want to continue?", comment: "")
alert.addButton(withTitle: NSLocalizedString("Continue", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
let buttonIndex = alert.runModal()
if buttonIndex == NSApplication.ModalResponse.alertSecondButtonReturn
{
isCancelled = true
}
}
guard !isCancelled else { return completionHandler(.failure(InstallError.cancelled)) }
}
if team.type != .free
{
DispatchQueue.main.sync {
let alert = NSAlert()
alert.messageText = NSLocalizedString("Installing AltStore will revoke your iOS development certificate.", comment: "")
alert.messageText = NSLocalizedString("Installing this app will revoke your iOS development certificate.", comment: "")
alert.informativeText = NSLocalizedString("""
This will not affect apps you've submitted to the App Store, but may cause apps you've installed to your devices with Xcode to stop working until you reinstall them.
To prevent this from happening, feel free to try again with another Apple ID to install AltStore.
To prevent this from happening, feel free to try again with another Apple ID.
""", comment: "")
alert.addButton(withTitle: NSLocalizedString("Continue", comment: ""))
@@ -298,78 +335,7 @@ To prevent this from happening, feel free to try again with another Apple ID to
}
}
if isCancelled
{
return completionHandler(.failure(InstallError.cancelled))
}
}
completionHandler(.success(team))
}
}
ALTAppleAPI.shared.fetchTeams(for: account, session: session) { (teams, error) in
do
{
let teams = try Result(teams, error).get()
if let team = teams.first(where: { $0.type == .free })
{
return finish(.success(team))
}
else if let team = teams.first(where: { $0.type == .individual })
{
return finish(.success(team))
}
else if let team = teams.first
{
return finish(.success(team))
}
else
{
throw InstallError.noTeam
}
}
catch
{
finish(.failure(error))
}
}
}
func fetchCertificate(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTCertificate, Error>) -> Void)
{
ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in
do
{
let certificates = try Result(certificates, error).get()
// Check if there is another AltStore certificate, which means AltStore has been installed with this Apple ID before.
if certificates.contains(where: { $0.machineName?.starts(with: "AltStore") == true })
{
var isCancelled = false
DispatchQueue.main.sync {
let alert = NSAlert()
alert.messageText = NSLocalizedString("AltStore already installed on another device.", comment: "")
alert.informativeText = NSLocalizedString("Apps installed with AltStore on your other devices will stop working. Are you sure you want to continue?", comment: "")
alert.addButton(withTitle: NSLocalizedString("Continue", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
let buttonIndex = alert.runModal()
if buttonIndex == NSApplication.ModalResponse.alertSecondButtonReturn
{
isCancelled = true
}
}
if isCancelled
{
return completionHandler(.failure(InstallError.cancelled))
}
guard !isCancelled else { return completionHandler(.failure(InstallError.cancelled)) }
}
if let certificate = certificates.first
@@ -406,6 +372,14 @@ To prevent this from happening, feel free to try again with another Apple ID to
certificate.privateKey = privateKey
completionHandler(.success(certificate))
if let machineIdentifier = certificate.machineIdentifier,
let encryptedData = certificate.encryptedP12Data(withPassword: machineIdentifier)
{
// Cache certificate.
do { try encryptedData.write(to: certificateFileURL, options: .atomic) }
catch { print("Failed to cache certificate:", error) }
}
}
catch
{
@@ -427,10 +401,120 @@ To prevent this from happening, feel free to try again with another Apple ID to
}
}
func registerAppID(name appName: String, identifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
func prepareAllProvisioningProfiles(for application: ALTApplication, device: ALTDevice, team: ALTTeam, session: ALTAppleAPISession,
completion: @escaping (Result<[String: ALTProvisioningProfile], Error>) -> Void)
{
let bundleID = "com.\(team.identifier).\(identifier)"
self.prepareProvisioningProfile(for: application, parentApp: nil, device: device, team: team, session: session) { (result) in
do
{
let profile = try result.get()
var profiles = [application.bundleIdentifier: profile]
var error: Error?
let dispatchGroup = DispatchGroup()
for appExtension in application.appExtensions
{
dispatchGroup.enter()
self.prepareProvisioningProfile(for: appExtension, parentApp: application, device: device, team: team, session: session) { (result) in
switch result
{
case .failure(let e): error = e
case .success(let profile): profiles[appExtension.bundleIdentifier] = profile
}
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .global()) {
if let error = error
{
completion(.failure(error))
}
else
{
completion(.success(profiles))
}
}
}
catch
{
completion(.failure(error))
}
}
}
func prepareProvisioningProfile(for application: ALTApplication, parentApp: ALTApplication?, device: ALTDevice, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
let parentBundleID = parentApp?.bundleIdentifier ?? application.bundleIdentifier
let updatedParentBundleID: String
if application.isAltStoreApp
{
// Use legacy bundle ID format for AltStore (and its extensions).
updatedParentBundleID = "com.\(team.identifier).\(parentBundleID)"
}
else
{
updatedParentBundleID = parentBundleID + "." + team.identifier // Append just team identifier to make it harder to track.
}
let bundleID = application.bundleIdentifier.replacingOccurrences(of: parentBundleID, with: updatedParentBundleID)
let preferredName: String
if let parentApp = parentApp
{
preferredName = parentApp.name + " " + application.name
}
else
{
preferredName = application.name
}
self.registerAppID(name: preferredName, bundleID: bundleID, team: team, session: session) { (result) in
do
{
let appID = try result.get()
self.updateFeatures(for: appID, app: application, team: team, session: session) { (result) in
do
{
let appID = try result.get()
self.updateAppGroups(for: appID, app: application, team: team, session: session) { (result) in
do
{
let appID = try result.get()
self.fetchProvisioningProfile(for: appID, device: device, team: team, session: session) { (result) in
completionHandler(result)
}
}
catch
{
completionHandler(.failure(error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
catch
{
completionHandler(.failure(error))
}
}
}
func registerAppID(name appName: String, bundleID: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
do
{
@@ -468,17 +552,125 @@ To prevent this from happening, feel free to try again with another Apple ID to
features[.appGroups] = true
}
let appID = appID.copy() as! ALTAppID
appID.features = features
var updateFeatures = false
ALTAppleAPI.shared.update(appID, team: team, session: session) { (appID, error) in
completionHandler(Result(appID, error))
// Determine whether the required features are already enabled for the AppID.
for (feature, value) in features
{
if let appIDValue = appID.features[feature] as AnyObject?, (value as AnyObject).isEqual(appIDValue)
{
// AppID already has this feature enabled and the values are the same.
continue
}
else
{
// AppID either doesn't have this feature enabled or the value has changed,
// so we need to update it to reflect new values.
updateFeatures = true
break
}
}
if updateFeatures
{
let appID = appID.copy() as! ALTAppID
appID.features = features
ALTAppleAPI.shared.update(appID, team: team, session: session) { (appID, error) in
completionHandler(Result(appID, error))
}
}
else
{
completionHandler(.success(appID))
}
}
func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
let applicationGroups = app.entitlements[.appGroups] as? [String] ?? []
if applicationGroups.isEmpty
{
guard let isAppGroupsEnabled = appID.features[.appGroups] as? Bool, isAppGroupsEnabled else {
// No app groups, and we also haven't enabled the feature, so don't continue.
// For apps with no app groups but have had the feature enabled already
// we'll continue and assign the app ID to an empty array
// in case we need to explicitly remove them.
return completionHandler(.success(appID))
}
}
// Dispatch onto global queue to prevent appGroupsLock deadlock.
DispatchQueue.global().async {
// Ensure we're not concurrently fetching and updating app groups,
// which can lead to race conditions such as adding an app group twice.
appGroupsLock.lock()
func finish(_ result: Result<ALTAppID, Error>)
{
appGroupsLock.unlock()
completionHandler(result)
}
ALTAppleAPI.shared.fetchAppGroups(for: team, session: session) { (groups, error) in
switch Result(groups, error)
{
case .failure(let error): finish(.failure(error))
case .success(let fetchedGroups):
let dispatchGroup = DispatchGroup()
var groups = [ALTAppGroup]()
var errors = [Error]()
for groupIdentifier in applicationGroups
{
let adjustedGroupIdentifier = groupIdentifier + "." + team.identifier
if let group = fetchedGroups.first(where: { $0.groupIdentifier == adjustedGroupIdentifier })
{
groups.append(group)
}
else
{
dispatchGroup.enter()
// Not all characters are allowed in group names, so we replace periods with spaces (like Apple does).
let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ")
ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { (group, error) in
switch Result(group, error)
{
case .success(let group): groups.append(group)
case .failure(let error): errors.append(error)
}
dispatchGroup.leave()
}
}
}
dispatchGroup.notify(queue: .global()) {
if let error = errors.first
{
finish(.failure(error))
}
else
{
ALTAppleAPI.shared.assign(appID, to: Array(groups), team: team, session: session) { (success, error) in
let result = Result(success, error)
finish(result.map { _ in appID })
}
}
}
}
}
}
}
func register(_ device: ALTDevice, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTDevice, Error>) -> Void)
{
ALTAppleAPI.shared.fetchDevices(for: team, session: session) { (devices, error) in
ALTAppleAPI.shared.fetchDevices(for: team, types: device.type, session: session) { (devices, error) in
do
{
let devices = try Result(devices, error).get()
@@ -489,7 +681,7 @@ To prevent this from happening, feel free to try again with another Apple ID to
}
else
{
ALTAppleAPI.shared.registerDevice(name: device.name, identifier: device.identifier, team: team, session: session) { (device, error) in
ALTAppleAPI.shared.registerDevice(name: device.name, identifier: device.identifier, type: device.type, team: team, session: session) { (device, error) in
completionHandler(Result(device, error))
}
}
@@ -501,42 +693,86 @@ To prevent this from happening, feel free to try again with another Apple ID to
}
}
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
func fetchProvisioningProfile(for appID: ALTAppID, device: ALTDevice, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: device.type, team: team, session: session) { (profile, error) in
completionHandler(Result(profile, error))
}
}
func install(_ application: ALTApplication, to device: ALTDevice, team: ALTTeam, appID: ALTAppID, certificate: ALTCertificate, profile: ALTProvisioningProfile, completionHandler: @escaping (Result<Void, Error>) -> Void)
func install(_ application: ALTApplication, to device: ALTDevice, team: ALTTeam, certificate: ALTCertificate, profiles: [String: ALTProvisioningProfile], completionHandler: @escaping (Result<Void, Error>) -> Void)
{
func prepare(_ bundle: Bundle, additionalInfoDictionaryValues: [String: Any] = [:]) throws
{
guard let identifier = bundle.bundleIdentifier else { throw ALTError(.missingAppBundle) }
guard let profile = profiles[identifier] else { throw ALTError(.missingProvisioningProfile) }
guard var infoDictionary = bundle.completeInfoDictionary else { throw ALTError(.missingInfoPlist) }
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
infoDictionary[Bundle.Info.altBundleID] = identifier
for (key, value) in additionalInfoDictionaryValues
{
infoDictionary[key] = value
}
if let appGroups = profile.entitlements[.appGroups] as? [String]
{
infoDictionary[Bundle.Info.appGroups] = appGroups
}
try (infoDictionary as NSDictionary).write(to: bundle.infoPlistURL)
}
DispatchQueue.global().async {
do
{
let infoPlistURL = application.fileURL.appendingPathComponent("Info.plist")
guard let appBundle = Bundle(url: application.fileURL) else { throw ALTError(.missingAppBundle) }
guard let infoDictionary = appBundle.completeInfoDictionary else { throw ALTError(.missingInfoPlist) }
guard var infoDictionary = NSDictionary(contentsOf: infoPlistURL) as? [String: Any] else { throw ALTError(.missingInfoPlist) }
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
infoDictionary[Bundle.Info.deviceID] = device.identifier
infoDictionary[Bundle.Info.serverID] = UserDefaults.standard.serverID
infoDictionary[Bundle.Info.certificateID] = certificate.serialNumber
try (infoDictionary as NSDictionary).write(to: infoPlistURL)
if
let machineIdentifier = certificate.machineIdentifier,
let encryptedData = certificate.encryptedP12Data(withPassword: machineIdentifier)
let openAppURL = URL(string: "altstore-" + application.bundleIdentifier + "://")!
var allURLSchemes = infoDictionary[Bundle.Info.urlTypes] as? [[String: Any]] ?? []
// Embed open URL so AltBackup can return to AltStore.
let altstoreURLScheme = ["CFBundleTypeRole": "Editor",
"CFBundleURLName": application.bundleIdentifier,
"CFBundleURLSchemes": [openAppURL.scheme!]] as [String : Any]
allURLSchemes.append(altstoreURLScheme)
var additionalValues: [String: Any] = [Bundle.Info.urlTypes: allURLSchemes]
if application.isAltStoreApp
{
let certificateURL = application.fileURL.appendingPathComponent("ALTCertificate.p12")
try encryptedData.write(to: certificateURL, options: .atomic)
additionalValues[Bundle.Info.deviceID] = device.identifier
additionalValues[Bundle.Info.serverID] = UserDefaults.standard.serverID
if
let machineIdentifier = certificate.machineIdentifier,
let encryptedData = certificate.encryptedP12Data(withPassword: machineIdentifier)
{
additionalValues[Bundle.Info.certificateID] = certificate.serialNumber
let certificateURL = application.fileURL.appendingPathComponent("ALTCertificate.p12")
try encryptedData.write(to: certificateURL, options: .atomic)
}
}
try prepare(appBundle, additionalInfoDictionaryValues: additionalValues)
for appExtension in application.appExtensions
{
guard let bundle = Bundle(url: appExtension.fileURL) else { throw ALTError(.missingAppBundle) }
try prepare(bundle)
}
let resigner = ALTSigner(team: team, certificate: certificate)
resigner.signApp(at: application.fileURL, provisioningProfiles: [profile]) { (success, error) in
resigner.signApp(at: application.fileURL, provisioningProfiles: Array(profiles.values)) { (success, error) in
do
{
try Result(success, error).get()
let activeProfiles: Set<String>? = (team.type == .free) ? [profile.bundleIdentifier] : nil
let activeProfiles: Set<String>? = (team.type == .free && application.isAltStoreApp) ? Set(profiles.values.map(\.bundleIdentifier)) : nil
ALTDeviceManager.shared.installApp(at: application.fileURL, toDeviceWithUDID: device.identifier, activeProvisioningProfiles: activeProfiles) { (success, error) in
completionHandler(Result(success, error))
}
@@ -555,6 +791,16 @@ To prevent this from happening, feel free to try again with another Apple ID to
}
}
}
func showInstallationAlert(appName: String, deviceName: String)
{
let content = UNMutableNotificationContent()
content.title = String(format: NSLocalizedString("Installing %@ to %@...", comment: ""), appName, deviceName)
content.body = NSLocalizedString("This may take a few seconds.", comment: "")
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
}
}
private var securityCodeAlertKey = 0

View File

@@ -7,7 +7,7 @@
//
#import <Foundation/Foundation.h>
#import <AltSign/AltSign.h>
#import "AltSign.h"
@class ALTWiredConnection;
@class ALTNotificationConnection;
@@ -28,6 +28,7 @@ extern NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification
/* App Installation */
- (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(nullable NSSet<NSString *> *)activeProvisioningProfiles completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
- (void)removeAppForBundleIdentifier:(NSString *)bundleIdentifier fromDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
- (void)installProvisioningProfiles:(NSSet<ALTProvisioningProfile *> *)provisioningProfiles toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(nullable NSSet<NSString *> *)activeProvisioningProfiles completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
- (void)removeProvisioningProfilesForBundleIdentifiers:(NSSet<NSString *> *)bundleIdentifiers fromDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;

View File

@@ -8,10 +8,12 @@
#import "ALTDeviceManager.h"
#import "AltKit.h"
#import "ALTWiredConnection+Private.h"
#import "ALTNotificationConnection+Private.h"
#import "ALTConstants.h"
#import "NSError+ALTServerError.h"
#include <libimobiledevice/libimobiledevice.h>
#include <libimobiledevice/lockdown.h>
#include <libimobiledevice/installation_proxy.h>
@@ -20,6 +22,7 @@
#include <libimobiledevice/misagent.h>
void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *udid);
void ALTDeviceManagerUpdateAppDeletionStatus(plist_t command, plist_t status, void *uuid);
void ALTDeviceDidChangeConnectionStatus(const idevice_event_t *event, void *user_data);
NSNotificationName const ALTDeviceManagerDeviceDidConnectNotification = @"ALTDeviceManagerDeviceDidConnectNotification";
@@ -28,6 +31,8 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT
@interface ALTDeviceManager ()
@property (nonatomic, readonly) NSMutableDictionary<NSUUID *, void (^)(NSError *)> *installationCompletionHandlers;
@property (nonatomic, readonly) NSMutableDictionary<NSUUID *, void (^)(NSError *)> *deletionCompletionHandlers;
@property (nonatomic, readonly) NSMutableDictionary<NSUUID *, NSProgress *> *installationProgress;
@property (nonatomic, readonly) dispatch_queue_t installationQueue;
@@ -54,8 +59,9 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT
if (self)
{
_installationCompletionHandlers = [NSMutableDictionary dictionary];
_installationProgress = [NSMutableDictionary dictionary];
_deletionCompletionHandlers = [NSMutableDictionary dictionary];
_installationProgress = [NSMutableDictionary dictionary];
_installationQueue = dispatch_queue_create("com.rileytestut.AltServer.InstallationQueue", DISPATCH_QUEUE_SERIAL);
_cachedDevices = [NSMutableSet set];
@@ -291,6 +297,9 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT
NSError *writeError = nil;
if (![self writeDirectory:appBundleURL toDestinationURL:destinationURL client:afc progress:nil error:&writeError])
{
int removeResult = afc_remove_path_and_contents(afc, stagingURL.relativePath.fileSystemRepresentation);
NSLog(@"Remove staging app result: %@", @(removeResult));
return finish(writeError);
}
@@ -424,7 +433,7 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT
else
{
NSURL *destinationFileURL = [destinationURL URLByAppendingPathComponent:fileURL.lastPathComponent isDirectory:NO];
if (![self writeFile:fileURL toDestinationURL:destinationFileURL client:afc error:error])
if (![self writeFile:fileURL toDestinationURL:destinationFileURL progress:progress client:afc error:error])
{
return NO;
}
@@ -436,7 +445,7 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT
return YES;
}
- (BOOL)writeFile:(NSURL *)fileURL toDestinationURL:(NSURL *)destinationURL client:(afc_client_t)afc error:(NSError **)error
- (BOOL)writeFile:(NSURL *)fileURL toDestinationURL:(NSURL *)destinationURL progress:(NSProgress *)progress client:(afc_client_t)afc error:(NSError **)error
{
NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:fileURL.path];
if (fileHandle == nil)
@@ -452,8 +461,16 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT
NSData *data = [fileHandle readDataToEndOfFile];
uint64_t af = 0;
if ((afc_file_open(afc, destinationURL.relativePath.fileSystemRepresentation, AFC_FOPEN_WRONLY, &af) != AFC_E_SUCCESS) || af == 0)
int openResult = afc_file_open(afc, destinationURL.relativePath.fileSystemRepresentation, AFC_FOPEN_WRONLY, &af);
if (openResult != AFC_E_SUCCESS || af == 0)
{
if (openResult == AFC_E_OBJECT_IS_DIR)
{
NSLog(@"Treating file as directory: %@ %@", fileURL, destinationURL);
return [self writeDirectory:fileURL toDestinationURL:destinationURL client:afc progress:progress error:error];
}
if (error)
{
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteUnknownError userInfo:@{NSURLErrorKey: destinationURL}];
@@ -469,10 +486,12 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT
{
uint32_t count = 0;
if (afc_file_write(afc, af, (const char *)data.bytes + bytesWritten, (uint32_t)data.length - bytesWritten, &count) != AFC_E_SUCCESS)
int writeResult = afc_file_write(afc, af, (const char *)data.bytes + bytesWritten, (uint32_t)data.length - bytesWritten, &count);
if (writeResult != AFC_E_SUCCESS)
{
if (error)
{
NSLog(@"Failed writing file with error: %@ (%@ %@)", @(writeResult), fileURL, destinationURL);
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteUnknownError userInfo:@{NSURLErrorKey: destinationURL}];
}
@@ -487,6 +506,7 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT
{
if (error)
{
NSLog(@"Failed writing file due to mismatched sizes: %@ vs %@ (%@ %@)", @(bytesWritten), @(data.length), fileURL, destinationURL);
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteUnknownError userInfo:@{NSURLErrorKey: destinationURL}];
}
@@ -498,6 +518,87 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT
return success;
}
- (void)removeAppForBundleIdentifier:(NSString *)bundleIdentifier fromDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler
{
__block idevice_t device = NULL;
__block lockdownd_client_t client = NULL;
__block instproxy_client_t ipc = NULL;
__block lockdownd_service_descriptor_t service = NULL;
void (^finish)(NSError *error) = ^(NSError *e) {
__block NSError *error = e;
lockdownd_service_descriptor_free(service);
instproxy_client_free(ipc);
lockdownd_client_free(client);
idevice_free(device);
if (error != nil)
{
completionHandler(NO, error);
}
else
{
completionHandler(YES, nil);
}
};
/* Find Device */
if (idevice_new(&device, udid.UTF8String) != IDEVICE_E_SUCCESS)
{
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceNotFound userInfo:nil]);
}
/* Connect to Device */
if (lockdownd_client_new_with_handshake(device, &client, "altserver") != LOCKDOWN_E_SUCCESS)
{
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
}
/* Connect to Installation Proxy */
if ((lockdownd_start_service(client, "com.apple.mobile.installation_proxy", &service) != LOCKDOWN_E_SUCCESS) || service == NULL)
{
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
}
if (instproxy_client_new(device, service, &ipc) != INSTPROXY_E_SUCCESS)
{
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
}
if (service)
{
lockdownd_service_descriptor_free(service);
service = NULL;
}
NSUUID *UUID = [NSUUID UUID];
__block char *uuidString = (char *)malloc(UUID.UUIDString.length + 1);
strncpy(uuidString, (const char *)UUID.UUIDString.UTF8String, UUID.UUIDString.length);
uuidString[UUID.UUIDString.length] = '\0';
self.deletionCompletionHandlers[UUID] = ^(NSError *error) {
if (error != nil)
{
NSString *localizedFailure = [NSString stringWithFormat:NSLocalizedString(@"Could not remove “%@”.", @""), bundleIdentifier];
NSMutableDictionary *userInfo = [error.userInfo mutableCopy];
userInfo[NSLocalizedFailureErrorKey] = localizedFailure;
NSError *localizedError = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo];
finish(localizedError);
}
else
{
finish(nil);
}
free(uuidString);
};
instproxy_uninstall(ipc, bundleIdentifier.UTF8String, NULL, ALTDeviceManagerUpdateAppDeletionStatus, uuidString);
}
#pragma mark - Provisioning Profiles -
- (void)installProvisioningProfiles:(NSSet<ALTProvisioningProfile *> *)provisioningProfiles toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(nullable NSSet<NSString *> *)activeProvisioningProfiles completionHandler:(void (^)(BOOL success, NSError *error))completionHandler
@@ -1026,13 +1127,51 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT
continue;
}
plist_t device_type_plist = NULL;
if (lockdownd_get_value(client, NULL, "ProductType", &device_type_plist) != LOCKDOWN_E_SUCCESS)
{
fprintf(stderr, "ERROR: Could not get device type for %s!\n", device_name);
lockdownd_client_free(client);
idevice_free(device);
continue;
}
ALTDeviceType deviceType = ALTDeviceTypeiPhone;
char *device_type_string = NULL;
plist_get_string_val(device_type_plist, &device_type_string);
if ([@(device_type_string) hasPrefix:@"iPhone"])
{
deviceType = ALTDeviceTypeiPhone;
}
else if ([@(device_type_string) hasPrefix:@"iPad"])
{
deviceType = ALTDeviceTypeiPad;
}
else if ([@(device_type_string) hasPrefix:@"AppleTV"])
{
deviceType = ALTDeviceTypeAppleTV;
}
else
{
fprintf(stderr, "ERROR: Unknown device type %s for %s!\n", device_type_string, device_name);
lockdownd_client_free(client);
idevice_free(device);
continue;
}
lockdownd_client_free(client);
idevice_free(device);
NSString *name = [NSString stringWithCString:device_name encoding:NSUTF8StringEncoding];
NSString *identifier = [NSString stringWithCString:udid encoding:NSUTF8StringEncoding];
ALTDevice *altDevice = [[ALTDevice alloc] initWithName:name identifier:identifier];
ALTDevice *altDevice = [[ALTDevice alloc] initWithName:name identifier:identifier type:deviceType];
[connectedDevices addObject:altDevice];
if (device_name != NULL)
@@ -1075,7 +1214,7 @@ void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *uuid)
{
if (code != 0 || name != NULL)
{
NSLog(@"Error installing app. %@ (%@). %@", @(code), @(name), @(description));
NSLog(@"Error installing app. %@ (%@). %@", @(code), @(name ?: ""), @(description ?: ""));
NSError *error = nil;
@@ -1085,14 +1224,14 @@ void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *uuid)
}
else
{
NSString *errorName = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
NSString *errorName = [NSString stringWithCString:name ?: "" encoding:NSUTF8StringEncoding];
if ([errorName isEqualToString:@"DeviceOSVersionTooLow"])
{
error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorUnsupportediOSVersion userInfo:nil];
}
else
{
NSError *underlyingError = [NSError errorWithDomain:AltServerInstallationErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: @(description)}];
NSError *underlyingError = [NSError errorWithDomain:AltServerInstallationErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: @(description ?: "")}];
error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorInstallationFailed userInfo:@{NSUnderlyingErrorKey: underlyingError}];
}
}
@@ -1117,6 +1256,43 @@ void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *uuid)
}
}
void ALTDeviceManagerUpdateAppDeletionStatus(plist_t command, plist_t status, void *uuid)
{
NSUUID *UUID = [[NSUUID alloc] initWithUUIDString:[NSString stringWithUTF8String:(const char *)uuid]];
char *statusName = NULL;
instproxy_status_get_name(status, &statusName);
char *errorName = NULL;
char *errorDescription = NULL;
uint64_t code = 0;
instproxy_status_get_error(status, &errorName, &errorDescription, &code);
if ([@(statusName) isEqualToString:@"Complete"] || code != 0 || errorName != NULL)
{
void (^completionHandler)(NSError *) = ALTDeviceManager.sharedManager.deletionCompletionHandlers[UUID];
if (completionHandler != nil)
{
if (code != 0 || errorName != NULL)
{
NSLog(@"Error removing app. %@ (%@). %@", @(code), @(errorName ?: ""), @(errorDescription ?: ""));
NSError *underlyingError = [NSError errorWithDomain:AltServerInstallationErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: @(errorDescription ?: "")}];
NSError *error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorAppDeletionFailed userInfo:@{NSUnderlyingErrorKey: underlyingError}];
completionHandler(error);
}
else
{
NSLog(@"Finished removing app!");
completionHandler(nil);
}
ALTDeviceManager.sharedManager.deletionCompletionHandlers[UUID] = nil;
}
}
}
void ALTDeviceDidChangeConnectionStatus(const idevice_event_t *event, void *user_data)
{
ALTDevice * (^deviceForUDID)(NSString *, NSArray<ALTDevice *> *) = ^ALTDevice *(NSString *udid, NSArray<ALTDevice *> *devices) {

View File

@@ -0,0 +1,310 @@
//
// PluginManager.swift
// AltServer
//
// Created by Riley Testut on 9/16/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AppKit
import CryptoKit
import STPrivilegedTask
private let pluginDirectoryURL = URL(fileURLWithPath: "/Library/Mail/Bundles", isDirectory: true)
private let pluginURL = pluginDirectoryURL.appendingPathComponent("AltPlugin.mailbundle")
enum PluginError: LocalizedError
{
case cancelled
case unknown
case notFound
case mismatchedHash(hash: String, expectedHash: String)
case taskError(String)
case taskErrorCode(Int)
var errorDescription: String? {
switch self
{
case .cancelled: return NSLocalizedString("Mail plug-in installation was cancelled.", comment: "")
case .unknown: return NSLocalizedString("Failed to install Mail plug-in.", comment: "")
case .notFound: return NSLocalizedString("The Mail plug-in does not exist at the requested URL.", comment: "")
case .mismatchedHash(let hash, let expectedHash): return String(format: NSLocalizedString("The hash of the downloaded Mail plug-in does not match the expected hash.\n\nHash:\n%@\n\nExpected Hash:\n%@", comment: ""), hash, expectedHash)
case .taskError(let output): return output
case .taskErrorCode(let errorCode): return String(format: NSLocalizedString("There was an error installing the Mail plug-in. (Error Code: %@)", comment: ""), NSNumber(value: errorCode))
}
}
}
struct PluginVersion
{
var url: URL
var sha256Hash: String
var version: String
static let v1_0 = PluginVersion(url: URL(string: "https://f000.backblazeb2.com/file/altstore/altserver/altplugin/1_0.zip")!,
sha256Hash: "070e9b7e1f74e7a6474d36253ab5a3623ff93892acc9e1043c3581f2ded12200",
version: "1.0")
static let v1_3 = PluginVersion(url: Bundle.main.url(forResource: "AltPlugin", withExtension: "zip")!,
sha256Hash: "6c939d6601ea9793f149e4f6dd4a154e8229a9b9cf7f4bea4a1d6bca7d433512",
version: "1.3")
}
class PluginManager
{
var isMailPluginInstalled: Bool {
let isMailPluginInstalled = FileManager.default.fileExists(atPath: pluginURL.path)
return isMailPluginInstalled
}
var isUpdateAvailable: Bool {
guard let bundle = Bundle(url: pluginURL) else { return false }
// Load Info.plist from disk because Bundle.infoDictionary is cached by system.
let infoDictionaryURL = bundle.bundleURL.appendingPathComponent("Contents/Info.plist")
guard let infoDictionary = NSDictionary(contentsOf: infoDictionaryURL) as? [String: Any],
let version = infoDictionary["CFBundleShortVersionString"] as? String
else { return false }
let isUpdateAvailable = (version != self.preferredVersion.version)
return isUpdateAvailable
}
private var preferredVersion: PluginVersion {
if #available(macOS 11, *)
{
return .v1_3
}
else
{
return .v1_0
}
}
}
extension PluginManager
{
func installMailPlugin(completionHandler: @escaping (Result<Void, Error>) -> Void)
{
do
{
let alert = NSAlert()
if self.isUpdateAvailable
{
alert.messageText = NSLocalizedString("Update Mail Plug-in", comment: "")
alert.informativeText = NSLocalizedString("An update is available for AltServer's Mail plug-in. Please update the plug-in now in order to keep using AltStore.", comment: "")
alert.addButton(withTitle: NSLocalizedString("Update Plug-in", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
}
else
{
alert.messageText = NSLocalizedString("Install Mail Plug-in", comment: "")
alert.informativeText = NSLocalizedString("AltServer requires a Mail plug-in in order to retrieve necessary information about your Apple ID. Would you like to install it now?", comment: "")
alert.addButton(withTitle: NSLocalizedString("Install Plug-in", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
}
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
let response = alert.runModal()
guard response == .alertFirstButtonReturn else { throw PluginError.cancelled }
self.downloadPlugin { (result) in
do
{
let fileURL = try result.get()
// Ensure plug-in directory exists.
let authorization = try self.runAndKeepAuthorization("mkdir", arguments: ["-p", pluginDirectoryURL.path])
// Create temporary directory.
let temporaryDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil)
defer { try? FileManager.default.removeItem(at: temporaryDirectoryURL) }
// Unzip AltPlugin to temporary directory.
try self.runAndKeepAuthorization("unzip", arguments: ["-o", fileURL.path, "-d", temporaryDirectoryURL.path], authorization: authorization)
if FileManager.default.fileExists(atPath: pluginURL.path)
{
// Delete existing Mail plug-in.
try self.runAndKeepAuthorization("rm", arguments: ["-rf", pluginURL.path], authorization: authorization)
}
// Copy AltPlugin to Mail plug-ins directory.
// Must be separate step than unzip to prevent macOS from considering plug-in corrupted.
let unzippedPluginURL = temporaryDirectoryURL.appendingPathComponent(pluginURL.lastPathComponent)
try self.runAndKeepAuthorization("cp", arguments: ["-R", unzippedPluginURL.path, pluginDirectoryURL.path], authorization: authorization)
guard self.isMailPluginInstalled else { throw PluginError.unknown }
// Enable Mail plug-in preferences.
try self.run("defaults", arguments: ["write", "/Library/Preferences/com.apple.mail", "EnableBundles", "-bool", "YES"], authorization: authorization)
print("Finished installing Mail plug-in!")
completionHandler(.success(()))
}
catch
{
completionHandler(.failure(error))
}
}
}
catch
{
completionHandler(.failure(PluginError.cancelled))
}
}
func uninstallMailPlugin(completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let alert = NSAlert()
alert.messageText = NSLocalizedString("Uninstall Mail Plug-in", comment: "")
alert.informativeText = NSLocalizedString("Are you sure you want to uninstall the AltServer Mail plug-in? You will no longer be able to install or refresh apps with AltStore.", comment: "")
alert.addButton(withTitle: NSLocalizedString("Uninstall Plug-in", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
NSRunningApplication.current.activate(options: .activateIgnoringOtherApps)
let response = alert.runModal()
guard response == .alertFirstButtonReturn else { return completionHandler(.failure(PluginError.cancelled)) }
DispatchQueue.global().async {
do
{
if FileManager.default.fileExists(atPath: pluginURL.path)
{
// Delete Mail plug-in from privileged directory.
try self.run("rm", arguments: ["-rf", pluginURL.path])
}
completionHandler(.success(()))
}
catch
{
completionHandler(.failure(error))
}
}
}
}
private extension PluginManager
{
func downloadPlugin(completion: @escaping (Result<URL, Error>) -> Void)
{
let pluginVersion = self.preferredVersion
func finish(_ result: Result<URL, Error>)
{
do
{
let fileURL = try result.get()
if #available(OSX 10.15, *)
{
let data = try Data(contentsOf: fileURL)
let sha256Hash = SHA256.hash(data: data)
let hashString = sha256Hash.compactMap { String(format: "%02x", $0) }.joined()
print("Comparing Mail plug-in hash (\(hashString)) against expected hash (\(pluginVersion.sha256Hash))...")
guard hashString == pluginVersion.sha256Hash else { throw PluginError.mismatchedHash(hash: hashString, expectedHash: pluginVersion.sha256Hash) }
}
completion(.success(fileURL))
}
catch
{
completion(.failure(error))
}
}
if pluginVersion.url.isFileURL
{
finish(.success(pluginVersion.url))
}
else
{
let downloadTask = URLSession.shared.downloadTask(with: pluginVersion.url) { (fileURL, response, error) in
if let response = response as? HTTPURLResponse
{
guard response.statusCode != 404 else { return finish(.failure(PluginError.notFound)) }
}
let result = Result(fileURL, error)
finish(result)
if let fileURL = fileURL
{
try? FileManager.default.removeItem(at: fileURL)
}
}
downloadTask.resume()
}
}
func run(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil) throws
{
_ = try self._run(program, arguments: arguments, authorization: authorization, freeAuthorization: true)
}
@discardableResult
func runAndKeepAuthorization(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil) throws -> AuthorizationRef
{
return try self._run(program, arguments: arguments, authorization: authorization, freeAuthorization: false)
}
func _run(_ program: String, arguments: [String], authorization: AuthorizationRef? = nil, freeAuthorization: Bool) throws -> AuthorizationRef
{
var launchPath = "/usr/bin/" + program
if !FileManager.default.fileExists(atPath: launchPath)
{
launchPath = "/bin/" + program
}
print("Running program:", launchPath)
let task = STPrivilegedTask()
task.launchPath = launchPath
task.arguments = arguments
task.freeAuthorizationWhenDone = freeAuthorization
let errorCode: OSStatus
if let authorization = authorization
{
errorCode = task.launch(withAuthorization: authorization)
}
else
{
errorCode = task.launch()
}
guard errorCode == 0 else { throw PluginError.taskErrorCode(Int(errorCode)) }
task.waitUntilExit()
print("Exit code:", task.terminationStatus)
guard task.terminationStatus == 0 else {
let outputData = task.outputFileHandle.readDataToEndOfFile()
if let outputString = String(data: outputData, encoding: .utf8), !outputString.isEmpty
{
throw PluginError.taskError(outputString)
}
throw PluginError.taskErrorCode(Int(task.terminationStatus))
}
guard let authorization = task.authorization else { throw PluginError.unknown }
return authorization
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1150"
version = "1.3">
<BuildAction
parallelizeBuildables = "NO"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A7A6DC28A6D60809855FE404C6A3EA29"
BuildableName = "libPods-AltDaemon.a"
BlueprintName = "Pods-AltDaemon"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF1E314F22A0616100370A3C"
BuildableName = "libAltKit.a"
BlueprintName = "AltKit"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
BuildableName = "AltDaemon"
BlueprintName = "AltDaemon"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
BuildableName = "AltDaemon"
BlueprintName = "AltDaemon"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</MacroExpansion>
<EnvironmentVariables>
<EnvironmentVariable
key = "THEOS"
value = "~/theos"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF18BFE624857D7900DD5981"
BuildableName = "AltDaemon"
BlueprintName = "AltDaemon"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -29,17 +29,6 @@
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BFD247692284B9A500981D42"
BuildableName = "AltStore.app"
BlueprintName = "AltStore"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@@ -67,8 +56,6 @@
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1230"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BFF7C903257844C900E55F36"
BuildableName = "AltXPC.xpc"
BlueprintName = "AltXPC"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BF45868C229872EA00BD7491"
BuildableName = "AltServer.app"
BlueprintName = "AltServer"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BFF7C903257844C900E55F36"
BuildableName = "AltXPC.xpc"
BlueprintName = "AltXPC"
ReferencedContainer = "container:AltStore.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -5,7 +5,7 @@
location = "container:AltStore.xcodeproj">
</FileRef>
<FileRef
location = "group:Dependencies/AltSign/AltSign.xcodeproj">
location = "group:Dependencies/AltSign">
</FileRef>
<FileRef
location = "group:Dependencies/Roxas/Roxas.xcodeproj">

View File

@@ -2,10 +2,4 @@
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import "AltKit.h"
#import "ALTAppPermission.h"
#import "ALTPatreonBenefitType.h"
#import "ALTSourceUserInfoKey.h"
#import "NSAttributedString+Markdown.h"

View File

@@ -4,5 +4,11 @@
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.siri</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.rileytestut.AltStore</string>
</array>
</dict>
</plist>

View File

@@ -8,10 +8,20 @@
import Foundation
import AltStoreCore
import AppCenter
import AppCenterAnalytics
import AppCenterCrashes
#if DEBUG
private let appCenterAppSecret = "bb08e9bb-c126-408d-bf3f-324c8473fd40"
#elseif RELEASE
private let appCenterAppSecret = "b6718932-294a-432b-81f2-be1e17ff85c5"
#else
private let appCenterAppSecret = "e873f6ca-75eb-4685-818f-801e0e375d60"
#endif
extension AnalyticsManager
{
enum EventProperty: String
@@ -80,7 +90,7 @@ extension AnalyticsManager
{
func start()
{
MSAppCenter.start("bb08e9bb-c126-408d-bf3f-324c8473fd40", withServices:[
MSAppCenter.start(appCenterAppSecret, withServices:[
MSAnalytics.self,
MSCrashes.self
])

View File

@@ -8,6 +8,7 @@
import UIKit
import AltStoreCore
import Roxas
import Nuke

View File

@@ -8,6 +8,7 @@
import UIKit
import AltStoreCore
import Roxas
import Nuke
@@ -81,14 +82,14 @@ class AppViewController: UIViewController
self.bannerView.frame = CGRect(x: 0, y: 0, width: 300, height: 93)
self.bannerView.backgroundEffectView.effect = UIBlurEffect(style: .regular)
self.bannerView.backgroundEffectView.backgroundColor = .clear
self.bannerView.titleLabel.text = self.app.name
self.bannerView.subtitleLabel.text = self.app.developerName
self.bannerView.iconImageView.image = nil
self.bannerView.iconImageView.tintColor = self.app.tintColor
self.bannerView.button.tintColor = self.app.tintColor
self.bannerView.betaBadgeView.isHidden = !self.app.isBeta
self.bannerView.tintColor = self.app.tintColor
self.bannerView.configure(for: self.app)
self.bannerView.accessibilityTraits.remove(.button)
self.bannerView.button.addTarget(self, action: #selector(AppViewController.performAppAction(_:)), for: .primaryActionTriggered)
self.backButtonContainerView.tintColor = self.app.tintColor

View File

@@ -8,6 +8,8 @@
import UIKit
import AltStoreCore
class PermissionPopoverViewController: UIViewController
{
var permission: AppPermission!

View File

@@ -8,6 +8,7 @@
import UIKit
import AltStoreCore
import Roxas
class AppIDsViewController: UICollectionViewController
@@ -78,10 +79,11 @@ private extension AppIDsViewController
cell.bannerView.iconImageView.isHidden = true
cell.bannerView.button.isIndicatingActivity = false
cell.bannerView.betaBadgeView.isHidden = true
cell.bannerView.buttonLabel.text = NSLocalizedString("Expires in", comment: "")
let attributedAccessibilityLabel = NSMutableAttributedString(string: appID.name + ". ")
if let expirationDate = appID.expirationDate
{
cell.bannerView.button.isHidden = false
@@ -92,19 +94,16 @@ private extension AppIDsViewController
let currentDate = Date()
let numberOfDays = expirationDate.numberOfCalendarDays(since: currentDate)
let numberOfDaysText = (numberOfDays == 1) ? NSLocalizedString("1 day", comment: "") : String(format: NSLocalizedString("%@ days", comment: ""), NSNumber(value: numberOfDays))
cell.bannerView.button.setTitle(numberOfDaysText.uppercased(), for: .normal)
if numberOfDays == 1
{
cell.bannerView.button.setTitle(NSLocalizedString("1 DAY", comment: ""), for: .normal)
}
else
{
cell.bannerView.button.setTitle(String(format: NSLocalizedString("%@ DAYS", comment: ""), NSNumber(value: numberOfDays)), for: .normal)
}
attributedAccessibilityLabel.mutableString.append(String(format: NSLocalizedString("Expires in %@.", comment: ""), numberOfDaysText) + " ")
}
else
{
cell.bannerView.button.isHidden = true
cell.bannerView.button.isUserInteractionEnabled = true
cell.bannerView.buttonLabel.isHidden = true
}
@@ -112,6 +111,18 @@ private extension AppIDsViewController
cell.bannerView.subtitleLabel.text = appID.bundleIdentifier
cell.bannerView.subtitleLabel.numberOfLines = 2
let attributedBundleIdentifier = NSMutableAttributedString(string: appID.bundleIdentifier.lowercased(), attributes: [.accessibilitySpeechPunctuation: true])
if let team = appID.team, let range = attributedBundleIdentifier.string.range(of: team.identifier.lowercased()), #available(iOS 13, *)
{
// Prefer to speak the team ID one character at a time.
let nsRange = NSRange(range, in: attributedBundleIdentifier.string)
attributedBundleIdentifier.addAttributes([.accessibilitySpeechSpellOut: true], range: nsRange)
}
attributedAccessibilityLabel.append(attributedBundleIdentifier)
cell.bannerView.accessibilityAttributedLabel = attributedAccessibilityLabel
// Make sure refresh button is correct size.
cell.layoutIfNeeded()
}

View File

@@ -9,53 +9,23 @@
import UIKit
import UserNotifications
import AVFoundation
import Intents
import AltStoreCore
import AltSign
import AltKit
import Roxas
private enum RefreshError: LocalizedError
{
case noInstalledApps
var errorDescription: String? {
switch self
{
case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "")
}
}
}
private extension CFNotificationName
{
static let requestAppState = CFNotificationName("com.altstore.RequestAppState" as CFString)
static let appIsRunning = CFNotificationName("com.altstore.AppState.Running" as CFString)
static func requestAppState(for appID: String) -> CFNotificationName
{
let name = String(CFNotificationName.requestAppState.rawValue) + "." + appID
return CFNotificationName(name as CFString)
}
static func appIsRunning(for appID: String) -> CFNotificationName
{
let name = String(CFNotificationName.appIsRunning.rawValue) + "." + appID
return CFNotificationName(name as CFString)
}
}
private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =
{ (center, observer, name, object, userInfo) in
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, let name = name else { return }
appDelegate.receivedApplicationState(notification: name)
}
extension AppDelegate
{
static let openPatreonSettingsDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.OpenPatreonSettingsDeepLinkNotification")
static let importAppDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.ImportAppDeepLinkNotification")
static let addSourceDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.AddSourceDeepLinkNotification")
static let appBackupDidFinish = Notification.Name("com.rileytestut.AltStore.AppBackupDidFinish")
static let importAppDeepLinkURLKey = "fileURL"
static let appBackupResultKey = "result"
static let addSourceDeepLinkURLKey = "sourceURL"
}
@UIApplicationMain
@@ -63,18 +33,35 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
private var runningApplications: Set<String>?
private var backgroundRefreshContext: NSManagedObjectContext? // Keep context alive until finished refreshing.
@available(iOS 14, *)
private lazy var intentHandler = IntentHandler()
@available(iOS 14, *)
private lazy var viewAppIntentHandler = ViewAppIntentHandler()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
// Register default settings before doing anything else.
UserDefaults.registerDefaults()
DatabaseManager.shared.start { (error) in
if let error = error
{
print("Failed to start DatabaseManager. Error:", error as Any)
}
else
{
print("Started DatabaseManager.")
}
}
AnalyticsManager.shared.start()
self.setTintColor()
ServerManager.shared.startDiscovering()
UserDefaults.standard.registerDefaults()
SecureValueTransformer.register()
if UserDefaults.standard.firstLaunch == nil
{
@@ -110,6 +97,36 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
{
return self.open(url)
}
func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any?
{
guard #available(iOS 14, *) else { return nil }
switch intent
{
case is RefreshAllIntent: return self.intentHandler
case is ViewAppIntent: return self.viewAppIntentHandler
default: return nil
}
}
}
@available(iOS 13, *)
extension AppDelegate
{
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration
{
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>)
{
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}
private extension AppDelegate
@@ -134,13 +151,63 @@ private extension AppDelegate
else
{
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
guard let host = components.host, host.lowercased() == "patreon" else { return false }
guard let host = components.host?.lowercased() else { return false }
DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
switch host
{
case "patreon":
DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
}
return true
case "appbackupresponse":
let result: Result<Void, Error>
switch url.path.lowercased()
{
case "/success": result = .success(())
case "/failure":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name] = $1.value } ?? [:]
guard
let errorDomain = queryItems["errorDomain"],
let errorCodeString = queryItems["errorCode"], let errorCode = Int(errorCodeString),
let errorDescription = queryItems["errorDescription"]
else { return false }
let error = NSError(domain: errorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: errorDescription])
result = .failure(error)
default: return false
}
NotificationCenter.default.post(name: AppDelegate.appBackupDidFinish, object: nil, userInfo: [AppDelegate.appBackupResultKey: result])
return true
case "install":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
guard let downloadURLString = queryItems["url"], let downloadURL = URL(string: downloadURLString) else { return false }
DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: downloadURL])
}
return true
case "source":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
guard let sourceURLString = queryItems["url"], let sourceURL = URL(string: sourceURLString) else { return false }
DispatchQueue.main.async {
NotificationCenter.default.post(name: AppDelegate.addSourceDeepLinkNotification, object: nil, userInfo: [AppDelegate.addSourceDeepLinkURLKey: sourceURL])
}
return true
default: return false
}
return true
}
}
}
@@ -177,95 +244,92 @@ extension AppDelegate
func application(_ application: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void)
{
if UserDefaults.standard.isBackgroundRefreshEnabled
if UserDefaults.standard.isBackgroundRefreshEnabled && !UserDefaults.standard.presentedLaunchReminderNotification
{
ServerManager.shared.startDiscovering()
let threeHours: TimeInterval = 3 * 60 * 60
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false)
if !UserDefaults.standard.presentedLaunchReminderNotification
{
let threeHours: TimeInterval = 3 * 60 * 60
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false)
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("App Refresh Tip", comment: "")
content.body = NSLocalizedString("The more you open AltStore, the more chances it's given to refresh apps in the background.", comment: "")
let request = UNNotificationRequest(identifier: "background-refresh-reminder5", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
UserDefaults.standard.presentedLaunchReminderNotification = true
}
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("App Refresh Tip", comment: "")
content.body = NSLocalizedString("The more you open AltStore, the more chances it's given to refresh apps in the background.", comment: "")
let request = UNNotificationRequest(identifier: "background-refresh-reminder5", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
UserDefaults.standard.presentedLaunchReminderNotification = true
}
let refreshIdentifier = UUID().uuidString
BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in
func finish(_ result: Result<[String: Result<InstalledApp, Error>], Error>)
{
// If finish is actually called, that means an error occured during installation.
if UserDefaults.standard.isBackgroundRefreshEnabled
{
ServerManager.shared.stopDiscovering()
self.scheduleFinishedRefreshingNotification(for: result, identifier: refreshIdentifier, delay: 0)
}
taskCompletionHandler()
self.backgroundRefreshContext = nil
}
if let error = taskResult.error
{
print("Error starting extended background task. Aborting.", error)
backgroundFetchCompletionHandler(.failed)
finish(.failure(error))
taskCompletionHandler()
return
}
if !DatabaseManager.shared.isStarted
{
DatabaseManager.shared.start() { (error) in
if let error = error
if error != nil
{
backgroundFetchCompletionHandler(.failed)
finish(.failure(error))
taskCompletionHandler()
}
else
{
self.refreshApps(identifier: refreshIdentifier, backgroundFetchCompletionHandler: backgroundFetchCompletionHandler, completionHandler: finish(_:))
self.performBackgroundFetch { (backgroundFetchResult) in
backgroundFetchCompletionHandler(backgroundFetchResult)
} refreshAppsCompletionHandler: { (refreshAppsResult) in
taskCompletionHandler()
}
}
}
}
else
{
self.refreshApps(identifier: refreshIdentifier, backgroundFetchCompletionHandler: backgroundFetchCompletionHandler, completionHandler: finish(_:))
self.performBackgroundFetch { (backgroundFetchResult) in
backgroundFetchCompletionHandler(backgroundFetchResult)
} refreshAppsCompletionHandler: { (refreshAppsResult) in
taskCompletionHandler()
}
}
}
}
func performBackgroundFetch(backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
refreshAppsCompletionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
{
self.fetchSources { (result) in
switch result
{
case .failure: backgroundFetchCompletionHandler(.failed)
case .success: backgroundFetchCompletionHandler(.newData)
}
if !UserDefaults.standard.isBackgroundRefreshEnabled
{
refreshAppsCompletionHandler(.success([:]))
}
}
guard UserDefaults.standard.isBackgroundRefreshEnabled else { return }
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
AppManager.shared.backgroundRefresh(installedApps, completionHandler: refreshAppsCompletionHandler)
}
}
}
private extension AppDelegate
{
func refreshApps(identifier: String,
backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
func fetchSources(completionHandler: @escaping (Result<Set<Source>, Error>) -> Void)
{
var fetchSourcesResult: Result<Set<Source>, Error>?
var serversResult: Result<Void, Error>?
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
AppManager.shared.fetchSources() { (result) in
fetchSourcesResult = result
do
{
let sources = try result.get()
guard let context = sources.first?.managedObjectContext else { return }
let (sources, context) = try result.get()
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
previousUpdatesFetchRequest.includesPendingChanges = false
@@ -328,223 +392,14 @@ private extension AppDelegate
DispatchQueue.main.async {
UIApplication.shared.applicationIconBadgeNumber = updates.count
}
completionHandler(.success(sources))
}
catch
{
print("Error fetching apps:", error)
fetchSourcesResult = .failure(error)
}
dispatchGroup.leave()
}
if UserDefaults.standard.isBackgroundRefreshEnabled
{
dispatchGroup.enter()
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
guard !installedApps.isEmpty else {
serversResult = .success(())
dispatchGroup.leave()
completionHandler(.failure(RefreshError.noInstalledApps))
return
}
self.runningApplications = []
self.backgroundRefreshContext = context
let identifiers = installedApps.compactMap { $0.bundleIdentifier }
print("Apps to refresh:", identifiers)
DispatchQueue.global().async {
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
for identifier in identifiers
{
let appIsRunningNotification = CFNotificationName.appIsRunning(for: identifier)
CFNotificationCenterAddObserver(notificationCenter, nil, ReceivedApplicationState, appIsRunningNotification.rawValue, nil, .deliverImmediately)
let requestAppStateNotification = CFNotificationName.requestAppState(for: identifier)
CFNotificationCenterPostNotification(notificationCenter, requestAppStateNotification, nil, nil, true)
}
}
// Wait for three seconds to:
// a) give us time to discover AltServers
// b) give other processes a chance to respond to requestAppState notification
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
context.perform {
if ServerManager.shared.discoveredServers.isEmpty
{
serversResult = .failure(ConnectionError.serverNotFound)
}
else
{
serversResult = .success(())
}
dispatchGroup.leave()
let filteredApps = installedApps.filter { !(self.runningApplications?.contains($0.bundleIdentifier) ?? false) }
print("Filtered Apps to Refresh:", filteredApps.map { $0.bundleIdentifier })
let group = AppManager.shared.refresh(filteredApps, presentingViewController: nil)
group.beginInstallationHandler = { (installedApp) in
guard installedApp.bundleIdentifier == StoreApp.altstoreAppID else { return }
// We're starting to install AltStore, which means the app is about to quit.
// So, we schedule a "refresh successful" local notification to be displayed after a delay,
// but if the app is still running, we cancel the notification.
// Then, we schedule another notification and repeat the process.
// Also since AltServer has already received the app, it can finish installing even if we're no longer running in background.
if let error = group.context.error
{
self.scheduleFinishedRefreshingNotification(for: .failure(error), identifier: identifier)
}
else
{
var results = group.results
results[installedApp.bundleIdentifier] = .success(installedApp)
self.scheduleFinishedRefreshingNotification(for: .success(results), identifier: identifier)
}
}
group.completionHandler = { (results) in
completionHandler(.success(results))
}
}
}
completionHandler(.failure(error))
}
}
dispatchGroup.notify(queue: .main) {
if !UserDefaults.standard.isBackgroundRefreshEnabled
{
guard let fetchSourcesResult = fetchSourcesResult else {
backgroundFetchCompletionHandler(.failed)
return
}
switch fetchSourcesResult
{
case .failure: backgroundFetchCompletionHandler(.failed)
case .success: backgroundFetchCompletionHandler(.newData)
}
completionHandler(.success([:]))
}
else
{
guard let fetchSourcesResult = fetchSourcesResult, let serversResult = serversResult else {
backgroundFetchCompletionHandler(.failed)
return
}
// Call completionHandler early to improve chances of refreshing in the background again.
switch (fetchSourcesResult, serversResult)
{
case (.success, .success): backgroundFetchCompletionHandler(.newData)
case (.success, .failure(ConnectionError.serverNotFound)): backgroundFetchCompletionHandler(.newData)
case (.failure, _), (_, .failure): backgroundFetchCompletionHandler(.failed)
}
}
}
}
func receivedApplicationState(notification: CFNotificationName)
{
let baseName = String(CFNotificationName.appIsRunning.rawValue)
let appID = String(notification.rawValue).replacingOccurrences(of: baseName + ".", with: "")
self.runningApplications?.insert(appID)
}
func scheduleFinishedRefreshingNotification(for result: Result<[String: Result<InstalledApp, Error>], Error>, identifier: String, delay: TimeInterval = 5)
{
func scheduleFinishedRefreshingNotification()
{
self.cancelFinishedRefreshingNotification(identifier: identifier)
let content = UNMutableNotificationContent()
var shouldPresentAlert = true
do
{
let results = try result.get()
shouldPresentAlert = !results.isEmpty
for (_, result) in results
{
guard case let .failure(error) = result else { continue }
throw error
}
content.title = NSLocalizedString("Refreshed Apps", comment: "")
content.body = NSLocalizedString("All apps have been refreshed.", comment: "")
}
catch ConnectionError.serverNotFound
{
shouldPresentAlert = false
}
catch RefreshError.noInstalledApps
{
shouldPresentAlert = false
}
catch
{
print("Failed to refresh apps in background.", error)
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
content.body = error.localizedDescription
shouldPresentAlert = true
}
if shouldPresentAlert
{
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay + 1, repeats: false)
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
if delay > 0
{
DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
UNUserNotificationCenter.current().getPendingNotificationRequests() { (requests) in
// If app is still running at this point, we schedule another notification with same identifier.
// This prevents the currently scheduled notification from displaying, and starts another countdown timer.
// First though, make sure there _is_ still a pending request, otherwise it's been cancelled
// and we should stop polling.
guard requests.contains(where: { $0.identifier == identifier }) else { return }
scheduleFinishedRefreshingNotification()
}
}
}
}
}
scheduleFinishedRefreshingNotification()
// Perform synchronously to ensure app doesn't quit before we've finishing saving to disk.
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
context.performAndWait {
_ = RefreshAttempt(identifier: identifier, result: result, context: context)
do { try context.save() }
catch { print("Failed to save refresh attempt.", error) }
}
}
func cancelFinishedRefreshingNotification(identifier: String)
{
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [identifier])
}
}

View File

@@ -7,8 +7,9 @@
//
import UIKit
import AltSign
import AltStoreCore
import AltSign
import Roxas
class RefreshAltStoreViewController: UIViewController

View File

@@ -1,11 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16092.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17503.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16082.1"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17502"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@@ -16,8 +17,8 @@
<view key="view" contentMode="scaleToFill" id="G9E-Qs-gFM">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<viewLayoutGuide key="safeArea" id="sZd-sc-Bvn"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="vOq-mm-rY5" userLabel="First Responder" sceneMemberID="firstResponder"/>
@@ -38,6 +39,7 @@
<segue destination="faz-B4-Sub" kind="relationship" relationship="viewControllers" id="dXz-Tu-hW8"/>
<segue destination="3Ew-ox-i4n" kind="relationship" relationship="viewControllers" id="zii-dF-qEt"/>
<segue destination="p3d-dP-Swg" kind="relationship" relationship="viewControllers" id="4Nf-rL-P4c"/>
<segue destination="Qo4-72-Hmr" kind="presentation" identifier="presentSources" id="Qd6-ba-dIo"/>
</connections>
</tabBarController>
<placeholder placeholderIdentifier="IBFirstResponder" id="HuB-VB-40B" sceneMemberID="firstResponder"/>
@@ -71,6 +73,9 @@
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="sourcesBarButtonItem" destination="6Ul-JW-TMT" id="99s-O4-OpX"/>
</connections>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cMN-i4-Bxk" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
@@ -150,7 +155,7 @@
<rect key="frame" x="0.0" y="0.0" width="18" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="mkD-3C-WMV">
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="mkD-3C-WMV">
<rect key="frame" x="0.0" y="0.0" width="18" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<state key="normal" image="Back"/>
@@ -171,6 +176,7 @@
</subviews>
</view>
</subviews>
<viewLayoutGuide key="safeArea" id="wiR-52-nwg"/>
<color key="backgroundColor" name="Background"/>
<constraints>
<constraint firstItem="Ci9-Iw-aR2" firstAttribute="top" secondItem="0cR-li-tCB" secondAttribute="top" id="015-fz-v3B"/>
@@ -182,11 +188,10 @@
<constraint firstAttribute="trailing" secondItem="Qlg-m3-lXg" secondAttribute="trailing" id="UrQ-oK-TKQ"/>
<constraint firstAttribute="bottom" secondItem="Qlg-m3-lXg" secondAttribute="bottom" id="Vf7-Fg-88c"/>
</constraints>
<viewLayoutGuide key="safeArea" id="wiR-52-nwg"/>
</view>
<navigationItem key="navigationItem" largeTitleDisplayMode="never" id="maq-gT-QcS">
<barButtonItem key="rightBarButtonItem" style="plain" id="FLf-DS-F77">
<button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" id="grk-xM-YWA" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" id="grk-xM-YWA" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="287" y="6.5" width="72" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
@@ -334,7 +339,7 @@
<rect key="frame" x="0.0" y="0.0" width="335" height="0.0"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="What's New" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="obM-TM-y2E">
<rect key="frame" x="0.0" y="0.0" width="123.5" height="0.0"/>
<rect key="frame" x="0.0" y="0.0" width="124" height="0.0"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="22"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@@ -425,7 +430,7 @@
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" alignment="center" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="fSx-We-L4W">
<rect key="frame" x="0.0" y="0.0" width="56" height="56"/>
<subviews>
<button opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="79g-9q-mE2">
<button opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="79g-9q-mE2">
<rect key="frame" x="5" y="0.0" width="50" height="50"/>
<constraints>
<constraint firstAttribute="width" constant="50" id="0LZ-4n-COH"/>
@@ -529,14 +534,14 @@ World</string>
<edgeInsets key="layoutMargins" top="0.0" left="0.0" bottom="0.0" right="0.0"/>
</stackView>
</subviews>
<color key="backgroundColor" systemColor="tertiarySystemBackgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<viewLayoutGuide key="safeArea" id="c7x-ee-3HH"/>
<color key="backgroundColor" systemColor="tertiarySystemBackgroundColor"/>
<constraints>
<constraint firstItem="xnC-tS-ZdV" firstAttribute="leading" secondItem="c7x-ee-3HH" secondAttribute="leading" constant="20" id="LO8-Au-SYF"/>
<constraint firstItem="c7x-ee-3HH" firstAttribute="bottom" secondItem="xnC-tS-ZdV" secondAttribute="bottom" constant="10" id="NZ9-iG-E10"/>
<constraint firstItem="c7x-ee-3HH" firstAttribute="trailing" secondItem="xnC-tS-ZdV" secondAttribute="trailing" constant="20" id="ZkD-tb-mBf"/>
<constraint firstItem="xnC-tS-ZdV" firstAttribute="top" secondItem="c7x-ee-3HH" secondAttribute="top" constant="10" id="oKq-9e-DtW"/>
</constraints>
<viewLayoutGuide key="safeArea" id="c7x-ee-3HH"/>
</view>
<connections>
<outlet property="descriptionLabel" destination="ErG-8A-uqY" id="iuN-kE-IEm"/>
@@ -648,9 +653,6 @@ World</string>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="mos-e4-dQ7" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="8" y="0.0" width="359" height="60"/>
<accessibility key="accessibilityConfiguration">
<bool key="isElement" value="YES"/>
</accessibility>
</view>
</subviews>
</view>
@@ -688,7 +690,7 @@ World</string>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="No Updates Available" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="z04-yg-x1t">
<rect key="frame" x="96" y="20" width="167" height="20.5"/>
<rect key="frame" x="96.5" y="20" width="166" height="20.5"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="17"/>
<color key="textColor" name="Primary"/>
<nil key="highlightedColor"/>
@@ -734,16 +736,16 @@ World</string>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="900" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="GFQ-Wy-Qhy">
<rect key="frame" x="138.5" y="0.0" width="98" height="52.5"/>
<rect key="frame" x="139" y="0.0" width="97" height="52.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="5/10 App IDs" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="LLv-8I-6Of">
<rect key="frame" x="0.0" y="0.0" width="98" height="20.5"/>
<rect key="frame" x="0.0" y="0.0" width="97" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NHb-0F-cHZ">
<rect key="frame" x="0.0" y="20.5" width="98" height="32"/>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NHb-0F-cHZ">
<rect key="frame" x="0.0" y="20.5" width="97" height="32"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<state key="normal" title="View App IDs"/>
<connections>
@@ -790,7 +792,7 @@ World</string>
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" dataMode="prototypes" id="v1r-C8-h6h">
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="15" minimumInteritemSpacing="10" id="Wzt-qc-XG8">
<size key="itemSize" width="375" height="80"/>
<size key="headerReferenceSize" width="50" height="60"/>
@@ -850,7 +852,7 @@ World</string>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="10 App IDs" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Zna-7n-kBz">
<rect key="frame" x="146" y="0.0" width="83" height="21"/>
<rect key="frame" x="146.5" y="0.0" width="82" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.5" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
@@ -936,7 +938,7 @@ World</string>
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" dataMode="prototypes" id="S36-hD-vu2">
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="15" minimumInteritemSpacing="10" id="X20-b5-XEP">
<size key="itemSize" width="375" height="80"/>
<size key="headerReferenceSize" width="50" height="200"/>
@@ -1004,7 +1006,7 @@ World</string>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" style="done" systemItem="done" id="NQF-u2-PZv">
<connections>
<segue destination="zjS-Nr-VTw" kind="unwind" unwindAction="unwindToBrowseViewController:" id="VFy-hV-mNV"/>
<segue destination="zjS-Nr-VTw" kind="unwind" unwindAction="unwindFromSourcesViewController:" id="la1-dJ-UhL"/>
</connections>
</barButtonItem>
</navigationItem>
@@ -1034,6 +1036,7 @@ World</string>
</scene>
</scenes>
<inferredMetricsTieBreakers>
<segue reference="de9-NH-aec"/>
<segue reference="cnd-KK-o60"/>
</inferredMetricsTieBreakers>
<color key="tintColor" name="Primary"/>
@@ -1044,13 +1047,19 @@ World</string>
<image name="News" width="19" height="20"/>
<image name="Settings" width="20" height="20"/>
<namedColor name="Background">
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.0040000001899898052" green="0.50199997425079346" blue="0.51800000667572021" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="BlurTint">
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="Primary">
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.0040000001899898052" green="0.50199997425079346" blue="0.51800000667572021" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="tertiarySystemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View File

@@ -8,6 +8,7 @@
import UIKit
import AltStoreCore
import Roxas
import Nuke
@@ -27,6 +28,8 @@ class BrowseViewController: UICollectionViewController
private var cachedItemSizes = [String: CGSize]()
@IBOutlet private var sourcesBarButtonItem: UIBarButtonItem!
override func viewDidLoad()
{
super.viewDidLoad()
@@ -34,9 +37,6 @@ class BrowseViewController: UICollectionViewController
#if BETA
self.dataSource.searchController.searchableKeyPaths = [#keyPath(InstalledApp.name)]
self.navigationItem.searchController = self.dataSource.searchController
#else
// Hide Sources button for public version while in beta.
self.navigationItem.rightBarButtonItem = nil
#endif
self.prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false
@@ -57,9 +57,11 @@ class BrowseViewController: UICollectionViewController
self.fetchSource()
self.updateDataSource()
self.update()
}
@IBAction private func unwindToBrowseViewController(_ segue: UIStoryboardSegue)
@IBAction private func unwindFromSourcesViewController(_ segue: UIStoryboardSegue)
{
self.fetchSource()
}
@@ -85,9 +87,8 @@ private extension BrowseViewController
cell.subtitleLabel.text = app.subtitle
cell.imageURLs = Array(app.screenshotURLs.prefix(2))
cell.bannerView.titleLabel.text = app.name
cell.bannerView.subtitleLabel.text = app.developerName
cell.bannerView.betaBadgeView.isHidden = !app.isBeta
cell.bannerView.configure(for: app)
cell.bannerView.iconImageView.image = nil
cell.bannerView.iconImageView.isIndicatingActivity = true
@@ -104,7 +105,10 @@ private extension BrowseViewController
if app.installedApp == nil
{
cell.bannerView.button.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal)
let buttonTitle = NSLocalizedString("Free", comment: "")
cell.bannerView.button.setTitle(buttonTitle.uppercased(), for: .normal)
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Download %@", comment: ""), app.name)
cell.bannerView.button.accessibilityValue = buttonTitle
let progress = AppManager.shared.installationProgress(for: app)
cell.bannerView.button.progress = progress
@@ -121,6 +125,8 @@ private extension BrowseViewController
else
{
cell.bannerView.button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Open %@", comment: ""), app.name)
cell.bannerView.button.accessibilityValue = nil
cell.bannerView.button.progress = nil
cell.bannerView.button.countdownDate = nil
}
@@ -178,21 +184,28 @@ private extension BrowseViewController
AppManager.shared.fetchSources() { (result) in
do
{
let sources = try result.get()
try sources.first?.managedObjectContext?.save()
DispatchQueue.main.async {
self.loadingState = .finished(.success(()))
do
{
let (_, context) = try result.get()
try context.save()
DispatchQueue.main.async {
self.loadingState = .finished(.success(()))
}
}
catch let error as AppManager.FetchSourcesError
{
try error.managedObjectContext?.save()
throw error
}
}
catch let error as NSError
catch
{
DispatchQueue.main.async {
if self.dataSource.itemCount > 0
{
let error = error.withLocalizedFailure(NSLocalizedString("Failed to Fetch Sources", comment: ""))
let toastView = ToastView(error: error)
toastView.addTarget(nil, action: #selector(TabBarController.presentSources), for: .touchUpInside)
toastView.show(in: self)
}
@@ -229,6 +242,12 @@ private extension BrowseViewController
self.placeholderView.activityIndicatorView.stopAnimating()
}
#if !BETA
// Hide Sources button for public version if there's only 1 source.
let sources = Source.all(in: DatabaseManager.shared.viewContext)
self.navigationItem.rightBarButtonItem = (sources.count > 1) ? self.sourcesBarButtonItem : nil
#endif
}
}

View File

@@ -7,10 +7,37 @@
//
import UIKit
import AltStoreCore
import Roxas
class AppBannerView: RSTNibView
{
override var accessibilityLabel: String? {
get { return self.accessibilityView?.accessibilityLabel }
set { self.accessibilityView?.accessibilityLabel = newValue }
}
override open var accessibilityAttributedLabel: NSAttributedString? {
get { return self.accessibilityView?.accessibilityAttributedLabel }
set { self.accessibilityView?.accessibilityAttributedLabel = newValue }
}
override var accessibilityValue: String? {
get { return self.accessibilityView?.accessibilityValue }
set { self.accessibilityView?.accessibilityValue = newValue }
}
override open var accessibilityAttributedValue: NSAttributedString? {
get { return self.accessibilityView?.accessibilityAttributedValue }
set { self.accessibilityView?.accessibilityAttributedValue = newValue }
}
override open var accessibilityTraits: UIAccessibilityTraits {
get { return self.accessibilityView?.accessibilityTraits ?? [] }
set { self.accessibilityView?.accessibilityTraits = newValue }
}
private var originalTintColor: UIColor?
@IBOutlet var titleLabel: UILabel!
@@ -21,7 +48,33 @@ class AppBannerView: RSTNibView
@IBOutlet var betaBadgeView: UIView!
@IBOutlet var backgroundEffectView: UIVisualEffectView!
@IBOutlet private var vibrancyView: UIVisualEffectView!
@IBOutlet private var accessibilityView: UIView!
override init(frame: CGRect)
{
super.init(frame: frame)
self.initialize()
}
required init?(coder: NSCoder)
{
super.init(coder: coder)
self.initialize()
}
private func initialize()
{
self.accessibilityView.accessibilityTraits.formUnion(.button)
self.isAccessibilityElement = false
self.accessibilityElements = [self.accessibilityView, self.button].compactMap { $0 }
self.betaBadgeView.isHidden = true
}
override func tintColorDidChange()
{
@@ -36,6 +89,48 @@ class AppBannerView: RSTNibView
}
}
extension AppBannerView
{
func configure(for app: AppProtocol)
{
struct AppValues
{
var name: String
var developerName: String? = nil
var isBeta: Bool = false
init(app: AppProtocol)
{
self.name = app.name
guard let storeApp = (app as? StoreApp) ?? (app as? InstalledApp)?.storeApp else { return }
self.developerName = storeApp.developerName
if storeApp.isBeta
{
self.name = String(format: NSLocalizedString("%@ beta", comment: ""), app.name)
self.isBeta = true
}
}
}
let values = AppValues(app: app)
self.titleLabel.text = app.name // Don't use values.name since that already includes "beta".
self.betaBadgeView.isHidden = !values.isBeta
if let developerName = values.developerName
{
self.subtitleLabel.text = developerName
self.accessibilityLabel = String(format: NSLocalizedString("%@ by %@", comment: ""), values.name, developerName)
}
else
{
self.subtitleLabel.text = NSLocalizedString("Sideloaded", comment: "")
self.accessibilityLabel = values.name
}
}
}
private extension AppBannerView
{
func update()

View File

@@ -1,15 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17124"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<connections>
<outlet property="accessibilityView" destination="bJL-Yw-i4u" id="PWe-tw-jDA"/>
<outlet property="backgroundEffectView" destination="rZk-be-tiI" id="fzU-VT-JeW"/>
<outlet property="betaBadgeView" destination="qQl-Ez-zC5" id="6O1-Cx-7qz"/>
<outlet property="button" destination="tVx-3G-dcu" id="joa-AH-syX"/>
@@ -25,6 +27,13 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bJL-Yw-i4u">
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<accessibility key="accessibilityConfiguration">
<bool key="isElement" value="YES"/>
</accessibility>
</view>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rZk-be-tiI">
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="b8k-up-HtI">
@@ -45,20 +54,20 @@
</constraints>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="100" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="caL-vN-Svn">
<rect key="frame" x="85" y="18" width="190" height="52"/>
<rect key="frame" x="85" y="25.5" width="190" height="37.5"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="500" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="1Es-pv-zwd">
<rect key="frame" x="0.0" y="0.0" width="167" height="34"/>
<rect key="frame" x="0.0" y="0.0" width="126" height="19.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" horizontalCompressionResistancePriority="500" text="App Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="mFe-zJ-eva">
<rect key="frame" x="0.0" y="0.0" width="79" height="34"/>
<rect key="frame" x="0.0" y="0.0" width="79" height="19.5"/>
<accessibility key="accessibilityConfiguration" identifier="NameLabel"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="16"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="BetaBadge" translatesAutoresizingMaskIntoConstraints="NO" id="qQl-Ez-zC5">
<rect key="frame" x="85" y="0.0" width="82" height="34"/>
<rect key="frame" x="85" y="0.0" width="41" height="19.5"/>
<accessibility key="accessibilityConfiguration" identifier="Beta Badge">
<accessibilityTraits key="traits" image="YES" notEnabled="YES"/>
<bool key="isElement" value="YES"/>
@@ -67,14 +76,13 @@
</subviews>
</stackView>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fC4-1V-iMn">
<rect key="frame" x="0.0" y="36" width="190" height="16"/>
<rect key="frame" x="0.0" y="21.5" width="190" height="16"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="LQh-pN-ePC">
<rect key="frame" x="0.0" y="0.0" width="190" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="750" text="Developer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="oN5-vu-Dnw">
<rect key="frame" x="0.0" y="0.0" width="190" height="16"/>
<accessibility key="accessibilityConfiguration" label="Developer"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
@@ -99,10 +107,10 @@
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Yd9-jw-faD">
<rect key="frame" x="0.0" y="0.0" width="77" height="0.0"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="10"/>
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="900" horizontalCompressionResistancePriority="900" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tVx-3G-dcu" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="900" horizontalCompressionResistancePriority="900" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tVx-3G-dcu" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="77" height="31"/>
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
@@ -120,12 +128,16 @@
</subviews>
<constraints>
<constraint firstItem="d1T-UD-gWG" firstAttribute="leading" secondItem="FxI-Fh-ll5" secondAttribute="leading" id="5tv-QN-ZWU"/>
<constraint firstAttribute="bottom" secondItem="bJL-Yw-i4u" secondAttribute="bottom" id="FRq-ZD-2rE"/>
<constraint firstItem="rZk-be-tiI" firstAttribute="top" secondItem="FxI-Fh-ll5" secondAttribute="top" id="N6R-B2-Rie"/>
<constraint firstItem="bJL-Yw-i4u" firstAttribute="top" secondItem="FxI-Fh-ll5" secondAttribute="top" id="h6T-q1-YV9"/>
<constraint firstAttribute="trailing" secondItem="d1T-UD-gWG" secondAttribute="trailing" id="mlG-w3-Ly6"/>
<constraint firstAttribute="trailing" secondItem="rZk-be-tiI" secondAttribute="trailing" id="nEy-pm-Fcs"/>
<constraint firstAttribute="bottom" secondItem="d1T-UD-gWG" secondAttribute="bottom" id="nJo-To-LmX"/>
<constraint firstItem="bJL-Yw-i4u" firstAttribute="leading" secondItem="FxI-Fh-ll5" secondAttribute="leading" id="oLt-2z-QoJ"/>
<constraint firstItem="rZk-be-tiI" firstAttribute="leading" secondItem="FxI-Fh-ll5" secondAttribute="leading" id="pGD-Tl-U4c"/>
<constraint firstItem="d1T-UD-gWG" firstAttribute="top" secondItem="FxI-Fh-ll5" secondAttribute="top" id="q2p-0S-Nv5"/>
<constraint firstAttribute="trailing" secondItem="bJL-Yw-i4u" secondAttribute="trailing" id="vwx-P9-dlB"/>
<constraint firstAttribute="bottom" secondItem="rZk-be-tiI" secondAttribute="bottom" id="yk0-pw-joP"/>
</constraints>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
@@ -137,5 +149,8 @@
<namedColor name="BlurTint">
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<systemColor name="secondaryLabelColor">
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>

View File

@@ -10,7 +10,8 @@ import UIKit
class BannerCollectionViewCell: UICollectionViewCell
{
@IBOutlet var bannerView: AppBannerView!
private(set) var errorBadge: UIView?
@IBOutlet private(set) var bannerView: AppBannerView!
override func awakeFromNib()
{
@@ -18,5 +19,36 @@ class BannerCollectionViewCell: UICollectionViewCell
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.contentView.preservesSuperviewLayoutMargins = true
if #available(iOS 13.0, *)
{
let errorBadge = UIView()
errorBadge.translatesAutoresizingMaskIntoConstraints = false
errorBadge.isHidden = true
self.addSubview(errorBadge)
// Solid background to make the X opaque white.
let backgroundView = UIView()
backgroundView.translatesAutoresizingMaskIntoConstraints = false
backgroundView.backgroundColor = .white
errorBadge.addSubview(backgroundView)
let badgeView = UIImageView(image: UIImage(systemName: "exclamationmark.circle.fill"))
badgeView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(scale: .large)
badgeView.tintColor = .systemRed
errorBadge.addSubview(badgeView, pinningEdgesWith: .zero)
NSLayoutConstraint.activate([
errorBadge.centerXAnchor.constraint(equalTo: self.bannerView.trailingAnchor, constant: -5),
errorBadge.centerYAnchor.constraint(equalTo: self.bannerView.topAnchor, constant: 5),
backgroundView.centerXAnchor.constraint(equalTo: badgeView.centerXAnchor),
backgroundView.centerYAnchor.constraint(equalTo: badgeView.centerYAnchor),
backgroundView.widthAnchor.constraint(equalTo: badgeView.widthAnchor, multiplier: 0.5),
backgroundView.heightAnchor.constraint(equalTo: badgeView.heightAnchor, multiplier: 0.5)
])
self.errorBadge = errorBadge
}
}
}

View File

@@ -10,6 +10,14 @@ import UIKit
class PillButton: UIButton
{
override var accessibilityValue: String? {
get {
guard self.progress != nil else { return super.accessibilityValue }
return self.progressView.accessibilityValue
}
set { super.accessibilityValue = newValue }
}
var progress: Progress? {
didSet {
self.progressView.progress = Float(self.progress?.fractionCompleted ?? 0)
@@ -78,6 +86,7 @@ class PillButton: UIButton
super.awakeFromNib()
self.layer.masksToBounds = true
self.accessibilityTraits.formUnion([.updatesFrequently, .button])
self.activityIndicatorView.style = .white
self.activityIndicatorView.isUserInteractionEnabled = false

View File

@@ -8,6 +8,14 @@
import Roxas
import AltStoreCore
extension TimeInterval
{
static let shortToastViewDuration = 4.0
static let longToastViewDuration = 8.0
}
class ToastView: RSTToastView
{
var preferredDuration: TimeInterval
@@ -16,15 +24,17 @@ class ToastView: RSTToastView
{
if detailedText == nil
{
self.preferredDuration = 4.0
self.preferredDuration = .shortToastViewDuration
}
else
{
self.preferredDuration = 8.0
self.preferredDuration = .longToastViewDuration
}
super.init(text: text, detailText: detailedText)
self.isAccessibilityElement = true
self.layoutMargins = UIEdgeInsets(top: 8, left: 16, bottom: 10, right: 16)
self.setNeedsLayout()
@@ -38,7 +48,22 @@ class ToastView: RSTToastView
convenience init(error: Error)
{
let error = error as NSError
var error = error as NSError
var underlyingError = error.userInfo[NSUnderlyingErrorKey] as? NSError
var preferredDuration: TimeInterval?
if
let unwrappedUnderlyingError = underlyingError,
error.domain == AltServerErrorDomain && error.code == ALTServerError.Code.underlyingError.rawValue
{
// Treat underlyingError as the primary error.
error = unwrappedUnderlyingError
underlyingError = nil
preferredDuration = .longToastViewDuration
}
let text: String
let detailText: String?
@@ -46,20 +71,25 @@ class ToastView: RSTToastView
if let failure = error.localizedFailure
{
text = failure
detailText = error.localizedFailureReason ?? error.localizedRecoverySuggestion ?? error.localizedDescription
detailText = error.localizedFailureReason ?? error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription ?? error.localizedDescription
}
else if let reason = error.localizedFailureReason
{
text = reason
detailText = error.localizedRecoverySuggestion
detailText = error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription
}
else
{
text = error.localizedDescription
detailText = nil
detailText = underlyingError?.localizedDescription ?? error.localizedRecoverySuggestion
}
self.init(text: text, detailText: detailText)
if let preferredDuration = preferredDuration
{
self.preferredDuration = preferredDuration
}
}
required init(coder aDecoder: NSCoder) {
@@ -80,6 +110,19 @@ class ToastView: RSTToastView
self.show(in: viewController.navigationController?.view ?? viewController.view, duration: self.preferredDuration)
}
override func show(in view: UIView, duration: TimeInterval)
{
super.show(in: view, duration: duration)
let announcement = (self.textLabel.text ?? "") + ". " + (self.detailTextLabel.text ?? "")
self.accessibilityLabel = announcement
// Minimum 0.75 delay to prevent announcement being cut off by VoiceOver.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) {
UIAccessibility.post(notification: .announcement, argument: announcement)
}
}
override func show(in view: UIView)
{
self.show(in: view, duration: self.preferredDuration)

View File

@@ -0,0 +1,23 @@
//
// INInteraction+AltStore.swift
// AltStore
//
// Created by Riley Testut on 9/4/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Intents
// Requires iOS 14 in-app intent handling.
@available(iOS 14, *)
extension INInteraction
{
static func refreshAllApps() -> INInteraction
{
let refreshAllIntent = RefreshAllIntent()
refreshAllIntent.suggestedInvocationPhrase = NSString.deferredLocalizedIntentsString(with: "Refresh my apps") as String
let interaction = INInteraction(intent: refreshAllIntent, response: nil)
return interaction
}
}

View File

@@ -0,0 +1,60 @@
//
// NSError+AltStore.swift
// AltStore
//
// Created by Riley Testut on 3/11/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
extension NSError
{
@objc(alt_localizedFailure)
var localizedFailure: String? {
let localizedFailure = (self.userInfo[NSLocalizedFailureErrorKey] as? String) ?? (NSError.userInfoValueProvider(forDomain: self.domain)?(self, NSLocalizedFailureErrorKey) as? String)
return localizedFailure
}
func withLocalizedFailure(_ failure: String) -> NSError
{
var userInfo = self.userInfo
userInfo[NSLocalizedFailureErrorKey] = failure
userInfo[NSLocalizedDescriptionKey] = self.localizedDescription
userInfo[NSLocalizedFailureReasonErrorKey] = self.localizedFailureReason
userInfo[NSLocalizedRecoverySuggestionErrorKey] = self.localizedRecoverySuggestion
let error = NSError(domain: self.domain, code: self.code, userInfo: userInfo)
return error
}
func sanitizedForCoreData() -> NSError
{
var userInfo = self.userInfo
userInfo[NSLocalizedFailureErrorKey] = self.localizedFailure
userInfo[NSLocalizedDescriptionKey] = self.localizedDescription
userInfo[NSLocalizedFailureReasonErrorKey] = self.localizedFailureReason
userInfo[NSLocalizedRecoverySuggestionErrorKey] = self.localizedRecoverySuggestion
// Remove non-ObjC-compliant userInfo values.
userInfo["NSCodingPath"] = nil
let error = NSError(domain: self.domain, code: self.code, userInfo: userInfo)
return error
}
}
protocol ALTLocalizedError: LocalizedError, CustomNSError
{
var errorFailure: String? { get }
}
extension ALTLocalizedError
{
var errorUserInfo: [String : Any] {
let userInfo = [NSLocalizedDescriptionKey: self.errorDescription,
NSLocalizedFailureReasonErrorKey: self.failureReason,
NSLocalizedFailureErrorKey: self.errorFailure].compactMapValues { $0 }
return userInfo
}
}

View File

@@ -1,27 +0,0 @@
//
// NSError+LocalizedFailure.swift
// AltStore
//
// Created by Riley Testut on 3/11/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
extension NSError
{
@objc(alt_localizedFailure)
var localizedFailure: String? {
let localizedFailure = (self.userInfo[NSLocalizedFailureErrorKey] as? String) ?? (NSError.userInfoValueProvider(forDomain: self.domain)?(self, NSLocalizedFailureErrorKey) as? String)
return localizedFailure
}
func withLocalizedFailure(_ failure: String) -> NSError
{
var userInfo = self.userInfo
userInfo[NSLocalizedFailureErrorKey] = failure
let error = NSError(domain: self.domain, code: self.code, userInfo: userInfo)
return error
}
}

View File

@@ -1,21 +0,0 @@
//
// UIColor+AltStore.swift
// AltStore
//
// Created by Riley Testut on 5/9/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
extension UIColor
{
static let altPrimary = UIColor(named: "Primary")!
static let altPink = UIColor(named: "Pink")!
static let refreshRed = UIColor(named: "RefreshRed")!
static let refreshOrange = UIColor(named: "RefreshOrange")!
static let refreshYellow = UIColor(named: "RefreshYellow")!
static let refreshGreen = UIColor(named: "RefreshGreen")!
}

View File

@@ -0,0 +1,30 @@
//
// UIDevice+Jailbreak.swift
// AltStore
//
// Created by Riley Testut on 6/5/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
extension UIDevice
{
var isJailbroken: Bool {
if
FileManager.default.fileExists(atPath: "/Applications/Cydia.app") ||
FileManager.default.fileExists(atPath: "/Library/MobileSubstrate/MobileSubstrate.dylib") ||
FileManager.default.fileExists(atPath: "/bin/bash") ||
FileManager.default.fileExists(atPath: "/usr/sbin/sshd") ||
FileManager.default.fileExists(atPath: "/etc/apt") ||
FileManager.default.fileExists(atPath: "/private/var/lib/apt/") ||
UIApplication.shared.canOpenURL(URL(string:"cydia://")!)
{
return true
}
else
{
return false
}
}
}

View File

@@ -1,46 +0,0 @@
//
// UserDefaults+AltStore.swift
// AltStore
//
// Created by Riley Testut on 6/4/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import Roxas
extension UserDefaults
{
@NSManaged var firstLaunch: Date?
@NSManaged var preferredServerID: String?
@NSManaged var isBackgroundRefreshEnabled: Bool
@NSManaged var isDebugModeEnabled: Bool
@NSManaged var presentedLaunchReminderNotification: Bool
@NSManaged var legacySideloadedApps: [String]?
var activeAppsLimit: Int? {
get {
return self._activeAppsLimit?.intValue
}
set {
if let value = newValue
{
self._activeAppsLimit = NSNumber(value: value)
}
else
{
self._activeAppsLimit = nil
}
}
}
@NSManaged @objc(activeAppsLimit) private var _activeAppsLimit: NSNumber?
func registerDefaults()
{
self.register(defaults: [#keyPath(UserDefaults.isBackgroundRefreshEnabled): true])
}
}

View File

@@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ALTAppGroups</key>
<array>
<string>group.com.rileytestut.AltStore</string>
</array>
<key>ALTDeviceID</key>
<string>00008030-001948590202802E</string>
<key>ALTServerID</key>
@@ -49,9 +53,24 @@
<string>altstore</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>AltStore Backup</string>
<key>CFBundleURLSchemes</key>
<array>
<string>altstore-com.rileytestut.AltStore</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>INIntentsSupported</key>
<array>
<string>RefreshAllIntent</string>
<string>ViewAppIntent</string>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>altstore-com.rileytestut.AltStore</string>
@@ -65,6 +84,38 @@
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSBonjourServices</key>
<array>
<string>_altserver._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>AltStore uses the local network to find and communicate with AltServer.</string>
<key>NSUserActivityTypes</key>
<array>
<string>RefreshAllIntent</string>
<string>ViewAppIntent</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>

View File

@@ -0,0 +1,121 @@
//
// IntentHandler.swift
// AltStore
//
// Created by Riley Testut on 7/6/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltStoreCore
@available(iOS 14, *)
class IntentHandler: NSObject, RefreshAllIntentHandling
{
private let queue = DispatchQueue(label: "io.altstore.IntentHandler")
private var completionHandlers = [RefreshAllIntent: (RefreshAllIntentResponse) -> Void]()
private var queuedResponses = [RefreshAllIntent: RefreshAllIntentResponse]()
func confirm(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void)
{
// Refreshing apps usually, but not always, completes within alotted time.
// As a workaround, we'll start refreshing apps in confirm() so we can
// take advantage of some extra time before starting handle() timeout timer.
self.completionHandlers[intent] = { (response) in
if response.code != .ready
{
// Operation finished before confirmation "timeout".
// Cache response to return it when handle() is called.
self.queuedResponses[intent] = response
}
completion(RefreshAllIntentResponse(code: .ready, userActivity: nil))
}
// Give ourselves 9 extra seconds before starting handle() timeout timer.
// 10 seconds or longer results in timeout regardless.
self.queue.asyncAfter(deadline: .now() + 9.0) {
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
}
if !DatabaseManager.shared.isStarted
{
DatabaseManager.shared.start() { (error) in
if let error = error
{
self.finish(intent, response: RefreshAllIntentResponse.failure(localizedDescription: error.localizedDescription))
}
else
{
self.refreshApps(intent: intent)
}
}
}
else
{
self.refreshApps(intent: intent)
}
}
func handle(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void)
{
self.completionHandlers[intent] = { (response) in
// Ignore .ready response from confirm() timeout.
guard response.code != .ready else { return }
completion(response)
}
if let response = self.queuedResponses[intent]
{
self.queuedResponses[intent] = nil
self.finish(intent, response: response)
}
}
}
@available(iOS 14, *)
private extension IntentHandler
{
func finish(_ intent: RefreshAllIntent, response: RefreshAllIntentResponse)
{
self.queue.async {
guard let completionHandler = self.completionHandlers[intent] else { return }
self.completionHandlers[intent] = nil
completionHandler(response)
}
}
func refreshApps(intent: RefreshAllIntent)
{
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let installedApps = InstalledApp.fetchActiveApps(in: context)
AppManager.shared.backgroundRefresh(installedApps, presentsNotifications: false) { (result) in
do
{
let results = try result.get()
for (_, result) in results
{
guard case let .failure(error) = result else { continue }
throw error
}
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
}
catch RefreshError.noInstalledApps
{
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
}
catch let error as NSError
{
print("Failed to refresh apps in background.", error)
self.finish(intent, response: RefreshAllIntentResponse.failure(localizedDescription: error.localizedFailureReason ?? error.localizedDescription))
}
}
}
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>INEnums</key>
<array/>
<key>INIntentDefinitionModelVersion</key>
<string>1.2</string>
<key>INIntentDefinitionNamespace</key>
<string>KyhEWE</string>
<key>INIntentDefinitionSystemVersion</key>
<string>20A5354i</string>
<key>INIntentDefinitionToolsBuildVersion</key>
<string>12A8189n</string>
<key>INIntentDefinitionToolsVersion</key>
<string>12.0</string>
<key>INIntents</key>
<array>
<dict>
<key>INIntentCategory</key>
<string>generic</string>
<key>INIntentConfigurable</key>
<true/>
<key>INIntentDescriptionID</key>
<string>62S1rm</string>
<key>INIntentLastParameterTag</key>
<integer>3</integer>
<key>INIntentManagedParameterCombinations</key>
<dict>
<key></key>
<dict>
<key>INIntentParameterCombinationSupportsBackgroundExecution</key>
<true/>
<key>INIntentParameterCombinationTitle</key>
<string>Refresh All Apps</string>
<key>INIntentParameterCombinationTitleID</key>
<string>cJxa2I</string>
<key>INIntentParameterCombinationUpdatesLinked</key>
<true/>
</dict>
</dict>
<key>INIntentName</key>
<string>RefreshAll</string>
<key>INIntentParameterCombinations</key>
<dict>
<key></key>
<dict>
<key>INIntentParameterCombinationSupportsBackgroundExecution</key>
<true/>
<key>INIntentParameterCombinationTitle</key>
<string>Refresh All Apps</string>
<key>INIntentParameterCombinationTitleID</key>
<string>DKTGdO</string>
</dict>
</dict>
<key>INIntentResponse</key>
<dict>
<key>INIntentResponseCodes</key>
<array>
<dict>
<key>INIntentResponseCodeConciseFormatString</key>
<string>All apps have been refreshed.</string>
<key>INIntentResponseCodeConciseFormatStringID</key>
<string>3WMWsJ</string>
<key>INIntentResponseCodeFormatString</key>
<string>All apps have been refreshed.</string>
<key>INIntentResponseCodeFormatStringID</key>
<string>BjInD3</string>
<key>INIntentResponseCodeName</key>
<string>success</string>
<key>INIntentResponseCodeSuccess</key>
<true/>
</dict>
<dict>
<key>INIntentResponseCodeConciseFormatString</key>
<string>${localizedDescription}</string>
<key>INIntentResponseCodeConciseFormatStringID</key>
<string>GJdShK</string>
<key>INIntentResponseCodeFormatString</key>
<string>${localizedDescription}</string>
<key>INIntentResponseCodeFormatStringID</key>
<string>oXAiOU</string>
<key>INIntentResponseCodeName</key>
<string>failure</string>
</dict>
</array>
<key>INIntentResponseLastParameterTag</key>
<integer>3</integer>
<key>INIntentResponseParameters</key>
<array>
<dict>
<key>INIntentResponseParameterDisplayName</key>
<string>Localized Description</string>
<key>INIntentResponseParameterDisplayNameID</key>
<string>wdy22v</string>
<key>INIntentResponseParameterDisplayPriority</key>
<integer>1</integer>
<key>INIntentResponseParameterName</key>
<string>localizedDescription</string>
<key>INIntentResponseParameterTag</key>
<integer>3</integer>
<key>INIntentResponseParameterType</key>
<string>String</string>
</dict>
</array>
</dict>
<key>INIntentTitle</key>
<string>Refresh All Apps</string>
<key>INIntentTitleID</key>
<string>2b6Xto</string>
<key>INIntentType</key>
<string>Custom</string>
<key>INIntentVerb</key>
<string>Do</string>
</dict>
</array>
<key>INTypes</key>
<array/>
</dict>
</plist>

View File

@@ -9,6 +9,8 @@
import UIKit
import Roxas
import AltStoreCore
class LaunchViewController: RSTLaunchViewController
{
private var didFinishLaunching = false

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,86 @@
//
// AppManagerErrors.swift
// AltStore
//
// Created by Riley Testut on 8/27/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import AltStoreCore
extension AppManager
{
struct FetchSourcesError: LocalizedError, CustomNSError
{
var primaryError: Error?
var sources: Set<Source>?
var errors = [Source: Error]()
var managedObjectContext: NSManagedObjectContext?
var errorDescription: String? {
if let error = self.primaryError
{
return error.localizedDescription
}
else
{
var localizedDescription: String?
self.managedObjectContext?.performAndWait {
if self.sources?.count == 1
{
localizedDescription = NSLocalizedString("Could not refresh store.", comment: "")
}
else if self.errors.count == 1
{
guard let source = self.errors.keys.first else { return }
localizedDescription = String(format: NSLocalizedString("Could not refresh source “%@”.", comment: ""), source.name)
}
else
{
localizedDescription = String(format: NSLocalizedString("Could not refresh %@ sources.", comment: ""), NSNumber(value: self.errors.count))
}
}
return localizedDescription
}
}
var recoverySuggestion: String? {
if let error = self.primaryError as NSError?
{
return error.localizedRecoverySuggestion
}
else if self.errors.count == 1
{
return nil
}
else
{
return NSLocalizedString("Tap to view source errors.", comment: "")
}
}
var errorUserInfo: [String : Any] {
guard let error = self.errors.values.first, self.errors.count == 1 else { return [:] }
return [NSUnderlyingErrorKey: error]
}
init(_ error: Error)
{
self.primaryError = error
}
init(sources: Set<Source>, errors: [Source: Error], context: NSManagedObjectContext)
{
self.sources = sources
self.errors = errors
self.managedObjectContext = context
}
}
}

View File

@@ -1,224 +0,0 @@
//
// DatabaseManager.swift
// AltStore
//
// Created by Riley Testut on 5/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
import AltSign
import Roxas
public class DatabaseManager
{
public static let shared = DatabaseManager()
public let persistentContainer: RSTPersistentContainer
public private(set) var isStarted = false
private var startCompletionHandlers = [(Error?) -> Void]()
private init()
{
self.persistentContainer = RSTPersistentContainer(name: "AltStore")
self.persistentContainer.preferredMergePolicy = MergePolicy()
}
}
public extension DatabaseManager
{
func start(completionHandler: @escaping (Error?) -> Void)
{
self.startCompletionHandlers.append(completionHandler)
guard self.startCompletionHandlers.count == 1 else { return }
func finish(_ error: Error?)
{
self.startCompletionHandlers.forEach { $0(error) }
self.startCompletionHandlers.removeAll()
}
guard !self.isStarted else { return finish(nil) }
self.persistentContainer.loadPersistentStores { (description, error) in
guard error == nil else { return finish(error!) }
self.prepareDatabase() { (result) in
switch result
{
case .failure(let error):
finish(error)
case .success:
self.isStarted = true
finish(nil)
}
}
}
}
func signOut(completionHandler: @escaping (Error?) -> Void)
{
self.persistentContainer.performBackgroundTask { (context) in
if let account = self.activeAccount(in: context)
{
account.isActiveAccount = false
}
if let team = self.activeTeam(in: context)
{
team.isActiveTeam = false
}
do
{
try context.save()
Keychain.shared.reset()
completionHandler(nil)
}
catch
{
print("Failed to save when signing out.", error)
completionHandler(error)
}
}
}
}
public extension DatabaseManager
{
var viewContext: NSManagedObjectContext {
return self.persistentContainer.viewContext
}
}
extension DatabaseManager
{
func activeAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> Account?
{
let predicate = NSPredicate(format: "%K == YES", #keyPath(Account.isActiveAccount))
let activeAccount = Account.first(satisfying: predicate, in: context)
return activeAccount
}
func activeTeam(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> Team?
{
let predicate = NSPredicate(format: "%K == YES", #keyPath(Team.isActiveTeam))
let activeTeam = Team.first(satisfying: predicate, in: context)
return activeTeam
}
func patreonAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> PatreonAccount?
{
let patronAccount = PatreonAccount.first(in: context)
return patronAccount
}
}
private extension DatabaseManager
{
func prepareDatabase(completionHandler: @escaping (Result<Void, Error>) -> Void)
{
let context = self.persistentContainer.newBackgroundContext()
context.performAndWait {
guard let localApp = ALTApplication(fileURL: Bundle.main.bundleURL) else { return }
let altStoreSource: Source
if let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context)
{
altStoreSource = source
}
else
{
altStoreSource = Source.makeAltStoreSource(in: context)
}
// Make sure to always update source URL to be current.
altStoreSource.sourceURL = Source.altStoreSourceURL
let storeApp: StoreApp
if let app = StoreApp.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID), in: context)
{
storeApp = app
}
else
{
storeApp = StoreApp.makeAltStoreApp(in: context)
storeApp.version = localApp.version
storeApp.source = altStoreSource
}
let serialNumber = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.certificateID) as? String
let installedApp: InstalledApp
if let app = storeApp.installedApp
{
installedApp = app
}
else
{
installedApp = InstalledApp(resignedApp: localApp, originalBundleIdentifier: StoreApp.altstoreAppID, certificateSerialNumber: serialNumber, context: context)
installedApp.storeApp = storeApp
}
let fileURL = installedApp.fileURL
if !FileManager.default.fileExists(atPath: fileURL.path) || installedApp.version != localApp.version
{
FileManager.default.prepareTemporaryURL() { (temporaryFileURL) in
do
{
try FileManager.default.copyItem(at: Bundle.main.bundleURL, to: temporaryFileURL)
let infoPlistURL = temporaryFileURL.appendingPathComponent("Info.plist")
guard var infoDictionary = Bundle.main.infoDictionary else { throw ALTError(.missingInfoPlist) }
infoDictionary[kCFBundleIdentifierKey as String] = StoreApp.altstoreAppID
try (infoDictionary as NSDictionary).write(to: infoPlistURL)
try FileManager.default.copyItem(at: temporaryFileURL, to: fileURL, shouldReplace: true)
}
catch
{
print("Failed to copy AltStore app bundle to its proper location.", error)
}
}
}
let cachedRefreshedDate = installedApp.refreshedDate
let cachedExpirationDate = installedApp.expirationDate
// Must go after comparing versions to see if we need to update our cached AltStore app bundle.
installedApp.update(resignedApp: localApp, certificateSerialNumber: serialNumber)
if installedApp.refreshedDate < cachedRefreshedDate
{
// Embedded provisioning profile has a creation date older than our refreshed date.
// This most likely means we've refreshed the app since then, and profile is now outdated,
// so use cached dates instead (i.e. not the dates updated from provisioning profile).
installedApp.refreshedDate = cachedRefreshedDate
installedApp.expirationDate = cachedExpirationDate
}
do
{
try context.save()
completionHandler(.success(()))
}
catch
{
completionHandler(.failure(error))
}
}
}
}

View File

@@ -1,171 +0,0 @@
//
// StoreApp.swift
// AltStore
//
// Created by Riley Testut on 5/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
import Roxas
import AltSign
extension StoreApp
{
#if ALPHA
static let altstoreAppID = "com.rileytestut.AltStore.Alpha"
static let alternativeAltStoreAppIDs: Set<String> = ["com.rileytestut.AltStore", "com.rileytestut.AltStore.Beta"]
#elseif BETA
static let altstoreAppID = "com.rileytestut.AltStore.Beta"
static let alternativeAltStoreAppIDs: Set<String> = ["com.rileytestut.AltStore", "com.rileytestut.AltStore.Alpha"]
#else
static let altstoreAppID = "com.rileytestut.AltStore"
static let alternativeAltStoreAppIDs: Set<String> = ["com.rileytestut.AltStore.Beta", "com.rileytestut.AltStore.Alpha"]
#endif
}
@objc(StoreApp)
class StoreApp: NSManagedObject, Decodable, Fetchable
{
/* Properties */
@NSManaged private(set) var name: String
@NSManaged private(set) var bundleIdentifier: String
@NSManaged private(set) var subtitle: String?
@NSManaged private(set) var developerName: String
@NSManaged private(set) var localizedDescription: String
@NSManaged private(set) var size: Int32
@NSManaged private(set) var iconURL: URL
@NSManaged private(set) var screenshotURLs: [URL]
@NSManaged var version: String
@NSManaged private(set) var versionDate: Date
@NSManaged private(set) var versionDescription: String?
@NSManaged private(set) var downloadURL: URL
@NSManaged private(set) var tintColor: UIColor?
@NSManaged private(set) var isBeta: Bool
@NSManaged var sourceIdentifier: String?
@NSManaged var sortIndex: Int32
/* Relationships */
@NSManaged var installedApp: InstalledApp?
@NSManaged var newsItems: Set<NewsItem>
@NSManaged @objc(source) var _source: Source?
@NSManaged @objc(permissions) var _permissions: NSOrderedSet
@nonobjc var source: Source? {
set {
self._source = newValue
self.sourceIdentifier = newValue?.identifier
}
get {
return self._source
}
}
@nonobjc var permissions: [AppPermission] {
return self._permissions.array as! [AppPermission]
}
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{
super.init(entity: entity, insertInto: context)
}
private enum CodingKeys: String, CodingKey
{
case name
case bundleIdentifier
case developerName
case localizedDescription
case version
case versionDescription
case versionDate
case iconURL
case screenshotURLs
case downloadURL
case tintColor
case subtitle
case permissions
case size
case isBeta = "beta"
}
required init(from decoder: Decoder) throws
{
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
super.init(entity: StoreApp.entity(), insertInto: nil)
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.bundleIdentifier = try container.decode(String.self, forKey: .bundleIdentifier)
self.developerName = try container.decode(String.self, forKey: .developerName)
self.localizedDescription = try container.decode(String.self, forKey: .localizedDescription)
self.subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle)
self.version = try container.decode(String.self, forKey: .version)
self.versionDate = try container.decode(Date.self, forKey: .versionDate)
self.versionDescription = try container.decodeIfPresent(String.self, forKey: .versionDescription)
self.iconURL = try container.decode(URL.self, forKey: .iconURL)
self.screenshotURLs = try container.decodeIfPresent([URL].self, forKey: .screenshotURLs) ?? []
self.downloadURL = try container.decode(URL.self, forKey: .downloadURL)
if let tintColorHex = try container.decodeIfPresent(String.self, forKey: .tintColor)
{
guard let tintColor = UIColor(hexString: tintColorHex) else {
throw DecodingError.dataCorruptedError(forKey: .tintColor, in: container, debugDescription: "Hex code is invalid.")
}
self.tintColor = tintColor
}
self.size = try container.decode(Int32.self, forKey: .size)
self.isBeta = try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false
let permissions = try container.decodeIfPresent([AppPermission].self, forKey: .permissions) ?? []
context.insert(self)
// Must assign after we're inserted into context.
self._permissions = NSOrderedSet(array: permissions)
}
}
extension StoreApp
{
@nonobjc class func fetchRequest() -> NSFetchRequest<StoreApp>
{
return NSFetchRequest<StoreApp>(entityName: "StoreApp")
}
class func makeAltStoreApp(in context: NSManagedObjectContext) -> StoreApp
{
let app = StoreApp(context: context)
app.name = "AltStore"
app.bundleIdentifier = StoreApp.altstoreAppID
app.developerName = "Riley Testut"
app.localizedDescription = "AltStore is an alternative App Store."
app.iconURL = URL(string: "https://user-images.githubusercontent.com/705880/63392210-540c5980-c37b-11e9-968c-8742fc68ab2e.png")!
app.screenshotURLs = []
app.version = "1.0"
app.versionDate = Date()
app.downloadURL = URL(string: "http://rileytestut.com")!
#if BETA
app.isBeta = true
#endif
return app
}
}

View File

@@ -18,6 +18,7 @@ class InstalledAppsCollectionHeaderView: UICollectionReusableView
self.textLabel = UILabel()
self.textLabel.translatesAutoresizingMaskIntoConstraints = false
self.textLabel.font = UIFont.systemFont(ofSize: 24, weight: .bold)
self.textLabel.accessibilityTraits.insert(.header)
self.button = UIButton(type: .system)
self.button.translatesAutoresizingMaskIntoConstraints = false

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@
import UIKit
import SafariServices
import AltStoreCore
import Roxas
import Nuke
@@ -99,6 +100,11 @@ class NewsViewController: UICollectionViewController
self.collectionView.contentInset.bottom = 20
}
}
@IBAction private func unwindFromSourcesViewController(_ segue: UIStoryboardSegue)
{
self.fetchSource()
}
}
private extension NewsViewController
@@ -135,6 +141,18 @@ private extension NewsViewController
cell.imageView.isIndicatingActivity = false
cell.imageView.isHidden = true
}
cell.isAccessibilityElement = true
cell.accessibilityLabel = (cell.titleLabel.text ?? "") + ". " + (cell.captionLabel.text ?? "")
if newsItem.storeApp != nil || newsItem.externalURL != nil
{
cell.accessibilityTraits.insert(.button)
}
else
{
cell.accessibilityTraits.remove(.button)
}
}
dataSource.prefetchHandler = { (newsItem, indexPath, completionHandler) in
guard let imageURL = newsItem.imageURL else { return nil }
@@ -177,21 +195,28 @@ private extension NewsViewController
AppManager.shared.fetchSources() { (result) in
do
{
let sources = try result.get()
try sources.first?.managedObjectContext?.save()
DispatchQueue.main.async {
self.loadingState = .finished(.success(()))
do
{
let (_, context) = try result.get()
try context.save()
DispatchQueue.main.async {
self.loadingState = .finished(.success(()))
}
}
catch let error as AppManager.FetchSourcesError
{
try error.managedObjectContext?.save()
throw error
}
}
catch let error as NSError
catch
{
DispatchQueue.main.async {
if self.dataSource.itemCount > 0
{
let error = error.withLocalizedFailure(NSLocalizedString("Failed to Fetch Sources", comment: ""))
let toastView = ToastView(error: error)
toastView.addTarget(nil, action: #selector(TabBarController.presentSources), for: .touchUpInside)
toastView.show(in: self)
}
@@ -348,10 +373,9 @@ extension NewsViewController
footerView.layoutMargins.left = self.view.layoutMargins.left
footerView.layoutMargins.right = self.view.layoutMargins.right
footerView.bannerView.titleLabel.text = storeApp.name
footerView.bannerView.subtitleLabel.text = storeApp.developerName
footerView.bannerView.configure(for: storeApp)
footerView.bannerView.tintColor = storeApp.tintColor
footerView.bannerView.betaBadgeView.isHidden = !storeApp.isBeta
footerView.bannerView.button.addTarget(self, action: #selector(NewsViewController.performAppAction(_:)), for: .primaryActionTriggered)
footerView.tapGestureRecognizer.addTarget(self, action: #selector(NewsViewController.handleTapGesture(_:)))
@@ -359,7 +383,10 @@ extension NewsViewController
if storeApp.installedApp == nil
{
footerView.bannerView.button.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal)
let buttonTitle = NSLocalizedString("Free", comment: "")
footerView.bannerView.button.setTitle(buttonTitle.uppercased(), for: .normal)
footerView.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Download %@", comment: ""), storeApp.name)
footerView.bannerView.button.accessibilityValue = buttonTitle
let progress = AppManager.shared.installationProgress(for: storeApp)
footerView.bannerView.button.progress = progress
@@ -376,6 +403,8 @@ extension NewsViewController
else
{
footerView.bannerView.button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
footerView.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Open %@", comment: ""), storeApp.name)
footerView.bannerView.button.accessibilityValue = nil
footerView.bannerView.button.progress = nil
footerView.bannerView.button.countdownDate = nil
}

View File

@@ -10,7 +10,7 @@ import Foundation
import Roxas
import Network
import AltKit
import AltStoreCore
import AltSign
enum AuthenticationError: LocalizedError
@@ -432,11 +432,11 @@ private extension AuthenticationOperation
{
func selectTeam(from teams: [ALTTeam])
{
if let team = teams.first(where: { $0.type == .free })
if let team = teams.first(where: { $0.type == .individual })
{
return completionHandler(.success(team))
}
else if let team = teams.first(where: { $0.type == .individual })
else if let team = teams.first(where: { $0.type == .free })
{
return completionHandler(.success(team))
}
@@ -582,7 +582,7 @@ private extension AuthenticationOperation
return completionHandler(.failure(OperationError.unknownUDID))
}
ALTAppleAPI.shared.fetchDevices(for: team, session: session) { (devices, error) in
ALTAppleAPI.shared.fetchDevices(for: team, types: [.iphone, .ipad], session: session) { (devices, error) in
do
{
let devices = try Result(devices, error).get()
@@ -593,7 +593,7 @@ private extension AuthenticationOperation
}
else
{
ALTAppleAPI.shared.registerDevice(name: UIDevice.current.name, identifier: udid, team: team, session: session) { (device, error) in
ALTAppleAPI.shared.registerDevice(name: UIDevice.current.name, identifier: udid, type: .iphone, team: team, session: session) { (device, error) in
completionHandler(Result(device, error))
}
}

View File

@@ -0,0 +1,271 @@
//
// BackgroundRefreshAppsOperation.swift
// AltStore
//
// Created by Riley Testut on 7/6/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
import CoreData
import AltStoreCore
enum RefreshError: LocalizedError
{
case noInstalledApps
var errorDescription: String? {
switch self
{
case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "")
}
}
}
private extension CFNotificationName
{
static let requestAppState = CFNotificationName("com.altstore.RequestAppState" as CFString)
static let appIsRunning = CFNotificationName("com.altstore.AppState.Running" as CFString)
static func requestAppState(for appID: String) -> CFNotificationName
{
let name = String(CFNotificationName.requestAppState.rawValue) + "." + appID
return CFNotificationName(name as CFString)
}
static func appIsRunning(for appID: String) -> CFNotificationName
{
let name = String(CFNotificationName.appIsRunning.rawValue) + "." + appID
return CFNotificationName(name as CFString)
}
}
private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =
{ (center, observer, name, object, userInfo) in
guard let name = name, let observer = observer else { return }
let operation = unsafeBitCast(observer, to: BackgroundRefreshAppsOperation.self)
operation.receivedApplicationState(notification: name)
}
@objc(BackgroundRefreshAppsOperation)
class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<InstalledApp, Error>]>
{
let installedApps: [InstalledApp]
private let managedObjectContext: NSManagedObjectContext
var presentsFinishedNotification: Bool = true
private let refreshIdentifier: String = UUID().uuidString
private var runningApplications: Set<String> = []
init(installedApps: [InstalledApp])
{
self.installedApps = installedApps
self.managedObjectContext = installedApps.compactMap({ $0.managedObjectContext }).first ?? DatabaseManager.shared.persistentContainer.newBackgroundContext()
super.init()
}
override func finish(_ result: Result<[String: Result<InstalledApp, Error>], Error>)
{
super.finish(result)
self.scheduleFinishedRefreshingNotification(for: result, delay: 0)
self.managedObjectContext.perform {
self.stopListeningForRunningApps()
}
DispatchQueue.main.async {
if UIApplication.shared.applicationState == .background
{
ServerManager.shared.stopDiscovering()
}
}
}
override func main()
{
super.main()
guard !self.installedApps.isEmpty else {
self.finish(.failure(RefreshError.noInstalledApps))
return
}
if !ServerManager.shared.isDiscovering
{
ServerManager.shared.startDiscovering()
}
self.managedObjectContext.perform {
print("Apps to refresh:", self.installedApps.map(\.bundleIdentifier))
self.startListeningForRunningApps()
// Wait for 3 seconds (2 now, 1 later in FindServerOperation) to:
// a) give us time to discover AltServers
// b) give other processes a chance to respond to requestAppState notification
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
self.managedObjectContext.perform {
let filteredApps = self.installedApps.filter { !self.runningApplications.contains($0.bundleIdentifier) }
print("Filtered Apps to Refresh:", filteredApps.map { $0.bundleIdentifier })
let group = AppManager.shared.refresh(filteredApps, presentingViewController: nil)
group.beginInstallationHandler = { (installedApp) in
guard installedApp.bundleIdentifier == StoreApp.altstoreAppID else { return }
// We're starting to install AltStore, which means the app is about to quit.
// So, we schedule a "refresh successful" local notification to be displayed after a delay,
// but if the app is still running, we cancel the notification.
// Then, we schedule another notification and repeat the process.
// Also since AltServer has already received the app, it can finish installing even if we're no longer running in background.
if let error = group.context.error
{
self.scheduleFinishedRefreshingNotification(for: .failure(error))
}
else
{
var results = group.results
results[installedApp.bundleIdentifier] = .success(installedApp)
self.scheduleFinishedRefreshingNotification(for: .success(results))
}
}
group.completionHandler = { (results) in
self.finish(.success(results))
}
}
}
}
}
}
private extension BackgroundRefreshAppsOperation
{
func startListeningForRunningApps()
{
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
let observer = Unmanaged.passUnretained(self).toOpaque()
for installedApp in self.installedApps
{
let appIsRunningNotification = CFNotificationName.appIsRunning(for: installedApp.bundleIdentifier)
CFNotificationCenterAddObserver(notificationCenter, observer, ReceivedApplicationState, appIsRunningNotification.rawValue, nil, .deliverImmediately)
let requestAppStateNotification = CFNotificationName.requestAppState(for: installedApp.bundleIdentifier)
CFNotificationCenterPostNotification(notificationCenter, requestAppStateNotification, nil, nil, true)
}
}
func stopListeningForRunningApps()
{
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
let observer = Unmanaged.passUnretained(self).toOpaque()
for installedApp in self.installedApps
{
let appIsRunningNotification = CFNotificationName.appIsRunning(for: installedApp.bundleIdentifier)
CFNotificationCenterRemoveObserver(notificationCenter, observer, appIsRunningNotification, nil)
}
}
func receivedApplicationState(notification: CFNotificationName)
{
let baseName = String(CFNotificationName.appIsRunning.rawValue)
let appID = String(notification.rawValue).replacingOccurrences(of: baseName + ".", with: "")
self.runningApplications.insert(appID)
}
func scheduleFinishedRefreshingNotification(for result: Result<[String: Result<InstalledApp, Error>], Error>, delay: TimeInterval = 5)
{
func scheduleFinishedRefreshingNotification()
{
self.cancelFinishedRefreshingNotification()
let content = UNMutableNotificationContent()
var shouldPresentAlert = true
do
{
let results = try result.get()
shouldPresentAlert = !results.isEmpty
for (_, result) in results
{
guard case let .failure(error) = result else { continue }
throw error
}
content.title = NSLocalizedString("Refreshed Apps", comment: "")
content.body = NSLocalizedString("All apps have been refreshed.", comment: "")
}
catch ConnectionError.serverNotFound
{
shouldPresentAlert = false
}
catch RefreshError.noInstalledApps
{
shouldPresentAlert = false
}
catch
{
print("Failed to refresh apps in background.", error)
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
content.body = error.localizedDescription
shouldPresentAlert = true
}
if shouldPresentAlert
{
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay + 1, repeats: false)
let request = UNNotificationRequest(identifier: self.refreshIdentifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
if delay > 0
{
DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
UNUserNotificationCenter.current().getPendingNotificationRequests() { (requests) in
// If app is still running at this point, we schedule another notification with same identifier.
// This prevents the currently scheduled notification from displaying, and starts another countdown timer.
// First though, make sure there _is_ still a pending request, otherwise it's been cancelled
// and we should stop polling.
guard requests.contains(where: { $0.identifier == self.refreshIdentifier }) else { return }
scheduleFinishedRefreshingNotification()
}
}
}
}
}
if self.presentsFinishedNotification
{
scheduleFinishedRefreshingNotification()
}
// Perform synchronously to ensure app doesn't quit before we've finishing saving to disk.
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
context.performAndWait {
_ = RefreshAttempt(identifier: self.refreshIdentifier, result: result, context: context)
do { try context.save() }
catch { print("Failed to save refresh attempt.", error) }
}
}
func cancelFinishedRefreshingNotification()
{
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [self.refreshIdentifier])
}
}

View File

@@ -0,0 +1,183 @@
//
// BackupAppOperation.swift
// AltStore
//
// Created by Riley Testut on 5/12/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltStoreCore
import AltSign
extension BackupAppOperation
{
enum Action: String
{
case backup
case restore
}
}
@objc(BackupAppOperation)
class BackupAppOperation: ResultOperation<Void>
{
let action: Action
let context: InstallAppOperationContext
private var appName: String?
private var timeoutTimer: Timer?
init(action: Action, context: InstallAppOperationContext)
{
self.action = action
self.context = context
super.init()
}
override func main()
{
super.main()
do
{
if let error = self.context.error
{
throw error
}
guard let installedApp = self.context.installedApp, let context = installedApp.managedObjectContext else { throw OperationError.invalidParameters }
context.perform {
do
{
let appName = installedApp.name
self.appName = appName
guard let altstoreApp = InstalledApp.fetchAltStore(in: context) else { throw OperationError.appNotFound }
let altstoreOpenURL = altstoreApp.openAppURL
var returnURLComponents = URLComponents(url: altstoreOpenURL, resolvingAgainstBaseURL: false)
returnURLComponents?.host = "appBackupResponse"
guard let returnURL = returnURLComponents?.url else { throw OperationError.openAppFailed(name: appName) }
var openURLComponents = URLComponents()
openURLComponents.scheme = installedApp.openAppURL.scheme
openURLComponents.host = self.action.rawValue
openURLComponents.queryItems = [URLQueryItem(name: "returnURL", value: returnURL.absoluteString)]
guard let openURL = openURLComponents.url else { throw OperationError.openAppFailed(name: appName) }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
let currentTime = CFAbsoluteTimeGetCurrent()
UIApplication.shared.open(openURL, options: [:]) { (success) in
let elapsedTime = CFAbsoluteTimeGetCurrent() - currentTime
if success
{
self.registerObservers()
}
else if elapsedTime < 0.5
{
// Failed too quickly for human to respond to alert, possibly still finalizing installation.
// Try again in a couple seconds.
print("Failed too quickly, retrying after a few seconds...")
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
UIApplication.shared.open(openURL, options: [:]) { (success) in
if success
{
self.registerObservers()
}
else
{
self.finish(.failure(OperationError.openAppFailed(name: appName)))
}
}
}
}
else
{
self.finish(.failure(OperationError.openAppFailed(name: appName)))
}
}
}
}
catch
{
self.finish(.failure(error))
}
}
}
catch
{
self.finish(.failure(error))
}
}
override func finish(_ result: Result<Void, Error>)
{
let result = result.mapError { (error) -> Error in
let appName = self.appName ?? self.context.bundleIdentifier
switch (error, self.action)
{
case (let error as NSError, _) where (self.context.error as NSError?) == error: fallthrough
case (OperationError.cancelled, _):
return error
case (let error as NSError, .backup):
let localizedFailure = String(format: NSLocalizedString("Could not back up “%@”.", comment: ""), appName)
return error.withLocalizedFailure(localizedFailure)
case (let error as NSError, .restore):
let localizedFailure = String(format: NSLocalizedString("Could not restore “%@”.", comment: ""), appName)
return error.withLocalizedFailure(localizedFailure)
}
}
switch result
{
case .success: self.progress.completedUnitCount += 1
case .failure: break
}
super.finish(result)
}
}
private extension BackupAppOperation
{
func registerObservers()
{
var applicationWillReturnObserver: NSObjectProtocol!
applicationWillReturnObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] (notification) in
guard let self = self, !self.isFinished else { return }
self.timeoutTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] (timer) in
// Final delay to ensure we don't prematurely return failure
// in case timer expired while we were in background, but
// are now returning to app with success response.
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
guard let self = self, !self.isFinished else { return }
self.finish(.failure(OperationError.timedOut))
}
}
NotificationCenter.default.removeObserver(applicationWillReturnObserver!)
}
var backupResponseObserver: NSObjectProtocol!
backupResponseObserver = NotificationCenter.default.addObserver(forName: AppDelegate.appBackupDidFinish, object: nil, queue: nil) { [weak self] (notification) in
self?.timeoutTimer?.invalidate()
let result = notification.userInfo?[AppDelegate.appBackupResultKey] as? Result<Void, Error> ?? .failure(OperationError.unknownResult)
self?.finish(result)
NotificationCenter.default.removeObserver(backupResponseObserver!)
}
}
}

View File

@@ -8,9 +8,8 @@
import Foundation
import AltStoreCore
import AltSign
import AltKit
import Roxas
@objc(DeactivateAppOperation)

View File

@@ -9,6 +9,7 @@
import Foundation
import Roxas
import AltStoreCore
import AltSign
@objc(DownloadAppOperation)
@@ -23,14 +24,14 @@ class DownloadAppOperation: ResultOperation<ALTApplication>
private let session = URLSession(configuration: .default)
init(app: AppProtocol, context: AppOperationContext)
init(app: AppProtocol, destinationURL: URL, context: AppOperationContext)
{
self.app = app
self.context = context
self.bundleIdentifier = app.bundleIdentifier
self.sourceURL = app.url
self.destinationURL = InstalledApp.fileURL(for: app)
self.destinationURL = destinationURL
super.init()
@@ -83,6 +84,19 @@ class DownloadAppOperation: ResultOperation<ALTApplication>
try FileManager.default.copyItem(at: appBundleURL, to: self.destinationURL, shouldReplace: true)
if self.context.bundleIdentifier == StoreApp.dolphinAppID, self.context.bundleIdentifier != application.bundleIdentifier
{
let infoPlistURL = self.destinationURL.appendingPathComponent("Info.plist")
if var infoPlist = NSDictionary(contentsOf: infoPlistURL) as? [String: Any]
{
// Manually update the app's bundle identifier to match the one specified in the source.
// This allows people who previously installed the app to still update and refresh normally.
infoPlist[kCFBundleIdentifierKey as String] = StoreApp.dolphinAppID
(infoPlist as NSDictionary).write(to: infoPlistURL, atomically: true)
}
}
guard let copiedApplication = ALTApplication(fileURL: self.destinationURL) else { throw OperationError.invalidApp }
self.finish(.success(copiedApplication))
}

View File

@@ -8,9 +8,8 @@
import Foundation
import AltStoreCore
import AltSign
import AltKit
import Roxas
@objc(FetchAnisetteDataOperation)

View File

@@ -8,9 +8,8 @@
import Foundation
import AltStoreCore
import AltSign
import AltKit
import Roxas
@objc(FetchAppIDsOperation)

View File

@@ -7,15 +7,18 @@
//
import Foundation
import Roxas
import AltStoreCore
import AltSign
import Roxas
@objc(FetchProvisioningProfilesOperation)
class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
{
let context: AppOperationContext
var additionalEntitlements: [ALTEntitlement: Any]?
private let appGroupsLock = NSLock()
init(context: AppOperationContext)
@@ -38,11 +41,12 @@ class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioni
}
guard
let app = self.context.app,
let team = self.context.team,
let session = self.context.session
else { return self.finish(.failure(OperationError.invalidParameters)) }
guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound)) }
self.progress.totalUnitCount = Int64(1 + app.appExtensions.count)
self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in
@@ -129,7 +133,7 @@ extension FetchProvisioningProfilesOperation
#if DEBUG
if app.bundleIdentifier == StoreApp.altstoreAppID || StoreApp.alternativeAltStoreAppIDs.contains(app.bundleIdentifier)
if app.isAltStoreApp
{
// Use legacy bundle ID format for AltStore.
preferredBundleID = "com.\(team.identifier).\(app.bundleIdentifier)"
@@ -172,17 +176,19 @@ extension FetchProvisioningProfilesOperation
// Or, if the app _is_ installed but with a different team, we need to create a new
// bundle identifier anyway to prevent collisions with the previous team.
let parentBundleID = parentApp?.bundleIdentifier ?? app.bundleIdentifier
let updatedParentBundleID = parentBundleID + "." + team.identifier // Append just team identifier to make it harder to track.
let updatedParentBundleID: String
if app.bundleIdentifier == StoreApp.altstoreAppID || StoreApp.alternativeAltStoreAppIDs.contains(app.bundleIdentifier)
if app.isAltStoreApp
{
// Use legacy bundle ID format for AltStore.
bundleID = "com.\(team.identifier).\(app.bundleIdentifier)"
// Use legacy bundle ID format for AltStore (and its extensions).
updatedParentBundleID = "com.\(team.identifier).\(parentBundleID)"
}
else
{
bundleID = app.bundleIdentifier.replacingOccurrences(of: parentBundleID, with: updatedParentBundleID)
updatedParentBundleID = parentBundleID + "." + team.identifier // Append just team identifier to make it harder to track.
}
bundleID = app.bundleIdentifier.replacingOccurrences(of: parentBundleID, with: updatedParentBundleID)
}
let preferredName: String
@@ -237,7 +243,7 @@ extension FetchProvisioningProfilesOperation
{
let appIDs = try Result(appIDs, error).get()
if let appID = appIDs.first(where: { $0.bundleIdentifier == bundleIdentifier })
if let appID = appIDs.first(where: { $0.bundleIdentifier.lowercased() == bundleIdentifier.lowercased() })
{
completionHandler(.success(appID))
}
@@ -299,14 +305,20 @@ extension FetchProvisioningProfilesOperation
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
let requiredFeatures = app.entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in
var entitlements = app.entitlements
for (key, value) in additionalEntitlements ?? [:]
{
entitlements[key] = value
}
let requiredFeatures = entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in
guard let feature = ALTFeature(entitlement: entitlement) else { return nil }
return (feature, value)
}
var features = requiredFeatures.reduce(into: [ALTFeature: Any]()) { $0[$1.0] = $1.1 }
if let applicationGroups = app.entitlements[.appGroups] as? [String], !applicationGroups.isEmpty
if let applicationGroups = entitlements[.appGroups] as? [String], !applicationGroups.isEmpty
{
features[.appGroups] = true
}
@@ -347,56 +359,95 @@ extension FetchProvisioningProfilesOperation
func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
// TODO: Handle apps belonging to more than one app group.
guard let applicationGroups = app.entitlements[.appGroups] as? [String], let groupIdentifier = applicationGroups.first else {
var entitlements = app.entitlements
for (key, value) in additionalEntitlements ?? [:]
{
entitlements[key] = value
}
guard var applicationGroups = entitlements[.appGroups] as? [String], !applicationGroups.isEmpty else {
// Assigning an App ID to an empty app group array fails,
// so just do nothing if there are no app groups.
return completionHandler(.success(appID))
}
func finish(_ result: Result<ALTAppGroup, Error>)
if app.isAltStoreApp
{
switch result
// Potentially updating app groups for this specific AltStore.
// Find the (unique) AltStore app group, then replace it
// with the correct "base" app group ID.
// Otherwise, we may append a duplicate team identifier to the end.
if let index = applicationGroups.firstIndex(where: { $0.contains(Bundle.baseAltStoreAppGroupID) })
{
case .failure(let error): completionHandler(.failure(error))
case .success(let group):
// Assign App Group
// TODO: Determine whether app already belongs to app group.
ALTAppleAPI.shared.add(appID, to: group, team: team, session: session) { (success, error) in
let result = result.map { _ in appID }
completionHandler(result)
}
applicationGroups[index] = Bundle.baseAltStoreAppGroupID
}
else
{
applicationGroups.append(Bundle.baseAltStoreAppGroupID)
}
}
// Dispatch onto global queue to prevent appGroupsLock deadlock.
DispatchQueue.global().async {
let adjustedGroupIdentifier = groupIdentifier + "." + team.identifier
// Ensure we're not concurrently fetching and updating app groups,
// which can lead to race conditions such as adding an app group twice.
self.appGroupsLock.lock()
func finish(_ result: Result<ALTAppID, Error>)
{
self.appGroupsLock.unlock()
completionHandler(result)
}
ALTAppleAPI.shared.fetchAppGroups(for: team, session: session) { (groups, error) in
switch Result(groups, error)
{
case .failure(let error):
self.appGroupsLock.unlock()
completionHandler(.failure(error))
case .failure(let error): finish(.failure(error))
case .success(let fetchedGroups):
let dispatchGroup = DispatchGroup()
case .success(let groups):
if let group = groups.first(where: { $0.groupIdentifier == adjustedGroupIdentifier })
var groups = [ALTAppGroup]()
var errors = [Error]()
for groupIdentifier in applicationGroups
{
self.appGroupsLock.unlock()
finish(.success(group))
}
else
{
// Not all characters are allowed in group names, so we replace periods with spaces (like Apple does).
let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ")
let adjustedGroupIdentifier = groupIdentifier + "." + team.identifier
ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { (group, error) in
self.appGroupsLock.unlock()
finish(Result(group, error))
if let group = fetchedGroups.first(where: { $0.groupIdentifier == adjustedGroupIdentifier })
{
groups.append(group)
}
else
{
dispatchGroup.enter()
// Not all characters are allowed in group names, so we replace periods with spaces (like Apple does).
let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ")
ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { (group, error) in
switch Result(group, error)
{
case .success(let group): groups.append(group)
case .failure(let error): errors.append(error)
}
dispatchGroup.leave()
}
}
}
dispatchGroup.notify(queue: .global()) {
if let error = errors.first
{
finish(.failure(error))
}
else
{
ALTAppleAPI.shared.assign(appID, to: Array(groups), team: team, session: session) { (success, error) in
let result = Result(success, error)
finish(result.map { _ in appID })
}
}
}
}
@@ -406,7 +457,7 @@ extension FetchProvisioningProfilesOperation
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
switch Result(profile, error)
{
case .failure(let error): completionHandler(.failure(error))
@@ -420,7 +471,7 @@ extension FetchProvisioningProfilesOperation
case .success:
// Fetch new provisiong profile
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, team: team, session: session) { (profile, error) in
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
completionHandler(Result(profile, error))
}
}

View File

@@ -9,6 +9,7 @@
import Foundation
import CoreData
import AltStoreCore
import Roxas
@objc(FetchSourceOperation)
@@ -41,12 +42,15 @@ class FetchSourceOperation: ResultOperation<Source>
super.main()
let dataTask = self.session.dataTask(with: self.sourceURL) { (data, response, error) in
self.managedObjectContext.perform {
let childContext = DatabaseManager.shared.persistentContainer.newBackgroundContext(withParent: self.managedObjectContext)
childContext.mergePolicy = NSOverwriteMergePolicy
childContext.perform {
do
{
let (data, _) = try Result((data, response), error).get()
let decoder = JSONDecoder()
let decoder = AltStoreCore.JSONDecoder()
decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
let container = try decoder.singleValueContainer()
let text = try container.decode(String.self)
@@ -68,21 +72,35 @@ class FetchSourceOperation: ResultOperation<Source>
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Date is in invalid format.")
})
decoder.managedObjectContext = self.managedObjectContext
decoder.managedObjectContext = childContext
decoder.sourceURL = self.sourceURL
let source = try decoder.decode(Source.self, from: data)
let identifier = source.identifier
if source.identifier == Source.altStoreIdentifier, let patreonAccessToken = source.userInfo?[.patreonAccessToken]
if identifier == Source.altStoreIdentifier, let patreonAccessToken = source.userInfo?[.patreonAccessToken]
{
Keychain.shared.patreonCreatorAccessToken = patreonAccessToken
}
self.finish(.success(source))
try childContext.save()
self.managedObjectContext.perform {
if let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), identifier), in: self.managedObjectContext)
{
self.finish(.success(source))
}
else
{
self.finish(.failure(OperationError.noSources))
}
}
}
catch
{
self.finish(.failure(error))
self.managedObjectContext.perform {
self.finish(.failure(error))
}
}
}
}

View File

@@ -7,17 +7,16 @@
//
import Foundation
import AltKit
import AltStoreCore
import Roxas
private extension Notification.Name
{
static let didReceiveWiredServerConnectionResponse = Notification.Name("io.altstore.didReceiveWiredServerConnectionResponse")
}
private let ReceivedWiredServerConnectionResponse: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =
private let ReceivedServerConnectionResponse: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =
{ (center, observer, name, object, userInfo) in
NotificationCenter.default.post(name: .didReceiveWiredServerConnectionResponse, object: nil)
guard let name = name, let observer = observer else { return }
let operation = unsafeBitCast(observer, to: FindServerOperation.self)
operation.handle(name)
}
@objc(FindServerOperation)
@@ -26,12 +25,13 @@ class FindServerOperation: ResultOperation<Server>
let context: OperationContext
private var isWiredServerConnectionAvailable = false
private var localServerMachServiceName: String?
init(context: OperationContext = OperationContext())
{
self.context = context
}
override func main()
{
super.main()
@@ -49,48 +49,83 @@ class FindServerOperation: ResultOperation<Server>
}
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
let observer = Unmanaged.passUnretained(self).toOpaque()
// Prepare observers to receive callback from wired server (if connected).
CFNotificationCenterAddObserver(notificationCenter, nil, ReceivedWiredServerConnectionResponse, CFNotificationName.wiredServerConnectionAvailableResponse.rawValue, nil, .deliverImmediately)
NotificationCenter.default.addObserver(self, selector: #selector(FindServerOperation.didReceiveWiredServerConnectionResponse(_:)), name: .didReceiveWiredServerConnectionResponse, object: nil)
// Prepare observers to receive callback from wired connection or background daemon (if available).
CFNotificationCenterAddObserver(notificationCenter, observer, ReceivedServerConnectionResponse, CFNotificationName.wiredServerConnectionAvailableResponse.rawValue, nil, .deliverImmediately)
// Post notification.
// Post notifications.
CFNotificationCenterPostNotification(notificationCenter, .wiredServerConnectionAvailableRequest, nil, nil, true)
self.discoverLocalServer()
// Wait for either callback or timeout.
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
if self.isWiredServerConnectionAvailable
if let machServiceName = self.localServerMachServiceName
{
let server = Server(isWiredConnection: true)
// Prefer background daemon, if it exists and is running.
let server = Server(connectionType: .local, machServiceName: machServiceName)
self.finish(.success(server))
}
else if self.isWiredServerConnectionAvailable
{
let server = Server(connectionType: .wired)
self.finish(.success(server))
}
else if let server = ServerManager.shared.discoveredServers.first(where: { $0.isPreferred })
{
// Preferred server.
self.finish(.success(server))
}
else if let server = ServerManager.shared.discoveredServers.first
{
// Any available server.
self.finish(.success(server))
}
else
{
if let server = ServerManager.shared.discoveredServers.first(where: { $0.isPreferred })
// No servers.
self.finish(.failure(ConnectionError.serverNotFound))
}
}
}
override func finish(_ result: Result<Server, Error>)
{
super.finish(result)
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
let observer = Unmanaged.passUnretained(self).toOpaque()
CFNotificationCenterRemoveObserver(notificationCenter, observer, .wiredServerConnectionAvailableResponse, nil)
}
}
fileprivate extension FindServerOperation
{
func discoverLocalServer()
{
for machServiceName in XPCConnection.machServiceNames
{
let xpcConnection = NSXPCConnection.makeConnection(machServiceName: machServiceName)
let connection = XPCConnection(xpcConnection)
connection.connect { (result) in
switch result
{
// Preferred server.
self.finish(.success(server))
}
else if let server = ServerManager.shared.discoveredServers.first
{
// Any available server.
self.finish(.success(server))
}
else
{
// No servers.
self.finish(.failure(ConnectionError.serverNotFound))
case .failure(let error): print("Could not connect to AltDaemon XPC service \(machServiceName).", error)
case .success: self.localServerMachServiceName = machServiceName
}
}
}
}
}
private extension FindServerOperation
{
@objc func didReceiveWiredServerConnectionResponse(_ notification: Notification)
func handle(_ notification: CFNotificationName)
{
self.isWiredServerConnectionAvailable = true
switch notification
{
case .wiredServerConnectionAvailableResponse: self.isWiredServerConnectionAvailable = true
default: break
}
}
}

View File

@@ -9,7 +9,7 @@
import Foundation
import Network
import AltKit
import AltStoreCore
import AltSign
import Roxas
@@ -62,7 +62,8 @@ class InstallAppOperation: ResultOperation<InstalledApp>
}
installedApp.update(resignedApp: resignedApp, certificateSerialNumber: certificate.serialNumber)
installedApp.needsResign = false
if let team = DatabaseManager.shared.activeTeam(in: backgroundContext)
{
installedApp.team = team
@@ -122,10 +123,10 @@ class InstallAppOperation: ResultOperation<InstalledApp>
var activeApps = InstalledApp.fetch(fetchRequest, in: backgroundContext)
if !activeApps.contains(installedApp)
{
let availableActiveApps = max(sideloadedAppsLimit - activeApps.count, 0)
let requiredActiveAppSlots = 1 + installedExtensions.count // As of iOS 13.3.1, app extensions count as "apps"
let activeAppsCount = activeApps.map { $0.requiredActiveSlots }.reduce(0, +)
if requiredActiveAppSlots <= availableActiveApps
let availableActiveApps = max(sideloadedAppsLimit - activeAppsCount, 0)
if installedApp.requiredActiveSlots <= availableActiveApps
{
// This app has not been explicitly activated, but there are enough slots available,
// so implicitly activate it.
@@ -144,7 +145,7 @@ class InstallAppOperation: ResultOperation<InstalledApp>
})
}
let request = BeginInstallationRequest(activeProfiles: activeProfiles)
let request = BeginInstallationRequest(activeProfiles: activeProfiles, bundleIdentifier: installedApp.resignedBundleIdentifier)
connection.send(request) { (result) in
switch result
{
@@ -173,6 +174,21 @@ class InstallAppOperation: ResultOperation<InstalledApp>
{
self.cleanUp()
// Only remove refreshed IPA when finished.
if let app = self.context.app
{
let fileURL = InstalledApp.refreshedIPAURL(for: app)
do
{
try FileManager.default.removeItem(at: fileURL)
}
catch
{
print("Failed to remove refreshed .ipa:", error)
}
}
super.finish(result)
}
}
@@ -223,12 +239,6 @@ private extension InstallAppOperation
do
{
try FileManager.default.removeItem(at: self.context.temporaryDirectory)
if let app = self.context.app
{
let fileURL = InstalledApp.refreshedIPAURL(for: app)
try FileManager.default.removeItem(at: fileURL)
}
}
catch
{

View File

@@ -10,6 +10,7 @@ import Foundation
import CoreData
import Network
import AltStoreCore
import AltSign
class OperationContext
@@ -17,6 +18,8 @@ class OperationContext
var server: Server?
var error: Error?
var presentingViewController: UIViewController?
let operations: NSHashTable<Foundation.Operation>
init(server: Server? = nil, error: Error? = nil, operations: [Foundation.Operation] = [])
@@ -61,7 +64,7 @@ class AuthenticatedOperationContext: OperationContext
class AppOperationContext
{
let bundleIdentifier: String
private let authenticatedContext: AuthenticatedOperationContext
let authenticatedContext: AuthenticatedOperationContext
var app: ALTApplication?
var provisioningProfiles: [String: ALTProvisioningProfile]?
@@ -103,6 +106,14 @@ class InstallAppOperationContext: AppOperationContext
var resignedApp: ALTApplication?
var installationConnection: ServerConnection?
var installedApp: InstalledApp? {
didSet {
self.installedAppContext = self.installedApp?.managedObjectContext
}
}
private var installedAppContext: NSManagedObjectContext?
var beginInstallationHandler: ((InstalledApp) -> Void)?
var alternateIconURL: URL?
}

View File

@@ -14,6 +14,7 @@ enum OperationError: LocalizedError
case unknown
case unknownResult
case cancelled
case timedOut
case notAuthenticated
case appNotFound
@@ -28,17 +29,23 @@ enum OperationError: LocalizedError
case noSources
case openAppFailed(name: String)
case missingAppGroup
var failureReason: String? {
switch self {
case .unknown: return NSLocalizedString("An unknown error occured.", comment: "")
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
case .timedOut: return NSLocalizedString("The operation timed out.", comment: "")
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
case .appNotFound: return NSLocalizedString("App not found.", comment: "")
case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "")
case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "")
case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "")
case .noSources: return NSLocalizedString("There are no AltStore sources.", comment: "")
case .openAppFailed(let name): return String(format: NSLocalizedString("AltStore was denied permission to launch %@.", comment: ""), name)
case .missingAppGroup: return NSLocalizedString("AltStore's shared app group could not be found.", comment: "")
case .iOSVersionNotSupported(let app):
let name = app.name

View File

@@ -8,9 +8,8 @@
import Foundation
import AltStoreCore
import AltSign
import AltKit
import Roxas
@objc(RefreshAppOperation)
@@ -40,12 +39,9 @@ class RefreshAppOperation: ResultOperation<InstalledApp>
throw error
}
guard
let server = self.context.server,
let app = self.context.app,
let profiles = self.context.provisioningProfiles
else { throw OperationError.invalidParameters }
guard let server = self.context.server, let profiles = self.context.provisioningProfiles else { throw OperationError.invalidParameters }
guard let app = self.context.app else { throw OperationError.appNotFound }
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID }
ServerManager.shared.connect(to: server) { (result) in
@@ -88,7 +84,7 @@ class RefreshAppOperation: ResultOperation<InstalledApp>
self.managedObjectContext.perform {
let predicate = NSPredicate(format: "%K == %@", #keyPath(InstalledApp.bundleIdentifier), app.bundleIdentifier)
guard let installedApp = InstalledApp.first(satisfying: predicate, in: self.managedObjectContext) else {
return self.finish(.failure(OperationError.invalidApp))
return self.finish(.failure(OperationError.appNotFound))
}
self.progress.completedUnitCount += 1

View File

@@ -9,6 +9,7 @@
import Foundation
import CoreData
import AltStoreCore
import AltSign
class RefreshGroup: NSObject
@@ -21,6 +22,10 @@ class RefreshGroup: NSObject
private(set) var results = [String: Result<InstalledApp, Error>]()
// Keep strong references to managed object contexts
// so they don't die out from under us.
private(set) var _contexts = Set<NSManagedObjectContext>()
private var isFinished = false
private let dispatchGroup = DispatchGroup()
@@ -33,6 +38,8 @@ class RefreshGroup: NSObject
super.init()
}
/// Used to keep track of which operations belong to this group.
/// This does _not_ add them to any operation queue.
func add(_ operations: [Foundation.Operation])
{
for operation in operations
@@ -46,8 +53,8 @@ class RefreshGroup: NSObject
if self.operations.isEmpty && !operations.isEmpty
{
self.dispatchGroup.notify(queue: .global()) {
self.finish()
self.dispatchGroup.notify(queue: .global()) { [weak self] in
self?.finish()
}
}
@@ -57,6 +64,14 @@ class RefreshGroup: NSObject
func set(_ result: Result<InstalledApp, Error>, forAppWithBundleIdentifier bundleIdentifier: String)
{
self.results[bundleIdentifier] = result
switch result
{
case .failure: break
case .success(let installedApp):
guard let context = installedApp.managedObjectContext else { break }
self._contexts.insert(context)
}
}
func cancel()

Some files were not shown because too many files have changed in this diff Show More