Skip to content

Commit

Permalink
Implement Vapor.Client conformance for URLSession and use a client ba…
Browse files Browse the repository at this point in the history
…sed on that throughout OneRoster (due to incompatibilities between certain OneRoster servers and AHC's stricter standards compliance). (#8)
  • Loading branch information
gwynne authored Jul 25, 2022
1 parent f90e2c0 commit 6a594ea
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 12 deletions.
2 changes: 1 addition & 1 deletion Sources/OneRoster/Client/Application+OneRosterClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ extension Application {
/// missing, it will be added for all OneRoster requests. **However**, if the suffix _is_ provided, it is stripped
/// for requests relating to authorization, such as OAuth 2 token grant requests.
public func oneRoster(baseUrl: URL) -> OneRosterClient {
return OneRosterClient(baseUrl: baseUrl, client: self.client, logger: self.logger)
return OneRosterClient(baseUrl: baseUrl, client: self.sharedUrlSessionClient, logger: self.logger)
}

/// Get a `OneRosterClient` suitable for making OneRoster requests to the given base URL using OAuth1 authentication
Expand Down
9 changes: 4 additions & 5 deletions Sources/OneRoster/Client/OAuth1.swift
Original file line number Diff line number Diff line change
Expand Up @@ -224,28 +224,27 @@ extension MessageAuthenticationCode {
extension Application {
/// Get an `OAuth1Client` suitable for automatically calculating an OAuth 1 signature for each request.
///
/// Uses the application's default `Client` and `Logger`.
/// Uses the application's shared `URLSesssion` client and default `Logger`.
///
/// - Parameters:
/// - parameters: A set of OAuth 1 parameters containing the neceessary information (including sensitive
/// credentials such as a client secret) for signing requests. Credentials are retained in memory for
/// the lifetime of the client, due to the need to reuse them for each request.
public func oauth1(parameters: OAuth1.Parameters) -> OAuth1.Client {
OAuth1.Client(client: self.client, logger: self.logger, parameters: parameters)
OAuth1.Client(client: self.sharedUrlSessionClient, logger: self.logger, parameters: parameters)
}
}

extension Request {
/// Get an `OAuth1Client` suitable for automatically calculating an OAuth 1 signature for each request.
///
/// Uses the request's default `Client` and `Logger`.
/// Uses the request's shared `URLSesssion` client and default `Logger`.
///
/// - Parameters:
/// - Parameters:
/// - parameters: A set of OAuth 1 parameters containing the neceessary information (including sensitive
/// credentials such as a client secret) for signing requests. Credentials are retained in memory for
/// the lifetime of the client, due to the need to reuse them for each request.
public func oauth1(parameters: OAuth1.Parameters) -> OAuth1.Client {
OAuth1.Client(client: self.client, logger: self.logger, parameters: parameters)
OAuth1.Client(client: self.sharedUrlSessionClient, logger: self.logger, parameters: parameters)
}
}
8 changes: 4 additions & 4 deletions Sources/OneRoster/Client/OAuth2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ public enum OAuth2 {
extension Application {
/// Get an `OAuth2Client` suitable for automatically obtaining access tokens as needed to fulfill each request.
///
/// Uses the application's default `Client` and `Logger`.
/// Uses the application's shared `URLSesssion` client and default `Logger`.
///
/// - Parameters:
/// - parameters: A closure which returns a future whose value is a set of OAuth 2 parameters containing the
Expand All @@ -265,14 +265,14 @@ extension Application {
/// as long as required to make a request for an access token. The closure may return `nil` to indicate that
/// the appropriate parameters are no longer available.
public func oauth2(parameters: @escaping (OAuth2.Client) -> EventLoopFuture<OAuth2.Parameters?>) -> OAuth2.Client {
OAuth2.Client(client: self.client, logger: self.logger, parametersCallback: parameters)
OAuth2.Client(client: self.sharedUrlSessionClient, logger: self.logger, parametersCallback: parameters)
}
}

extension Request {
/// Get an `OAuth2Client` suitable for automatically obtaining access tokens as needed to fulfill each request.
///
/// Uses the request's default `Client` and `Logger`.
/// Uses the request's shared `URLSesssion` client and default `Logger`.
///
/// - Parameters:
/// - parameters: A closure which returns a future whose value is a set of OAuth 2 parameters containing the
Expand All @@ -281,6 +281,6 @@ extension Request {
/// as long as required to make a request for an access token. The closure may return `nil` to indicate that
/// the appropriate parameters are no longer available.
public func oauth2(parameters: @escaping (OAuth2.Client) -> EventLoopFuture<OAuth2.Parameters?>) -> OAuth2.Client {
OAuth2.Client(client: self.client, logger: self.logger, parametersCallback: parameters)
OAuth2.Client(client: self.sharedUrlSessionClient, logger: self.logger, parametersCallback: parameters)
}
}
4 changes: 2 additions & 2 deletions Sources/OneRoster/Client/Request+OneRosterClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import Vapor
extension Request {
/// Get a `OneRosterClient` suitable for making OneRoster requests to the given base URL without authentication.
///
/// Uses the request's default `Client` and `Logger`.
/// Uses the request's shared `URLSesssion` client and default `Logger`.
///
/// - Important: The base URL is allowed to be either a true "base" (the root to which the OneRoster RESTful path
/// and version should be appended, i.e. <https://example.com/oneroster>) or the RESTful base (e.g.
Expand All @@ -27,7 +27,7 @@ extension Request {
/// missing, it will be added for all OneRoster requests. **However**, if the suffix _is_ provided, it is stripped
/// for requests relating to authorization, such as OAuth 2 token grant requests.
public func oneRoster(baseUrl: URL) -> OneRosterClient {
return OneRosterClient(baseUrl: baseUrl, client: self.client, logger: self.logger)
return OneRosterClient(baseUrl: baseUrl, client: self.sharedUrlSessionClient, logger: self.logger)
}

/// Get a `OneRosterClient` suitable for making OneRoster requests to the given base URL using OAuth1 authentication
Expand Down
81 changes: 81 additions & 0 deletions Sources/OneRoster/Client/URLSessionClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import Vapor
import NIOCore
import Logging
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

internal struct URLSessionClient: Vapor.Client {
let session: URLSession
let eventLoop: EventLoop
let logger: Logger

static func shared(on eventLoop: EventLoop, logger: Logger) -> Client {
URLSessionClient(session: .shared, eventLoop: eventLoop, logger: logger)
}

func delegating(to eventLoop: EventLoop) -> Client {
URLSessionClient(session: self.session, eventLoop: eventLoop, logger: self.logger)
}

func logging(to logger: Logger) -> Client {
URLSessionClient(session: self.session, eventLoop: self.eventLoop, logger: logger)
}

func send(_ request: ClientRequest) -> EventLoopFuture<ClientResponse> {
guard let foundationUrl = URL(string: request.url.string) else {
return self.eventLoop.makeFailedFuture(Abort(.internalServerError, reason: "Client request with invalid URL"))
}

var foundationRequest = URLRequest(url: foundationUrl)

request.headers.forEach { foundationRequest.addValue($1, forHTTPHeaderField: $0) }
foundationRequest.httpBody = request.body.map { Data($0.readableBytesView) }
foundationRequest.httpMethod = request.method.string
foundationRequest.cachePolicy = .reloadIgnoringLocalCacheData

let promise = self.eventLoop.makePromise(of: ClientResponse.self)

let dataTask = self.session.dataTask(with: foundationRequest, completionHandler: { data, response, error in
if let error = error {
return promise.fail(error)
}
guard let response = response as? HTTPURLResponse else {
return promise.fail(Abort(.internalServerError, reason: "Client received no error but wrong kind of response"))
}

let clientResponse = ClientResponse(
status: HTTPStatus(statusCode: response.statusCode),
headers: .init(response.allHeaderFields.compactMapValues { $0 as? String }.compactMap { k, v in (k.base as? String).map { ($0, v) } }),
body: data.map { .init(data: $0) }
)
return promise.succeed(clientResponse)
})

dataTask.resume()
return promise.futureResult
}
}

extension Application.Clients.Provider {
public static var sharedUrlSession: Self {
.init {
$0.clients.use {
URLSessionClient.shared(on: $0.eventLoopGroup.any(), logger: $0.logger)
}
}
}
}

extension Application {
public var sharedUrlSessionClient: Client {
URLSessionClient.shared(on: self.eventLoopGroup.any(), logger: self.logger)
}
}

extension Request {
public var sharedUrlSessionClient: Client {
URLSessionClient.shared(on: self.eventLoop, logger: self.logger)
}
}

0 comments on commit 6a594ea

Please sign in to comment.