From 8b43903b42dd501f6b3e6e5452ac6595f3256b33 Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Fri, 16 Sep 2022 21:34:14 -0500 Subject: [PATCH] Add ability to download Xcode without logging in using XcodeRelease --- Xcodes/Backend/AppState+Install.swift | 24 ++++++++-------- Xcodes/Backend/AppState.swift | 40 ++++++++++++++++++++++++++- Xcodes/Backend/AvailableXcode.swift | 3 ++ Xcodes/Backend/Environment.swift | 2 +- Xcodes/Backend/URLRequest+Apple.swift | 10 +++++++ 5 files changed, 64 insertions(+), 15 deletions(-) diff --git a/Xcodes/Backend/AppState+Install.swift b/Xcodes/Backend/AppState+Install.swift index 79caf967..b4ef931b 100644 --- a/Xcodes/Backend/AppState+Install.swift +++ b/Xcodes/Backend/AppState+Install.swift @@ -40,15 +40,10 @@ extension AppState { } private func install(_ installationType: InstallationType, downloader: Downloader, attemptNumber: Int) -> AnyPublisher { - // We need to check if the Apple ID that is logged in is an Apple Developer - // Since users can use xcodereleases, we don't check for Apple ID on a xcode list refresh - // If the Apple Id is not a developer, the download action will try and download a xip that is invalid, causing a `xcode13.0.xip is damaged and can't be expanded.` + Logger.appState.info("Using \(downloader) downloader") - return validateSession() - .flatMap { _ in - self.getXcodeArchive(installationType, downloader: downloader) - } + return self.getXcodeArchive(installationType, downloader: downloader) .flatMap { xcode, url -> AnyPublisher in self.installArchivedXcode(xcode, at: url) } @@ -98,13 +93,16 @@ extension AppState { } private func downloadXcode(availableXcode: AvailableXcode, downloader: Downloader) -> AnyPublisher<(AvailableXcode, URL), Error> { - downloadOrUseExistingArchive(for: availableXcode, downloader: downloader, progressChanged: { [unowned self] progress in - DispatchQueue.main.async { - self.setInstallationStep(of: availableXcode.version, to: .downloading(progress: progress)) + return validateADCSession(path: availableXcode.downloadPath) + .flatMap { _ in + return self.downloadOrUseExistingArchive(for: availableXcode, downloader: downloader, progressChanged: { [unowned self] progress in + DispatchQueue.main.async { + self.setInstallationStep(of: availableXcode.version, to: .downloading(progress: progress)) + } + }) + .map { return (availableXcode, $0) } } - }) - .map { return (availableXcode, $0) } - .eraseToAnyPublisher() + .eraseToAnyPublisher() } public func downloadOrUseExistingArchive(for availableXcode: AvailableXcode, downloader: Downloader, progressChanged: @escaping (Progress) -> Void) -> AnyPublisher { diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 302e8832..a0986429 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -157,7 +157,16 @@ class AppState: ObservableObject { } // MARK: - Authentication + func validateADCSession(path: String) -> AnyPublisher { + return Current.network.dataTask(with: URLRequest.downloadADCAuth(path: path)) + .receive(on: DispatchQueue.main) + .tryMap { _ in + } + .eraseToAnyPublisher() + } + func validateSession() -> AnyPublisher { + return Current.network.validateSession() .receive(on: DispatchQueue.main) .handleEvents(receiveCompletion: { completion in @@ -368,7 +377,12 @@ class AppState: ObservableObject { } } - install(id: id) + switch self.dataSource { + case .apple: + install(id: id) + case .xcodeReleases: + installWithoutLogin(id: id) + } } func install(id: Xcode.ID) { @@ -439,6 +453,30 @@ class AppState: ObservableObject { ) } + /// Skips using the username/password to log in to Apple, and simply gets a Auth Cookie used in downloading + func installWithoutLogin(id: Xcode.ID) { + guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return } + + installationPublishers[id] = self.install(.version(availableXcode), downloader: Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2) + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { [unowned self] completion in + self.installationPublishers[id] = nil + if case let .failure(error) = completion { + // Prevent setting the app state error if it is an invalid session, we will present the sign in view instead + if error as? AuthenticationError != .invalidSession { + self.error = error + self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription) + } + if let index = self.allXcodes.firstIndex(where: { $0.id == id }) { + self.allXcodes[index].installState = .notInstalled + } + } + }, + receiveValue: { _ in } + ) + } + func cancelInstall(id: Xcode.ID) { guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return } diff --git a/Xcodes/Backend/AvailableXcode.swift b/Xcodes/Backend/AvailableXcode.swift index cf3f0e4d..d0f877e9 100644 --- a/Xcodes/Backend/AvailableXcode.swift +++ b/Xcodes/Backend/AvailableXcode.swift @@ -14,6 +14,9 @@ public struct AvailableXcode: Codable { public let sdks: SDKs? public let compilers: Compilers? public let fileSize: Int64? + public var downloadPath: String { + return url.path + } public init( version: Version, diff --git a/Xcodes/Backend/Environment.swift b/Xcodes/Backend/Environment.swift index b94fbc7c..9a43ec83 100644 --- a/Xcodes/Backend/Environment.swift +++ b/Xcodes/Backend/Environment.swift @@ -43,7 +43,7 @@ public struct Shell { "--max-connection-per-server=16", "--split=16", "--summary-interval=1", - "--stop-with-process=\(ProcessInfo.processInfo.processIdentifier)", + "--stop-with-process=\(ProcessInfo.processInfo.processIdentifier)", // if xcodes quits, stop aria2 process "--dir=\(destination.parent.string)", "--out=\(destination.basename())", "--human-readable=false", // sets the output to use bytes instead of formatting diff --git a/Xcodes/Backend/URLRequest+Apple.swift b/Xcodes/Backend/URLRequest+Apple.swift index efe9afc8..99e4b6e4 100644 --- a/Xcodes/Backend/URLRequest+Apple.swift +++ b/Xcodes/Backend/URLRequest+Apple.swift @@ -4,6 +4,7 @@ extension URL { static let download = URL(string: "https://developer.apple.com/download")! static let downloads = URL(string: "https://developer.apple.com/services-account/QH65B2/downloadws/listDownloads.action")! static let downloadXcode = URL(string: "https://developer.apple.com/devcenter/download.action")! + static let downloadADCAuth = URL(string: "https://developerservices2.apple.com/services/download")! } extension URLRequest { @@ -25,4 +26,13 @@ extension URLRequest { request.allHTTPHeaderFields?["Accept"] = "*/*" return request } + + static func downloadADCAuth(path: String) -> URLRequest { + var components = URLComponents(url: .downloadADCAuth, resolvingAgainstBaseURL: false)! + components.queryItems = [URLQueryItem(name: "path", value: path)] + var request = URLRequest(url: components.url!) + request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:] + request.allHTTPHeaderFields?["Accept"] = "*/*" + return request + } }