Skip to content

Commit

Permalink
Add support for Apple API sessions from Fastlane Spaceship
Browse files Browse the repository at this point in the history
  • Loading branch information
adamprice committed Jan 30, 2023
1 parent a2039d6 commit 5dee615
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 0 deletions.
9 changes: 9 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -49,6 +50,7 @@ let package = Package(
"Version",
.product(name: "XCModel", package: "data"),
"Rainbow",
"Yams",
]),
.testTarget(
name: "XcodesKitTests",
Expand Down
2 changes: 2 additions & 0 deletions Sources/XcodesKit/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down
70 changes: 70 additions & 0 deletions Sources/XcodesKit/FastlaneCookieParser.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
60 changes: 60 additions & 0 deletions Sources/XcodesKit/FastlaneSessionManager.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
21 changes: 21 additions & 0 deletions Sources/xcodes/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -268,6 +288,7 @@ struct Xcodes: AsyncParsableCommand {
using downloader: Downloader,
to destination: Path) {
firstly { () -> Promise<InstalledXcode> 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...")
Expand Down
1 change: 1 addition & 0 deletions Tests/XcodesKitTests/Fixtures/FastlaneCookies.yml
Original file line number Diff line number Diff line change
@@ -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
29 changes: 29 additions & 0 deletions Tests/XcodesKitTests/XcodesKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

0 comments on commit 5dee615

Please sign in to comment.