mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-08 22:33:26 +01:00
Installs apps from AltStore via AltServer
This commit is contained in:
5
AltStore/AltStore-Bridging-Header.h
Normal file
5
AltStore/AltStore-Bridging-Header.h
Normal file
@@ -0,0 +1,5 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
#import "NSError+ALTServerError.h"
|
||||
@@ -8,6 +8,9 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
@@ -15,6 +18,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
||||
{
|
||||
ServerManager.shared.startDiscovering()
|
||||
|
||||
DatabaseManager.shared.start { (error) in
|
||||
if let error = error
|
||||
{
|
||||
@@ -25,7 +30,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
print("Started DatabaseManager")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -34,13 +39,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
func applicationDidEnterBackground(_ application: UIApplication)
|
||||
{
|
||||
ServerManager.shared.stopDiscovering()
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
||||
func applicationWillEnterForeground(_ application: UIApplication)
|
||||
{
|
||||
ServerManager.shared.startDiscovering()
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
@@ -50,7 +56,4 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,13 @@ class AppDetailViewController: UITableViewController
|
||||
self.update()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews()
|
||||
{
|
||||
super.viewDidLayoutSubviews()
|
||||
@@ -77,10 +84,19 @@ private extension AppDetailViewController
|
||||
self.developerButton.setTitle(self.app.developerName, for: .normal)
|
||||
self.appIconImageView.image = UIImage(named: self.app.iconName)
|
||||
|
||||
let text = String(format: NSLocalizedString("Download %@", comment: ""), self.app.name)
|
||||
self.downloadButton.setTitle(text, for: .normal)
|
||||
|
||||
self.descriptionLabel.text = self.app.localizedDescription
|
||||
|
||||
if self.app.installedApp == nil
|
||||
{
|
||||
let text = String(format: NSLocalizedString("Download %@", comment: ""), self.app.name)
|
||||
self.downloadButton.setTitle(text, for: .normal)
|
||||
self.downloadButton.isEnabled = true
|
||||
}
|
||||
else
|
||||
{
|
||||
self.downloadButton.setTitle(NSLocalizedString("Installed", comment: ""), for: .normal)
|
||||
self.downloadButton.isEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
func makeScreenshotsDataSource() -> RSTArrayCollectionViewDataSource<UIImage>
|
||||
@@ -103,30 +119,69 @@ private extension AppDetailViewController
|
||||
{
|
||||
guard self.app.installedApp == nil else { return }
|
||||
|
||||
sender.isIndicatingActivity = true
|
||||
let appURL = Bundle.main.url(forResource: "App", withExtension: "ipa")!
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let app = context.object(with: self.app.objectID) as! App
|
||||
do
|
||||
{
|
||||
try FileManager.default.copyItem(at: appURL, to: self.app.ipaURL, shouldReplace: true)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to copy .ipa", error)
|
||||
}
|
||||
|
||||
if let server = ServerManager.shared.discoveredServers.first
|
||||
{
|
||||
sender.isIndicatingActivity = true
|
||||
|
||||
_ = InstalledApp(app: app,
|
||||
bundleIdentifier: app.identifier,
|
||||
signedDate: Date(),
|
||||
expirationDate: Date().addingTimeInterval(60 * 60 * 24 * 7),
|
||||
context: context)
|
||||
|
||||
do
|
||||
{
|
||||
try context.save()
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to download app.", error)
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
sender.isIndicatingActivity = false
|
||||
server.install(self.app) { (result) in
|
||||
DispatchQueue.main.async {
|
||||
switch result
|
||||
{
|
||||
case .success:
|
||||
let toastView = RSTToastView(text: "Installed \(self.app.name)!", detailText: nil)
|
||||
toastView.tintColor = .altPurple
|
||||
toastView.show(in: self.navigationController!.view, duration: 2)
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let app = context.object(with: self.app.objectID) as! App
|
||||
|
||||
_ = InstalledApp(app: app,
|
||||
bundleIdentifier: app.identifier,
|
||||
signedDate: Date(),
|
||||
expirationDate: Date().addingTimeInterval(60 * 60 * 24 * 7),
|
||||
context: context)
|
||||
|
||||
do
|
||||
{
|
||||
try context.save()
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to save context for downloaded app app.", error)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
let toastView = RSTToastView(text: "Failed to install \(self.app.name)", detailText: error.localizedDescription)
|
||||
toastView.tintColor = .altPurple
|
||||
toastView.show(in: self.navigationController!.view, duration: 2)
|
||||
}
|
||||
|
||||
sender.isIndicatingActivity = false
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
let toastView = RSTToastView(text: "Could not find AltServer", detailText: nil)
|
||||
toastView.tintColor = .altPurple
|
||||
toastView.show(in: self.navigationController!.view, duration: 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,5 +51,7 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>ALTDeviceID</key>
|
||||
<string>1c3416b7b0ab68773e6e7eb7f0d110f7c9353acc</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import Roxas
|
||||
|
||||
@objc(App)
|
||||
class App: NSManagedObject, Decodable
|
||||
{
|
||||
@@ -77,3 +79,29 @@ extension App
|
||||
return NSFetchRequest<App>(entityName: "App")
|
||||
}
|
||||
}
|
||||
|
||||
extension App
|
||||
{
|
||||
class var appsDirectoryURL: URL {
|
||||
let appsDirectoryURL = FileManager.default.applicationSupportDirectory.appendingPathComponent("Apps")
|
||||
|
||||
do { try FileManager.default.createDirectory(at: appsDirectoryURL, withIntermediateDirectories: true, attributes: nil) }
|
||||
catch { print(error) }
|
||||
|
||||
return appsDirectoryURL
|
||||
}
|
||||
|
||||
var directoryURL: URL {
|
||||
let directoryURL = App.appsDirectoryURL.appendingPathComponent(self.identifier)
|
||||
|
||||
do { try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) }
|
||||
catch { print(error) }
|
||||
|
||||
return directoryURL
|
||||
}
|
||||
|
||||
var ipaURL: URL {
|
||||
let ipaURL = self.directoryURL.appendingPathComponent("App.ipa")
|
||||
return ipaURL
|
||||
}
|
||||
}
|
||||
|
||||
256
AltStore/Server/Server.swift
Normal file
256
AltStore/Server/Server.swift
Normal file
@@ -0,0 +1,256 @@
|
||||
//
|
||||
// Server.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/30/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Network
|
||||
|
||||
import AltKit
|
||||
|
||||
extension ALTServerError
|
||||
{
|
||||
init<E: Error>(_ error: E)
|
||||
{
|
||||
switch error
|
||||
{
|
||||
case let error as ALTServerError: self = error
|
||||
case is DecodingError: self = ALTServerError(.invalidResponse)
|
||||
case is EncodingError: self = ALTServerError(.invalidRequest)
|
||||
default:
|
||||
assertionFailure("Caught unknown error type")
|
||||
self = ALTServerError(.unknown)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum InstallError: Error
|
||||
{
|
||||
case unknown
|
||||
case cancelled
|
||||
case invalidApp
|
||||
case noUDID
|
||||
case server(ALTServerError)
|
||||
}
|
||||
|
||||
struct Server: Equatable
|
||||
{
|
||||
var service: NetService
|
||||
|
||||
private let dispatchQueue = DispatchQueue(label: "com.rileytestut.AltStore.server", qos: .utility)
|
||||
|
||||
func install(_ app: App, completionHandler: @escaping (Result<Void, InstallError>) -> Void)
|
||||
{
|
||||
let ipaURL = app.ipaURL
|
||||
let appID = app.identifier
|
||||
|
||||
var isFinished = false
|
||||
|
||||
var serverConnection: NWConnection?
|
||||
|
||||
func finish(error: InstallError?)
|
||||
{
|
||||
// Prevent duplicate callbacks if connection is lost.
|
||||
guard !isFinished else { return }
|
||||
isFinished = true
|
||||
|
||||
if let connection = serverConnection
|
||||
{
|
||||
connection.cancel()
|
||||
}
|
||||
|
||||
if let error = error
|
||||
{
|
||||
print("Failed to install \(appID).", error)
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Installed \(appID)!")
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
self.connect { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(error: error)
|
||||
case .success(let connection):
|
||||
serverConnection = connection
|
||||
|
||||
self.sendApp(at: ipaURL, via: connection) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(error: error)
|
||||
case .success:
|
||||
|
||||
self.receiveResponse(from: connection) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success: finish(error: nil)
|
||||
case .failure(let error): finish(error: .server(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Server
|
||||
{
|
||||
func connect(completionHandler: @escaping (Result<NWConnection, InstallError>) -> Void)
|
||||
{
|
||||
let connection = NWConnection(to: .service(name: self.service.name, type: self.service.type, domain: self.service.domain, interface: nil), using: .tcp)
|
||||
|
||||
connection.stateUpdateHandler = { [weak service, unowned connection] (state) in
|
||||
switch state
|
||||
{
|
||||
case .ready: completionHandler(.success(connection))
|
||||
case .cancelled: completionHandler(.failure(.cancelled))
|
||||
|
||||
case .failed(let error):
|
||||
print("Failed to connect to service \(service?.name ?? "").", error)
|
||||
completionHandler(.failure(.server(.init(.connectionFailed))))
|
||||
|
||||
case .waiting: break
|
||||
case .setup: break
|
||||
case .preparing: break
|
||||
@unknown default: break
|
||||
}
|
||||
}
|
||||
|
||||
connection.start(queue: self.dispatchQueue)
|
||||
}
|
||||
|
||||
func sendApp(at fileURL: URL, via connection: NWConnection, completionHandler: @escaping (Result<Void, InstallError>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
guard let appData = try? Data(contentsOf: fileURL) else { throw InstallError.invalidApp }
|
||||
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw InstallError.noUDID }
|
||||
|
||||
let request = ServerRequest(udid: udid, contentSize: appData.count)
|
||||
let requestData = try JSONEncoder().encode(request)
|
||||
|
||||
let requestSize = Int32(requestData.count)
|
||||
let requestSizeData = withUnsafeBytes(of: requestSize) { Data($0) }
|
||||
|
||||
// Send request data size.
|
||||
connection.send(content: requestSizeData, completion: .contentProcessed { (error) in
|
||||
if error != nil
|
||||
{
|
||||
completionHandler(.failure(.server(.init(.lostConnection))))
|
||||
}
|
||||
else
|
||||
{
|
||||
// Send request.
|
||||
connection.send(content: requestData, completion: .contentProcessed { (error) in
|
||||
if error != nil
|
||||
{
|
||||
completionHandler(.failure(.server(.init(.lostConnection))))
|
||||
}
|
||||
else
|
||||
{
|
||||
// Send app data.
|
||||
connection.send(content: appData, completion: .contentProcessed { (error) in
|
||||
if error != nil
|
||||
{
|
||||
completionHandler(.failure(.server(.init(.lostConnection))))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
catch is EncodingError
|
||||
{
|
||||
completionHandler(.failure(.server(.init(.invalidRequest))))
|
||||
}
|
||||
catch let error as InstallError
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
catch
|
||||
{
|
||||
assertionFailure("Unknown error type. \(error)")
|
||||
completionHandler(.failure(.unknown))
|
||||
}
|
||||
}
|
||||
|
||||
func receiveResponse(from connection: NWConnection, completionHandler: @escaping (Result<Void, ALTServerError>) -> Void)
|
||||
{
|
||||
let size = MemoryLayout<Int32>.size
|
||||
|
||||
connection.receive(minimumIncompleteLength: size, maximumLength: size) { (data, _, _, error) in
|
||||
do
|
||||
{
|
||||
let data = try self.process(data: data, error: error, from: connection)
|
||||
|
||||
let expectedBytes = Int(data.withUnsafeBytes { $0.load(as: Int32.self) })
|
||||
connection.receive(minimumIncompleteLength: expectedBytes, maximumLength: expectedBytes) { (data, _, _, error) in
|
||||
do
|
||||
{
|
||||
let data = try self.process(data: data, error: error, from: connection)
|
||||
let response = try JSONDecoder().decode(ServerResponse.self, from: data)
|
||||
|
||||
if let error = response.error
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(ALTServerError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func process(data: Data?, error: NWError?, from connection: NWConnection) 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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
86
AltStore/Server/ServerManager.swift
Normal file
86
AltStore/Server/ServerManager.swift
Normal file
@@ -0,0 +1,86 @@
|
||||
//
|
||||
// ServerManager.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/30/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
import AltKit
|
||||
|
||||
class ServerManager: NSObject
|
||||
{
|
||||
static let shared = ServerManager()
|
||||
|
||||
private(set) var isDiscovering = false
|
||||
private(set) var discoveredServers = [Server]()
|
||||
|
||||
private let serviceBrowser = NetServiceBrowser()
|
||||
|
||||
private override init()
|
||||
{
|
||||
super.init()
|
||||
|
||||
self.serviceBrowser.delegate = self
|
||||
self.serviceBrowser.includesPeerToPeer = false
|
||||
}
|
||||
}
|
||||
|
||||
extension ServerManager
|
||||
{
|
||||
func startDiscovering()
|
||||
{
|
||||
guard !self.isDiscovering else { return }
|
||||
self.isDiscovering = true
|
||||
|
||||
self.serviceBrowser.searchForServices(ofType: ALTServerServiceType, inDomain: "")
|
||||
}
|
||||
|
||||
func stopDiscovering()
|
||||
{
|
||||
guard self.isDiscovering else { return }
|
||||
self.isDiscovering = false
|
||||
|
||||
self.discoveredServers.removeAll()
|
||||
self.serviceBrowser.stop()
|
||||
}
|
||||
}
|
||||
|
||||
extension ServerManager: NetServiceBrowserDelegate
|
||||
{
|
||||
func netServiceBrowserWillSearch(_ browser: NetServiceBrowser)
|
||||
{
|
||||
print("Discovering servers...")
|
||||
}
|
||||
|
||||
func netServiceBrowserDidStopSearch(_ browser: NetServiceBrowser)
|
||||
{
|
||||
print("Stopped discovering servers.")
|
||||
}
|
||||
|
||||
func netServiceBrowser(_ browser: NetServiceBrowser, didNotSearch errorDict: [String : NSNumber])
|
||||
{
|
||||
print("Failed to discovering servers.", errorDict)
|
||||
}
|
||||
|
||||
func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool)
|
||||
{
|
||||
let server = Server(service: service)
|
||||
guard !self.discoveredServers.contains(server) else { return }
|
||||
|
||||
self.discoveredServers.append(server)
|
||||
}
|
||||
|
||||
func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool)
|
||||
{
|
||||
let server = Server(service: service)
|
||||
|
||||
if let index = self.discoveredServers.firstIndex(of: server)
|
||||
{
|
||||
self.discoveredServers.remove(at: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user