From 5dee615494656d6bffa36d6bb0db7c2d60cda58f Mon Sep 17 00:00:00 2001 From: Adam Price Date: Mon, 12 Dec 2022 19:44:38 +0000 Subject: [PATCH 1/3] Add support for Apple API sessions from Fastlane Spaceship --- Package.resolved | 9 +++ Package.swift | 2 + Sources/XcodesKit/Environment.swift | 2 + Sources/XcodesKit/FastlaneCookieParser.swift | 70 +++++++++++++++++++ .../XcodesKit/FastlaneSessionManager.swift | 60 ++++++++++++++++ Sources/xcodes/App.swift | 21 ++++++ .../Fixtures/FastlaneCookies.yml | 1 + Tests/XcodesKitTests/XcodesKitTests.swift | 29 ++++++++ 8 files changed, 194 insertions(+) create mode 100644 Sources/XcodesKit/FastlaneCookieParser.swift create mode 100644 Sources/XcodesKit/FastlaneSessionManager.swift create mode 100644 Tests/XcodesKitTests/Fixtures/FastlaneCookies.yml diff --git a/Package.resolved b/Package.resolved index 66ec1a9..3caa2fe 100644 --- a/Package.resolved +++ b/Package.resolved @@ -88,6 +88,15 @@ "revision" : "087c91fedc110f9f833b14ef4c32745dabca8913", "version" : "1.0.3" } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams", + "state" : { + "revision" : "01835dc202670b5bb90d07f3eae41867e9ed29f6", + "version" : "5.0.1" + } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index 2fc780f..5e76b81 100644 --- a/Package.swift +++ b/Package.swift @@ -22,6 +22,7 @@ let package = Package( .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", .upToNextMinor(from: "3.2.0")), .package(url: "https://github.com/xcodereleases/data", revision: "fcf527b187817f67c05223676341f3ab69d4214d"), .package(url: "https://github.com/onevcat/Rainbow.git", .upToNextMinor(from: "3.2.0")), + .package(url: "https://github.com/jpsim/Yams", .upToNextMinor(from: "5.0.1")), ], targets: [ .executableTarget( @@ -49,6 +50,7 @@ let package = Package( "Version", .product(name: "XCModel", package: "data"), "Rainbow", + "Yams", ]), .testTarget( name: "XcodesKitTests", diff --git a/Sources/XcodesKit/Environment.swift b/Sources/XcodesKit/Environment.swift index 28b4379..7652c61 100644 --- a/Sources/XcodesKit/Environment.swift +++ b/Sources/XcodesKit/Environment.swift @@ -18,6 +18,7 @@ public struct Environment { public var network = Network() public var logging = Logging() public var keychain = Keychain() + public var fastlaneCookieParser = FastlaneCookieParser() } public var Current = Environment() @@ -204,6 +205,7 @@ public struct Shell { } public struct Files { + public var fileExistsAtPath: (String) -> Bool = { FileManager.default.fileExists(atPath: $0) } public func fileExists(atPath path: String) -> Bool { diff --git a/Sources/XcodesKit/FastlaneCookieParser.swift b/Sources/XcodesKit/FastlaneCookieParser.swift new file mode 100644 index 0000000..65f9529 --- /dev/null +++ b/Sources/XcodesKit/FastlaneCookieParser.swift @@ -0,0 +1,70 @@ +import Foundation +import Yams + +public class FastlaneCookieParser { + public func parse(cookieString: String) throws -> [HTTPCookie] { + let fixed = cookieString.replacingOccurrences(of: "\\n", with: "\n") + let cookies = try YAMLDecoder().decode([FastlaneCookie].self, from: fixed) + return cookies.compactMap(\.httpCookie) + } +} + +struct FastlaneCookie: Decodable { + + enum CodingKeys: String, CodingKey { + case name + case value + case domain + case forDomain = "for_domain" + case path + case secure + case expires + case maxAge = "max_age" + case createdAt = "created_at" + case accessedAt = "accessed_at" + } + + let name: String + let value: String + let domain: String + let forDomain: Bool + let path: String + let secure: Bool + let expires: Date? + let maxAge: Int? + let createdAt: Date + let accessedAt: Date +} + +protocol HTTPCookieConvertible { + var httpCookie: HTTPCookie? { get } +} + +extension FastlaneCookie: HTTPCookieConvertible { + var httpCookie: HTTPCookie? { + + var properties: [HTTPCookiePropertyKey: Any] = [ + .name: self.name, + .value: self.value, + .domain: self.domain, + .path: self.path, + .secure: self.secure, + ] + + if forDomain { + properties[.domain] = ".\(self.domain)" + } else { + properties[.domain] = "\(self.domain)" + } + + if let expires = self.expires { + properties[.expires] = expires + } + + if let maxAge = self.maxAge { + properties[.maximumAge] = maxAge + } + + return HTTPCookie(properties: properties) + } +} diff --git a/Sources/XcodesKit/FastlaneSessionManager.swift b/Sources/XcodesKit/FastlaneSessionManager.swift new file mode 100644 index 0000000..10f4e68 --- /dev/null +++ b/Sources/XcodesKit/FastlaneSessionManager.swift @@ -0,0 +1,60 @@ +import Foundation +import AppleAPI +import Path + +public class FastlaneSessionManager { + + public enum Constants { + public static let fastlaneSessionEnvVarName = "FASTLANE_SESSION" + public static let fastlaneSpaceshipDir = Path.environmentHome.url + .appendingPathComponent(".fastlane") + .appendingPathComponent("spaceship") + } + + public init() {} + + public func setupFastlaneAuth(fastlaneUser: String) { + // Use ephemeral session so that cookies don't conflict with normal usage + AppleAPI.Current.network.session = URLSession(configuration: .ephemeral) + switch fastlaneUser { + case Constants.fastlaneSessionEnvVarName: + importFastlaneCookiesFromEnv() + default: + importFastlaneCookiesFromFile(fastlaneUser: fastlaneUser) + } + } + + private func importFastlaneCookiesFromEnv() { + guard let cookieString = Current.shell.env(Constants.fastlaneSessionEnvVarName) else { + Current.logging.log("\(Constants.fastlaneSessionEnvVarName) not set") + return + } + do { + let cookies = try Current.fastlaneCookieParser.parse(cookieString: cookieString) + cookies.forEach(AppleAPI.Current.network.session.configuration.httpCookieStorage!.setCookie) + } catch { + Current.logging.log("Failed to parse cookies from \(Constants.fastlaneSessionEnvVarName)") + return + } + } + + private func importFastlaneCookiesFromFile(fastlaneUser: String) { + let cookieFilePath = Constants + .fastlaneSpaceshipDir + .appendingPathComponent(fastlaneUser) + .appendingPathComponent("cookie") + guard + let cookieString = try? String(contentsOf: cookieFilePath) + else { + Current.logging.log("Could not read cookies from \(cookieFilePath)") + return + } + do { + let cookies = try Current.fastlaneCookieParser.parse(cookieString: cookieString) + cookies.forEach(AppleAPI.Current.network.session.configuration.httpCookieStorage!.setCookie) + } catch { + Current.logging.log("Failed to parse cookies from \(cookieFilePath)") + return + } + } +} diff --git a/Sources/xcodes/App.swift b/Sources/xcodes/App.swift index 69de488..a64369e 100644 --- a/Sources/xcodes/App.swift +++ b/Sources/xcodes/App.swift @@ -65,12 +65,14 @@ struct Xcodes: AsyncParsableCommand { static let runtimeList = RuntimeList() static var runtimeInstaller: RuntimeInstaller! static var xcodeInstaller: XcodeInstaller! + static var fastlaneSessionManager: FastlaneSessionManager! static func main() async { try? xcodesConfiguration.load() sessionService = AppleSessionService(configuration: xcodesConfiguration) xcodeInstaller = XcodeInstaller(xcodeList: xcodeList, sessionService: sessionService) runtimeInstaller = RuntimeInstaller(runtimeList: runtimeList, sessionService: sessionService) + fastlaneSessionManager = FastlaneSessionManager() migrateApplicationSupportFiles() do { var command = try parseAsRoot() @@ -120,6 +122,13 @@ struct Xcodes: AsyncParsableCommand { completion: .directory) var directory: String? + @Flag(help: "Use fastlane spaceship session.") + var useFastlaneAuth: Bool = false + + @Option(help: "The fastlane spaceship user", + completion: .shellCommand("ls \(FastlaneSessionManager.Constants.fastlaneSpaceshipDir)")) + var fastlaneUser: String = FastlaneSessionManager.Constants.fastlaneSessionEnvVarName + @OptionGroup var globalDataSource: GlobalDataSourceOption @@ -150,6 +159,10 @@ struct Xcodes: AsyncParsableCommand { let destination = getDirectory(possibleDirectory: directory, default: .environmentDownloads) + if useFastlaneAuth { + fastlaneSessionManager.setupFastlaneAuth(fastlaneUser: fastlaneUser) + } + xcodeInstaller.download(installation, dataSource: globalDataSource.dataSource, downloader: downloader, destinationDirectory: destination) .catch { error in Install.processDownloadOrInstall(error: error) @@ -219,6 +232,13 @@ struct Xcodes: AsyncParsableCommand { completion: .directory) var directory: String? + @Flag(help: "Use fastlane spaceship session.") + var useFastlaneAuth: Bool = false + + @Option(help: "The fastlane spaceship user.", + completion: .shellCommand("ls \(FastlaneSessionManager.Constants.fastlaneSpaceshipDir)")) + var fastlaneUser: String = FastlaneSessionManager.Constants.fastlaneSessionEnvVarName + @OptionGroup var globalDataSource: GlobalDataSourceOption @@ -268,6 +288,7 @@ struct Xcodes: AsyncParsableCommand { using downloader: Downloader, to destination: Path) { firstly { () -> Promise in + if useFastlaneAuth { fastlaneSessionManager.setupFastlaneAuth(fastlaneUser: fastlaneUser) } // update the list before installing only for version type because the other types already update internally if update, case .version = installation { Current.logging.log("Updating...") diff --git a/Tests/XcodesKitTests/Fixtures/FastlaneCookies.yml b/Tests/XcodesKitTests/Fixtures/FastlaneCookies.yml new file mode 100644 index 0000000..9cc6258 --- /dev/null +++ b/Tests/XcodesKitTests/Fixtures/FastlaneCookies.yml @@ -0,0 +1 @@ +---\n- !ruby/object:HTTP::Cookie\n name: myacinfo\n value: myacinfo_dummy\n domain: apple.com\n for_domain: true\n path: "/"\n secure: true\n httponly: true\n expires: \n max_age: \n created_at: 2022-12-12 16:29:24.988867000 +00:00\n accessed_at: 2022-12-12 16:29:24.990398000 +00:00\n- !ruby/object:HTTP::Cookie\n name: DES5a8153bb0fcd039d87286b59d3accea2\n value: DES5a8153bb0fcd039d87286b59d3accea2_dummy\n domain: idmsa.apple.com\n for_domain: true\n path: "/"\n secure: true\n httponly: true\n expires: \n max_age: 2592000\n created_at: 2022-12-09 14:20:51.920971000 +00:00\n accessed_at: 2022-12-12 16:29:23.114185000 +00:00\n- !ruby/object:HTTP::Cookie\n name: dqsid\n value: dqsid_dummy\n domain: appstoreconnect.apple.com\n for_domain: false\n path: "/"\n secure: true\n httponly: true\n expires: \n max_age: 1800\n created_at: &1 2022-12-12 16:29:26.523387000 +00:00\n accessed_at: *1\n diff --git a/Tests/XcodesKitTests/XcodesKitTests.swift b/Tests/XcodesKitTests/XcodesKitTests.swift index f2d5f01..4b1954a 100644 --- a/Tests/XcodesKitTests/XcodesKitTests.swift +++ b/Tests/XcodesKitTests/XcodesKitTests.swift @@ -1450,4 +1450,33 @@ final class XcodesKitTests: XCTestCase { XCTAssert(xcodesList.availableXcodes == [Self.mockXcode]) } + + func test_FastlaneCookieParser_ShouldParseCookies() throws { + let url = Bundle.module.url(forResource: "FastlaneCookies", withExtension: "yml", subdirectory: "Fixtures")! + + let cookieString = try String(contentsOf: url) + + let parser = FastlaneCookieParser() + let cookies = try parser.parse(cookieString: cookieString) + + XCTAssertEqual(cookies.count, 3) + + XCTAssertEqual(cookies[0].name, "myacinfo") + XCTAssertEqual(cookies[0].value, "myacinfo_dummy") + XCTAssertEqual(cookies[0].domain, ".apple.com") + XCTAssertEqual(cookies[0].path, "/") + XCTAssertEqual(cookies[0].isSecure, true) + + XCTAssertEqual(cookies[1].name, "DES5a8153bb0fcd039d87286b59d3accea2") + XCTAssertEqual(cookies[1].value, "DES5a8153bb0fcd039d87286b59d3accea2_dummy") + XCTAssertEqual(cookies[1].domain, ".idmsa.apple.com") + XCTAssertEqual(cookies[1].path, "/") + XCTAssertEqual(cookies[1].isSecure, true) + + XCTAssertEqual(cookies[2].name, "dqsid") + XCTAssertEqual(cookies[2].value, "dqsid_dummy") + XCTAssertEqual(cookies[2].domain, "appstoreconnect.apple.com") + XCTAssertEqual(cookies[2].path, "/") + XCTAssertEqual(cookies[2].isSecure, true) + } } From f2d083a534416c5c7904da51ad2c52f2d28a3da6 Mon Sep 17 00:00:00 2001 From: Adam Price Date: Thu, 9 Feb 2023 12:05:41 -0500 Subject: [PATCH 2/3] Remove new line --- Sources/XcodesKit/Environment.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/XcodesKit/Environment.swift b/Sources/XcodesKit/Environment.swift index 7652c61..623a900 100644 --- a/Sources/XcodesKit/Environment.swift +++ b/Sources/XcodesKit/Environment.swift @@ -205,7 +205,6 @@ public struct Shell { } public struct Files { - public var fileExistsAtPath: (String) -> Bool = { FileManager.default.fileExists(atPath: $0) } public func fileExists(atPath path: String) -> Bool { From 0ed06ccb3b8a4ddfd23fff30633868bbb2d9668f Mon Sep 17 00:00:00 2001 From: Adam Price Date: Tue, 14 Feb 2023 14:03:20 -0500 Subject: [PATCH 3/3] Added .red to error logs --- Sources/XcodesKit/FastlaneSessionManager.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/XcodesKit/FastlaneSessionManager.swift b/Sources/XcodesKit/FastlaneSessionManager.swift index 10f4e68..e4c2de7 100644 --- a/Sources/XcodesKit/FastlaneSessionManager.swift +++ b/Sources/XcodesKit/FastlaneSessionManager.swift @@ -26,14 +26,14 @@ public class FastlaneSessionManager { private func importFastlaneCookiesFromEnv() { guard let cookieString = Current.shell.env(Constants.fastlaneSessionEnvVarName) else { - Current.logging.log("\(Constants.fastlaneSessionEnvVarName) not set") + Current.logging.log("\(Constants.fastlaneSessionEnvVarName) not set".red) return } do { let cookies = try Current.fastlaneCookieParser.parse(cookieString: cookieString) cookies.forEach(AppleAPI.Current.network.session.configuration.httpCookieStorage!.setCookie) } catch { - Current.logging.log("Failed to parse cookies from \(Constants.fastlaneSessionEnvVarName)") + Current.logging.log("Failed to parse cookies from \(Constants.fastlaneSessionEnvVarName)".red) return } } @@ -46,14 +46,14 @@ public class FastlaneSessionManager { guard let cookieString = try? String(contentsOf: cookieFilePath) else { - Current.logging.log("Could not read cookies from \(cookieFilePath)") + Current.logging.log("Could not read cookies from \(cookieFilePath)".red) return } do { let cookies = try Current.fastlaneCookieParser.parse(cookieString: cookieString) cookies.forEach(AppleAPI.Current.network.session.configuration.httpCookieStorage!.setCookie) } catch { - Current.logging.log("Failed to parse cookies from \(cookieFilePath)") + Current.logging.log("Failed to parse cookies from \(cookieFilePath)".red) return } }