//===----------------------------------------------------------------------===// // // This source file is part of the AsyncHTTPClient open source project // // Copyright (c) 2018-2019 Apple Inc. and the AsyncHTTPClient project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information // See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// import Foundation import Logging import NIOConcurrencyHelpers import NIOCore import NIOHTTP1 import NIOSSL extension HTTPClient { /// Represent request body. public struct Body { /// Chunk provider. public struct StreamWriter { let closure: (IOData) -> EventLoopFuture /// Create new StreamWriter /// /// - parameters: /// - closure: function that will be called to write actual bytes to the channel. public init(closure: @escaping (IOData) -> EventLoopFuture) { self.closure = closure } /// Write data to server. /// /// - parameters: /// - data: `IOData` to write. public func write(_ data: IOData) -> EventLoopFuture { return self.closure(data) } } /// Body size. if nil,`Transfer-Encoding` will automatically be set to `chunked`. Otherwise a `Content-Length` /// header is set with the given `length`. public var length: Int? /// Body chunk provider. public var stream: (StreamWriter) -> EventLoopFuture /// Create and stream body using `ByteBuffer`. /// /// - parameters: /// - buffer: Body `ByteBuffer` representation. public static func byteBuffer(_ buffer: ByteBuffer) -> Body { return Body(length: buffer.readableBytes) { writer in writer.write(.byteBuffer(buffer)) } } /// Create and stream body using `StreamWriter`. /// /// - parameters: /// - length: Body size. If nil, `Transfer-Encoding` will automatically be set to `chunked`. Otherwise a `Content-Length` /// header is set with the given `length`. /// - stream: Body chunk provider. public static func stream(length: Int? = nil, _ stream: @escaping (StreamWriter) -> EventLoopFuture) -> Body { return Body(length: length, stream: stream) } /// Create and stream body using `Data`. /// /// - parameters: /// - data: Body `Data` representation. public static func data(_ data: Data) -> Body { return Body(length: data.count) { writer in writer.write(.byteBuffer(ByteBuffer(bytes: data))) } } /// Create and stream body using `String`. /// /// - parameters: /// - string: Body `String` representation. public static func string(_ string: String) -> Body { return Body(length: string.utf8.count) { writer in writer.write(.byteBuffer(ByteBuffer(string: string))) } } } /// Represent HTTP request. public struct Request { /// Represent kind of Request enum Kind: Equatable { enum UnixScheme: Equatable { case baseURL case http_unix case https_unix } /// Remote host request. case host /// UNIX Domain Socket HTTP request. case unixSocket(_ scheme: UnixScheme) private static var hostRestrictedSchemes: Set = ["http", "https"] private static var allSupportedSchemes: Set = ["http", "https", "unix", "http+unix", "https+unix"] init(forScheme scheme: String) throws { switch scheme { case "http", "https": self = .host case "unix": self = .unixSocket(.baseURL) case "http+unix": self = .unixSocket(.http_unix) case "https+unix": self = .unixSocket(.https_unix) default: throw HTTPClientError.unsupportedScheme(scheme) } } func hostFromURL(_ url: URL) throws -> String { switch self { case .host: guard let host = url.host else { throw HTTPClientError.emptyHost } return host case .unixSocket: return "" } } func socketPathFromURL(_ url: URL) throws -> String { switch self { case .unixSocket(.baseURL): return url.baseURL?.path ?? url.path case .unixSocket: guard let socketPath = url.host else { throw HTTPClientError.missingSocketPath } return socketPath case .host: return "" } } func uriFromURL(_ url: URL) -> String { switch self { case .host: return url.uri case .unixSocket(.baseURL): return url.baseURL != nil ? url.uri : "/" case .unixSocket: return url.uri } } func supportsRedirects(to scheme: String?) -> Bool { guard let scheme = scheme?.lowercased() else { return false } switch self { case .host: return Kind.hostRestrictedSchemes.contains(scheme) case .unixSocket: return Kind.allSupportedSchemes.contains(scheme) } } } /// Request HTTP method, defaults to `GET`. public let method: HTTPMethod /// Remote URL. public let url: URL /// Remote HTTP scheme, resolved from `URL`. public let scheme: String /// Remote host, resolved from `URL`. public let host: String /// Socket path, resolved from `URL`. let socketPath: String /// URI composed of the path and query, resolved from `URL`. let uri: String /// Request custom HTTP Headers, defaults to no headers. public var headers: HTTPHeaders /// Request body, defaults to no body. public var body: Body? /// Request-specific TLS configuration, defaults to no request-specific TLS configuration. public var tlsConfiguration: TLSConfiguration? struct RedirectState { var count: Int var visited: Set? } var redirectState: RedirectState? let kind: Kind /// Create HTTP request. /// /// - parameters: /// - url: Remote `URL`. /// - version: HTTP version. /// - method: HTTP method. /// - headers: Custom HTTP headers. /// - body: Request body. /// - throws: /// - `invalidURL` if URL cannot be parsed. /// - `emptyScheme` if URL does not contain HTTP scheme. /// - `unsupportedScheme` if URL does contains unsupported HTTP scheme. /// - `emptyHost` if URL does not contains a host. public init(url: String, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: Body? = nil) throws { try self.init(url: url, method: method, headers: headers, body: body, tlsConfiguration: nil) } /// Create HTTP request. /// /// - parameters: /// - url: Remote `URL`. /// - version: HTTP version. /// - method: HTTP method. /// - headers: Custom HTTP headers. /// - body: Request body. /// - tlsConfiguration: Request TLS configuration /// - throws: /// - `invalidURL` if URL cannot be parsed. /// - `emptyScheme` if URL does not contain HTTP scheme. /// - `unsupportedScheme` if URL does contains unsupported HTTP scheme. /// - `emptyHost` if URL does not contains a host. public init(url: String, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: Body? = nil, tlsConfiguration: TLSConfiguration?) throws { guard let url = URL(string: url) else { throw HTTPClientError.invalidURL } try self.init(url: url, method: method, headers: headers, body: body, tlsConfiguration: tlsConfiguration) } /// Create an HTTP `Request`. /// /// - parameters: /// - url: Remote `URL`. /// - method: HTTP method. /// - headers: Custom HTTP headers. /// - body: Request body. /// - throws: /// - `emptyScheme` if URL does not contain HTTP scheme. /// - `unsupportedScheme` if URL does contains unsupported HTTP scheme. /// - `emptyHost` if URL does not contains a host. /// - `missingSocketPath` if URL does not contains a socketPath as an encoded host. public init(url: URL, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: Body? = nil) throws { try self.init(url: url, method: method, headers: headers, body: body, tlsConfiguration: nil) } /// Create an HTTP `Request`. /// /// - parameters: /// - url: Remote `URL`. /// - method: HTTP method. /// - headers: Custom HTTP headers. /// - body: Request body. /// - tlsConfiguration: Request TLS configuration /// - throws: /// - `emptyScheme` if URL does not contain HTTP scheme. /// - `unsupportedScheme` if URL does contains unsupported HTTP scheme. /// - `emptyHost` if URL does not contains a host. /// - `missingSocketPath` if URL does not contains a socketPath as an encoded host. public init(url: URL, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: Body? = nil, tlsConfiguration: TLSConfiguration?) throws { guard let scheme = url.scheme?.lowercased() else { throw HTTPClientError.emptyScheme } self.kind = try Kind(forScheme: scheme) self.host = try self.kind.hostFromURL(url) self.socketPath = try self.kind.socketPathFromURL(url) self.uri = self.kind.uriFromURL(url) self.redirectState = nil self.url = url self.method = method self.scheme = scheme self.headers = headers self.body = body self.tlsConfiguration = tlsConfiguration } /// Whether request will be executed using secure socket. public var useTLS: Bool { return self.scheme == "https" || self.scheme == "https+unix" } /// Resolved port. public var port: Int { return self.url.port ?? (self.useTLS ? 443 : 80) } func createRequestHead() throws -> (HTTPRequestHead, RequestFramingMetadata) { var head = HTTPRequestHead( version: .http1_1, method: self.method, uri: self.uri, headers: self.headers ) if !head.headers.contains(name: "host") { let port = self.port var host = self.host if !(port == 80 && self.scheme == "http"), !(port == 443 && self.scheme == "https") { host += ":\(port)" } head.headers.add(name: "host", value: host) } let metadata = try head.headers.validateAndSetTransportFraming(method: self.method, bodyLength: .init(self.body)) return (head, metadata) } } /// Represent HTTP response. public struct Response { /// Remote host of the request. public var host: String /// Response HTTP status. public var status: HTTPResponseStatus /// Response HTTP version. public var version: HTTPVersion /// Reponse HTTP headers. public var headers: HTTPHeaders /// Response body. public var body: ByteBuffer? /// Create HTTP `Response`. /// /// - parameters: /// - host: Remote host of the request. /// - status: Response HTTP status. /// - headers: Reponse HTTP headers. /// - body: Response body. @available(*, deprecated, renamed: "init(host:status:version:headers:body:)") public init(host: String, status: HTTPResponseStatus, headers: HTTPHeaders, body: ByteBuffer?) { self.host = host self.status = status self.version = HTTPVersion(major: 1, minor: 1) self.headers = headers self.body = body } /// Create HTTP `Response`. /// /// - parameters: /// - host: Remote host of the request. /// - status: Response HTTP status. /// - version: Response HTTP version. /// - headers: Reponse HTTP headers. /// - body: Response body. public init(host: String, status: HTTPResponseStatus, version: HTTPVersion, headers: HTTPHeaders, body: ByteBuffer?) { self.host = host self.status = status self.version = version self.headers = headers self.body = body } } /// HTTP authentication public struct Authorization: Hashable { private enum Scheme: Hashable { case Basic(String) case Bearer(String) } private let scheme: Scheme private init(scheme: Scheme) { self.scheme = scheme } public static func basic(username: String, password: String) -> HTTPClient.Authorization { return .basic(credentials: Data("\(username):\(password)".utf8).base64EncodedString()) } public static func basic(credentials: String) -> HTTPClient.Authorization { return .init(scheme: .Basic(credentials)) } public static func bearer(tokens: String) -> HTTPClient.Authorization { return .init(scheme: .Bearer(tokens)) } public var headerValue: String { switch self.scheme { case .Basic(let credentials): return "Basic \(credentials)" case .Bearer(let tokens): return "Bearer \(tokens)" } } } } public class ResponseAccumulator: HTTPClientResponseDelegate { public typealias Response = HTTPClient.Response enum State { case idle case head(HTTPResponseHead) case body(HTTPResponseHead, ByteBuffer) case end case error(Error) } var state = State.idle let request: HTTPClient.Request public init(request: HTTPClient.Request) { self.request = request } public func didReceiveHead(task: HTTPClient.Task, _ head: HTTPResponseHead) -> EventLoopFuture { switch self.state { case .idle: self.state = .head(head) case .head: preconditionFailure("head already set") case .body: preconditionFailure("no head received before body") case .end: preconditionFailure("request already processed") case .error: break } return task.eventLoop.makeSucceededFuture(()) } public func didReceiveBodyPart(task: HTTPClient.Task, _ part: ByteBuffer) -> EventLoopFuture { switch self.state { case .idle: preconditionFailure("no head received before body") case .head(let head): self.state = .body(head, part) case .body(let head, var body): // The compiler can't prove that `self.state` is dead here (and it kinda isn't, there's // a cross-module call in the way) so we need to drop the original reference to `body` in // `self.state` or we'll get a CoW. To fix that we temporarily set the state to `.end` (which // has no associated data). We'll fix it at the bottom of this block. self.state = .end var part = part body.writeBuffer(&part) self.state = .body(head, body) case .end: preconditionFailure("request already processed") case .error: break } return task.eventLoop.makeSucceededFuture(()) } public func didReceiveError(task: HTTPClient.Task, _ error: Error) { self.state = .error(error) } public func didFinishRequest(task: HTTPClient.Task) throws -> Response { switch self.state { case .idle: preconditionFailure("no head received before end") case .head(let head): return Response(host: self.request.host, status: head.status, version: head.version, headers: head.headers, body: nil) case .body(let head, let body): return Response(host: self.request.host, status: head.status, version: head.version, headers: head.headers, body: body) case .end: preconditionFailure("request already processed") case .error(let error): throw error } } } /// `HTTPClientResponseDelegate` allows an implementation to receive notifications about request processing and to control how response parts are processed. /// You can implement this protocol if you need fine-grained control over an HTTP request/response, for example, if you want to inspect the response /// headers before deciding whether to accept a response body, or if you want to stream your request body. Pass an instance of your conforming /// class to the `HTTPClient.execute()` method and this package will call each delegate method appropriately as the request takes place./ /// /// ### Backpressure /// /// A `HTTPClientResponseDelegate` can be used to exert backpressure on the server response. This is achieved by way of the futures returned from /// `didReceiveHead` and `didReceiveBodyPart`. The following functions are part of the "backpressure system" in the delegate: /// /// - `didReceiveHead` /// - `didReceiveBodyPart` /// - `didFinishRequest` /// - `didReceiveError` /// /// The first three methods are strictly _exclusive_, with that exclusivity managed by the futures returned by `didReceiveHead` and /// `didReceiveBodyPart`. What this means is that until the returned future is completed, none of these three methods will be called /// again. This allows delegates to rate limit the server to a capacity it can manage. `didFinishRequest` does not return a future, /// as we are expecting no more data from the server at this time. /// /// `didReceiveError` is somewhat special: it signals the end of this regime. `didRecieveError` is not exclusive: it may be called at /// any time, even if a returned future is not yet completed. `didReceiveError` is terminal, meaning that once it has been called none /// of these four methods will be called again. This can be used as a signal to abandon all outstanding work. /// /// - note: This delegate is strongly held by the `HTTPTaskHandler` /// for the duration of the `Request` processing and will be /// released together with the `HTTPTaskHandler` when channel is closed. /// Users of the library are not required to keep a reference to the /// object that implements this protocol, but may do so if needed. public protocol HTTPClientResponseDelegate: AnyObject { associatedtype Response /// Called when the request head is sent. Will be called once. /// /// - parameters: /// - task: Current request context. /// - head: Request head. func didSendRequestHead(task: HTTPClient.Task, _ head: HTTPRequestHead) /// Called when a part of the request body is sent. Could be called zero or more times. /// /// - parameters: /// - task: Current request context. /// - part: Request body `Part`. func didSendRequestPart(task: HTTPClient.Task, _ part: IOData) /// Called when the request is fully sent. Will be called once. /// /// - parameters: /// - task: Current request context. func didSendRequest(task: HTTPClient.Task) /// Called when response head is received. Will be called once. /// You must return an `EventLoopFuture` that you complete when you have finished processing the body part. /// You can create an already succeeded future by calling `task.eventLoop.makeSucceededFuture(())`. /// /// - parameters: /// - task: Current request context. /// - head: Received reposonse head. /// - returns: `EventLoopFuture` that will be used for backpressure. func didReceiveHead(task: HTTPClient.Task, _ head: HTTPResponseHead) -> EventLoopFuture /// Called when part of a response body is received. Could be called zero or more times. /// You must return an `EventLoopFuture` that you complete when you have finished processing the body part. /// You can create an already succeeded future by calling `task.eventLoop.makeSucceededFuture(())`. /// /// This function will not be called until the future returned by `didReceiveHead` has completed. /// /// This function will not be called for subsequent body parts until the previous future returned by a /// call to this function completes. /// /// - parameters: /// - task: Current request context. /// - buffer: Received body `Part`. /// - returns: `EventLoopFuture` that will be used for backpressure. func didReceiveBodyPart(task: HTTPClient.Task, _ buffer: ByteBuffer) -> EventLoopFuture /// Called when error was thrown during request execution. Will be called zero or one time only. Request processing will be stopped after that. /// /// This function may be called at any time: it does not respect the backpressure exerted by `didReceiveHead` and `didReceiveBodyPart`. /// All outstanding work may be cancelled when this is received. Once called, no further calls will be made to `didReceiveHead`, `didReceiveBodyPart`, /// or `didFinishRequest`. /// /// - parameters: /// - task: Current request context. /// - error: Error that occured during response processing. func didReceiveError(task: HTTPClient.Task, _ error: Error) /// Called when the complete HTTP request is finished. You must return an instance of your `Response` associated type. Will be called once, except if an error occurred. /// /// This function will not be called until all futures returned by `didReceiveHead` and `didReceiveBodyPart` have completed. Once called, /// no further calls will be made to `didReceiveHead`, `didReceiveBodyPart`, or `didReceiveError`. /// /// - parameters: /// - task: Current request context. /// - returns: Result of processing. func didFinishRequest(task: HTTPClient.Task) throws -> Response } extension HTTPClientResponseDelegate { public func didSendRequestHead(task: HTTPClient.Task, _ head: HTTPRequestHead) {} public func didSendRequestPart(task: HTTPClient.Task, _ part: IOData) {} public func didSendRequest(task: HTTPClient.Task) {} public func didReceiveHead(task: HTTPClient.Task, _: HTTPResponseHead) -> EventLoopFuture { return task.eventLoop.makeSucceededFuture(()) } public func didReceiveBodyPart(task: HTTPClient.Task, _: ByteBuffer) -> EventLoopFuture { return task.eventLoop.makeSucceededFuture(()) } public func didReceiveError(task: HTTPClient.Task, _: Error) {} } extension URL { var percentEncodedPath: String { if self.path.isEmpty { return "/" } return URLComponents(url: self, resolvingAgainstBaseURL: false)?.percentEncodedPath ?? self.path } var uri: String { var uri = self.percentEncodedPath if let query = self.query { uri += "?" + query } return uri } func hasTheSameOrigin(as other: URL) -> Bool { return self.host == other.host && self.scheme == other.scheme && self.port == other.port } /// Initializes a newly created HTTP URL connecting to a unix domain socket path. The socket path is encoded as the URL's host, replacing percent encoding invalid path characters, and will use the "http+unix" scheme. /// - Parameters: /// - socketPath: The path to the unix domain socket to connect to. /// - uri: The URI path and query that will be sent to the server. public init?(httpURLWithSocketPath socketPath: String, uri: String = "/") { guard let host = socketPath.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { return nil } var urlString: String if uri.hasPrefix("/") { urlString = "http+unix://\(host)\(uri)" } else { urlString = "http+unix://\(host)/\(uri)" } self.init(string: urlString) } /// Initializes a newly created HTTPS URL connecting to a unix domain socket path over TLS. The socket path is encoded as the URL's host, replacing percent encoding invalid path characters, and will use the "https+unix" scheme. /// - Parameters: /// - socketPath: The path to the unix domain socket to connect to. /// - uri: The URI path and query that will be sent to the server. public init?(httpsURLWithSocketPath socketPath: String, uri: String = "/") { guard let host = socketPath.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { return nil } var urlString: String if uri.hasPrefix("/") { urlString = "https+unix://\(host)\(uri)" } else { urlString = "https+unix://\(host)/\(uri)" } self.init(string: urlString) } } protocol HTTPClientTaskDelegate { func cancel() } extension HTTPClient { /// Response execution context. Will be created by the library and could be used for obtaining /// `EventLoopFuture` of the execution or cancellation of the execution. public final class Task { /// The `EventLoop` the delegate will be executed on. public let eventLoop: EventLoop let promise: EventLoopPromise let logger: Logger // We are okay to store the logger here because a Task is for only one request. var isCancelled: Bool { self.lock.withLock { self._isCancelled } } var taskDelegate: HTTPClientTaskDelegate? { get { self.lock.withLock { self._taskDelegate } } set { self.lock.withLock { self._taskDelegate = newValue } } } private var _isCancelled: Bool = false private var _taskDelegate: HTTPClientTaskDelegate? private let lock = Lock() init(eventLoop: EventLoop, logger: Logger) { self.eventLoop = eventLoop self.promise = eventLoop.makePromise() self.logger = logger } static func failedTask(eventLoop: EventLoop, error: Error, logger: Logger) -> Task { let task = self.init(eventLoop: eventLoop, logger: logger) task.promise.fail(error) return task } /// `EventLoopFuture` for the response returned by this request. public var futureResult: EventLoopFuture { return self.promise.futureResult } /// Waits for execution of this request to complete. /// /// - returns: The value of the `EventLoopFuture` when it completes. /// - throws: The error value of the `EventLoopFuture` if it errors. public func wait() throws -> Response { return try self.promise.futureResult.wait() } /// Cancels the request execution. public func cancel() { let taskDelegate = self.lock.withLock { () -> HTTPClientTaskDelegate? in self._isCancelled = true return self._taskDelegate } taskDelegate?.cancel() } func succeed(promise: EventLoopPromise?, with value: Response, delegateType: Delegate.Type, closing: Bool) { promise?.succeed(value) } func fail(with error: Error, delegateType: Delegate.Type) { self.promise.fail(error) } } } internal struct TaskCancelEvent {} // MARK: - RedirectHandler internal struct RedirectHandler { let request: HTTPClient.Request let execute: (HTTPClient.Request) -> HTTPClient.Task func redirectTarget(status: HTTPResponseStatus, headers: HTTPHeaders) -> URL? { switch status { case .movedPermanently, .found, .seeOther, .notModified, .useProxy, .temporaryRedirect, .permanentRedirect: break default: return nil } guard let location = headers.first(name: "Location") else { return nil } guard let url = URL(string: location, relativeTo: request.url) else { return nil } guard self.request.kind.supportsRedirects(to: url.scheme) else { return nil } if url.isFileURL { return nil } return url.absoluteURL } func redirect(status: HTTPResponseStatus, to redirectURL: URL, promise: EventLoopPromise) { var nextState: HTTPClient.Request.RedirectState? if var state = request.redirectState { guard state.count > 0 else { return promise.fail(HTTPClientError.redirectLimitReached) } state.count -= 1 if var visited = state.visited { guard !visited.contains(redirectURL) else { return promise.fail(HTTPClientError.redirectCycleDetected) } visited.insert(redirectURL) state.visited = visited } nextState = state } let originalRequest = self.request var convertToGet = false if status == .seeOther, self.request.method != .HEAD { convertToGet = true } else if status == .movedPermanently || status == .found, self.request.method == .POST { convertToGet = true } var method = originalRequest.method var headers = originalRequest.headers var body = originalRequest.body if convertToGet { method = .GET body = nil headers.remove(name: "Content-Length") headers.remove(name: "Content-Type") } if !originalRequest.url.hasTheSameOrigin(as: redirectURL) { headers.remove(name: "Origin") headers.remove(name: "Cookie") headers.remove(name: "Authorization") headers.remove(name: "Proxy-Authorization") } do { var newRequest = try HTTPClient.Request(url: redirectURL, method: method, headers: headers, body: body) newRequest.redirectState = nextState self.execute(newRequest).futureResult.whenComplete { result in promise.futureResult.eventLoop.execute { promise.completeWith(result) } } } catch { promise.fail(error) } } } extension RequestBodyLength { init(_ body: HTTPClient.Body?) { guard let body = body else { self = .fixed(length: 0) return } guard let length = body.length else { self = .dynamic return } self = .fixed(length: length) } }