Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Sources/XKit/GrandSlam/GrandSlamClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ struct GrandSlamClient: Sendable {
}
request.configure(request: &httpRequest, deviceInfo: deviceInfo, anisetteData: anisetteData)

let resp = try await httpClient.makeRequest(httpRequest, body: body)
// GrandSlam doesn't seem to like reused (keep-alive) connections so create a
// new one for each request. cf https://github.com/rileytestut/AltSign/pull/52
let resp = try await httpClient.withEphemeralClient {
try await $0.makeRequest(httpRequest, body: body)
}
return try R.Decoder.decode(data: resp.body)
}

Expand Down
37 changes: 27 additions & 10 deletions Sources/XKit/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,39 @@ import OpenAPIAsyncHTTPClient
import Dependencies

extension HTTPClientDependencyKey: DependencyKey {
public static let liveValue: HTTPClientProtocol = {
public static let liveValue: HTTPClientProtocol = Client()
}

private struct Client: HTTPClientProtocol {
private static let tlsConfiguration: TLSConfiguration = {
// if ssl cert parsing fails we're screwed so we might as well force try
// swiftlint:disable:next force_try
let appleRootCA = try! NIOSSLCertificate(bytes: Array(appleRootPEM.utf8), format: .pem)
var tlsConfiguration: TLSConfiguration = .makeClientConfiguration()
tlsConfiguration.additionalTrustRoots = [.certificates([appleRootCA])]
return tlsConfiguration
}()

var client: HTTPClient

init() {
var config = HTTPClient.Configuration(
tlsConfiguration: tlsConfiguration,
tlsConfiguration: Self.tlsConfiguration,
decompression: .enabled(limit: .none)
)
config.timeout.connect = .seconds(60)
return HTTPClient(configuration: config)
}()
}
self.client = HTTPClient(configuration: config)
}

extension HTTPClient: HTTPClientProtocol {
public func makeWebSocket(url: URL) async throws -> any WebSocketSession {
func makeWebSocket(url: URL) async throws -> any WebSocketSession {
let (stream, continuation) = AsyncStream.makeStream(of: WebSocketSessionWrapper.self)
async let value = stream.first(where: { _ in true })
// must be after the `async let` so that we finish if connect throws
defer { continuation.finish() }
// we can't use the async overload because we need to immediately subscribe
// to onText/onBinary in the same EventLoop tick that the WebSocket is created.
// This is also why we create the SessionWrapper inside the closure.
let future = WebSocket.connect(to: url, on: eventLoopGroup) {
let future = WebSocket.connect(to: url, on: client.eventLoopGroup) {
continuation.yield(WebSocketSessionWrapper(webSocket: $0))
}
try await future.get()
Expand All @@ -56,8 +64,17 @@ extension HTTPClient: HTTPClientProtocol {
case connectFailed
}

public var asOpenAPITransport: any ClientTransport {
AsyncHTTPClientTransport(configuration: .init(client: self))
var asOpenAPITransport: any ClientTransport {
AsyncHTTPClientTransport(configuration: .init(client: client))
}

func withEphemeralClient<T>(
perform: (any HTTPClientProtocol) async throws -> T
) async throws -> T {
let ephemeralClient = Client()
let result = await Result { try await perform(ephemeralClient) }
try? await ephemeralClient.client.shutdown()
return try result.get()
}
}

Expand Down
12 changes: 11 additions & 1 deletion Sources/XKit/HTTPClientProtocol/HTTPClientProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ public protocol HTTPClientProtocol: Sendable {
var asOpenAPITransport: ClientTransport { get }

func makeWebSocket(url: URL) async throws -> WebSocketSession

func withEphemeralClient<T>(
perform: (any HTTPClientProtocol) async throws -> T
) async throws -> T
}

extension HTTPClientProtocol {
Expand Down Expand Up @@ -46,7 +50,7 @@ extension HTTPClientProtocol {
) async throws -> (response: HTTPResponse, body: Data) {
await onProgress(0)
let (response, responseBody) = try await send(request, body: body.map { HTTPBody($0) })
guard !requireHTTPSuccess || response.status.kind == .successful else {
guard !requireHTTPSuccess || ![.clientError, .serverError].contains(response.status.kind) else {
let errorBody = (try? await responseBody.collect()) ?? Data()
throw HTTPResponseError(
method: request.method,
Expand Down Expand Up @@ -128,6 +132,12 @@ private struct UnimplementedHTTPClient: HTTPClientProtocol, ClientTransport {
return try closure()
}

func withEphemeralClient<T>(
perform: (any HTTPClientProtocol) async throws -> T
) async throws -> T {
try await perform(self)
}

public func makeWebSocket(url: URL) async throws -> any WebSocketSession {
let closure: (URL) async throws -> any WebSocketSession = unimplemented()
return try await closure(url)
Expand Down
9 changes: 9 additions & 0 deletions Sources/XKit/HTTPClientProtocol/URLSession+HTTP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ private struct Client: HTTPClientProtocol {
URLSessionTransport(configuration: .init(session: session))
}

func withEphemeralClient<T>(
perform: (any HTTPClientProtocol) async throws -> T
) async throws -> T {
let ephemeralClient = Client()
let result = await Result { try await perform(ephemeralClient) }
ephemeralClient.session.finishTasksAndInvalidate()
return try result.get()
}

public func makeWebSocket(url: URL) async throws -> any WebSocketSession {
let task = session.webSocketTask(with: url)
let (event, eventContinuation) = AsyncStream<URLSessionWebSocketTask.CloseCode?>.makeStream()
Expand Down