From 939aa2b94735fa219dd40e694bedca813e76573a Mon Sep 17 00:00:00 2001 From: Artem Redkin <aredkin@apple.com> Date: Mon, 23 Sep 2019 17:03:24 +0100 Subject: [PATCH 01/10] add support for redirect limits --- Sources/AsyncHTTPClient/HTTPClient.swift | 56 ++++++++++++++++--- Sources/AsyncHTTPClient/HTTPHandler.swift | 23 +++++++- .../HTTPClientTestUtils.swift | 10 ++++ .../HTTPClientTests.swift | 34 ++++++++++- 4 files changed, 110 insertions(+), 13 deletions(-) diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index 76f59c9ac..09846ce87 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -221,14 +221,16 @@ public class HTTPClient { } private func execute<Delegate: HTTPClientResponseDelegate>(request: Request, + redirectLimit: Configuration.RedirectLimit.Next? = nil, delegate: Delegate, eventLoop delegateEL: EventLoop, channelEL: EventLoop? = nil, deadline: NIODeadline? = nil) -> Task<Delegate.Response> { let redirectHandler: RedirectHandler<Delegate.Response>? - if self.configuration.followRedirects { - redirectHandler = RedirectHandler<Delegate.Response>(request: request) { newRequest in + if let next = redirectLimit ?? self.configuration.followRedirects.next { + redirectHandler = RedirectHandler<Delegate.Response>(limit: next, request: request) { newRequest, limit in self.execute(request: newRequest, + redirectLimit: limit, delegate: delegate, eventLoop: delegateEL, channelEL: channelEL, @@ -317,7 +319,7 @@ public class HTTPClient { /// - `305: Use Proxy` /// - `307: Temporary Redirect` /// - `308: Permanent Redirect` - public var followRedirects: Bool + public var followRedirects: FollowRedirects /// Default client timeout, defaults to no timeouts. public var timeout: Timeout /// Upstream proxy, defaults to no proxy. @@ -325,11 +327,11 @@ public class HTTPClient { /// Ignore TLS unclean shutdown error, defaults to `false`. public var ignoreUncleanSSLShutdown: Bool - public init(tlsConfiguration: TLSConfiguration? = nil, followRedirects: Bool = false, timeout: Timeout = Timeout(), proxy: Proxy? = nil) { + public init(tlsConfiguration: TLSConfiguration? = nil, followRedirects: FollowRedirects = .disabled, timeout: Timeout = Timeout(), proxy: Proxy? = nil) { self.init(tlsConfiguration: tlsConfiguration, followRedirects: followRedirects, timeout: timeout, proxy: proxy, ignoreUncleanSSLShutdown: false) } - public init(tlsConfiguration: TLSConfiguration? = nil, followRedirects: Bool = false, timeout: Timeout = Timeout(), proxy: Proxy? = nil, ignoreUncleanSSLShutdown: Bool = false) { + public init(tlsConfiguration: TLSConfiguration? = nil, followRedirects: FollowRedirects = .disabled, timeout: Timeout = Timeout(), proxy: Proxy? = nil, ignoreUncleanSSLShutdown: Bool = false) { self.tlsConfiguration = tlsConfiguration self.followRedirects = followRedirects self.timeout = timeout @@ -337,11 +339,11 @@ public class HTTPClient { self.ignoreUncleanSSLShutdown = ignoreUncleanSSLShutdown } - public init(certificateVerification: CertificateVerification, followRedirects: Bool = false, timeout: Timeout = Timeout(), proxy: Proxy? = nil) { + public init(certificateVerification: CertificateVerification, followRedirects: FollowRedirects = .disabled, timeout: Timeout = Timeout(), proxy: Proxy? = nil) { self.init(certificateVerification: certificateVerification, followRedirects: followRedirects, timeout: timeout, proxy: proxy, ignoreUncleanSSLShutdown: false) } - public init(certificateVerification: CertificateVerification, followRedirects: Bool = false, timeout: Timeout = Timeout(), proxy: Proxy? = nil, ignoreUncleanSSLShutdown: Bool = false) { + public init(certificateVerification: CertificateVerification, followRedirects: FollowRedirects = .disabled, timeout: Timeout = Timeout(), proxy: Proxy? = nil, ignoreUncleanSSLShutdown: Bool = false) { self.tlsConfiguration = TLSConfiguration.forClient(certificateVerification: certificateVerification) self.followRedirects = followRedirects self.timeout = timeout @@ -423,6 +425,41 @@ extension HTTPClient.Configuration { self.read = read } } + + /// Specifies redirect limit. + public struct RedirectLimit { + enum Next { + case none + case loop(visited: Set<URL>) + case count(left: Int) + } + + var next: Next + + /// No redirect limit. + public static let none = RedirectLimit(next: .none) + /// Request execution will be stopped if redirect loop is detected. + public static let detectLoop = RedirectLimit(next: .loop(visited: Set())) + /// Specifies maximum number of redirects for a single request. + public static func count(_ value: Int) -> RedirectLimit { return RedirectLimit(next: .count(left: value)) } + } + + /// Specifies redirect processing settings. + public enum FollowRedirects { + /// Redirects are not followed. + case disabled + /// Redirecets are followed with a specified limit. + case enabled(limit: RedirectLimit) + + var next: RedirectLimit.Next? { + switch self { + case .disabled: + return nil + case .enabled(let limit): + return limit.next + } + } + } } private extension ChannelPipeline { @@ -472,6 +509,7 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible { case invalidProxyResponse case contentLengthMissing case proxyAuthenticationRequired + case redirectLimitReached } private var code: Code @@ -508,6 +546,8 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible { public static let invalidProxyResponse = HTTPClientError(code: .invalidProxyResponse) /// Request does not contain `Content-Length` header. public static let contentLengthMissing = HTTPClientError(code: .contentLengthMissing) - /// Proxy Authentication Required + /// Proxy Authentication Required. public static let proxyAuthenticationRequired = HTTPClientError(code: .proxyAuthenticationRequired) + /// Redirect Limit reached. + public static let redirectLimitReached = HTTPClientError(code: .redirectLimitReached) } diff --git a/Sources/AsyncHTTPClient/HTTPHandler.swift b/Sources/AsyncHTTPClient/HTTPHandler.swift index 3913389a1..6f26c8e61 100644 --- a/Sources/AsyncHTTPClient/HTTPHandler.swift +++ b/Sources/AsyncHTTPClient/HTTPHandler.swift @@ -787,8 +787,9 @@ extension TaskHandler: ChannelDuplexHandler { // MARK: - RedirectHandler internal struct RedirectHandler<ResponseType> { + let limit: HTTPClient.Configuration.RedirectLimit.Next let request: HTTPClient.Request - let execute: (HTTPClient.Request) -> HTTPClient.Task<ResponseType> + let execute: (HTTPClient.Request, HTTPClient.Configuration.RedirectLimit.Next) -> HTTPClient.Task<ResponseType> func redirectTarget(status: HTTPResponseStatus, headers: HTTPHeaders) -> URL? { switch status { @@ -818,6 +819,24 @@ internal struct RedirectHandler<ResponseType> { } func redirect(status: HTTPResponseStatus, to redirectURL: URL, promise: EventLoopPromise<ResponseType>) { + let nextLimit: HTTPClient.Configuration.RedirectLimit.Next + switch self.limit { + case .none: + nextLimit = .none + case .count(let left): + if left == 0 { + return promise.fail(HTTPClientError.redirectLimitReached) + } + nextLimit = .count(left: left - 1) + case .loop(let visited): + if visited.contains(redirectURL) { + return promise.fail(HTTPClientError.redirectLimitReached) + } + var visited = visited + visited.insert(redirectURL) + nextLimit = .loop(visited: visited) + } + let originalRequest = self.request var convertToGet = false @@ -847,7 +866,7 @@ internal struct RedirectHandler<ResponseType> { do { let newRequest = try HTTPClient.Request(url: redirectURL, method: method, headers: headers, body: body) - return self.execute(newRequest).futureResult.cascade(to: promise) + return self.execute(newRequest, nextLimit).futureResult.cascade(to: promise) } catch { return promise.fail(error) } diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift b/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift index 9dd207079..54cdf721d 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift @@ -273,6 +273,16 @@ internal final class HttpBinHandler: ChannelInboundHandler { headers.add(name: "Location", value: "http://127.0.0.1:\(port)/echohostheader") self.resps.append(HTTPResponseBuilder(status: .found, headers: headers)) return + case "/redirect/infinite1": + var headers = HTTPHeaders() + headers.add(name: "Location", value: "/redirect/infinite2") + self.resps.append(HTTPResponseBuilder(status: .found, headers: headers)) + return + case "/redirect/infinite2": + var headers = HTTPHeaders() + headers.add(name: "Location", value: "/redirect/infinite1") + self.resps.append(HTTPResponseBuilder(status: .found, headers: headers)) + return // Since this String is taken from URL.path, the percent encoding has been removed case "/percent encoded": if req.method != .GET { diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift index fb8a35fe4..3e40dc5f7 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift @@ -130,7 +130,7 @@ class HTTPClientTests: XCTestCase { let httpBin = HTTPBin(ssl: false) let httpsBin = HTTPBin(ssl: true) let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, - configuration: HTTPClient.Configuration(certificateVerification: .none, followRedirects: true)) + configuration: HTTPClient.Configuration(certificateVerification: .none, followRedirects: .enabled(limit: .none))) defer { XCTAssertNoThrow(try httpClient.syncShutdown()) @@ -148,7 +148,7 @@ class HTTPClientTests: XCTestCase { func testHttpHostRedirect() throws { let httpBin = HTTPBin(ssl: false) let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, - configuration: HTTPClient.Configuration(certificateVerification: .none, followRedirects: true)) + configuration: HTTPClient.Configuration(certificateVerification: .none, followRedirects: .enabled(limit: .none))) defer { XCTAssertNoThrow(try httpClient.syncShutdown()) @@ -525,7 +525,7 @@ class HTTPClientTests: XCTestCase { let httpBin = HTTPBin() let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 5) let httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoopGroup), - configuration: HTTPClient.Configuration(followRedirects: true)) + configuration: HTTPClient.Configuration(followRedirects: .enabled(limit: .none))) defer { XCTAssertNoThrow(try httpClient.syncShutdown()) XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) @@ -563,4 +563,32 @@ class HTTPClientTests: XCTestCase { response = try httpClient.execute(request: request, delegate: delegate, eventLoop: .delegate(on: eventLoop)).wait() XCTAssertEqual(true, response) } + + func testLoopDetectionRedirectLimit() throws { + let httpBin = HTTPBin(ssl: true) + let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + configuration: HTTPClient.Configuration(certificateVerification: .none, followRedirects: .enabled(limit: .detectLoop))) + defer { + XCTAssertNoThrow(try httpClient.syncShutdown()) + XCTAssertNoThrow(try httpBin.shutdown()) + } + + XCTAssertThrowsError(try httpClient.get(url: "https://localhost:\(httpBin.port)/redirect/infinite1").wait(), "Should fail with redirect limit") { error in + XCTAssertEqual(error as! HTTPClientError, HTTPClientError.redirectLimitReached) + } + } + + func testCountRedirectLimit() throws { + let httpBin = HTTPBin(ssl: true) + let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, + configuration: HTTPClient.Configuration(certificateVerification: .none, followRedirects: .enabled(limit: .count(5)))) + defer { + XCTAssertNoThrow(try httpClient.syncShutdown()) + XCTAssertNoThrow(try httpBin.shutdown()) + } + + XCTAssertThrowsError(try httpClient.get(url: "https://localhost:\(httpBin.port)/redirect/infinite1").wait(), "Should fail with redirect limit") { error in + XCTAssertEqual(error as! HTTPClientError, HTTPClientError.redirectLimitReached) + } + } } From b4371119656840df1959e776fb90d8354622f1ee Mon Sep 17 00:00:00 2001 From: Artem Redkin <aredkin@apple.com> Date: Mon, 30 Sep 2019 14:38:18 +0100 Subject: [PATCH 02/10] add tests on linux --- Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift b/Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift index a06c148f1..d03b62ae3 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift @@ -57,6 +57,8 @@ extension HTTPClientTests { ("testWrongContentLengthForSSLUncleanShutdown", testWrongContentLengthForSSLUncleanShutdown), ("testWrongContentLengthWithIgnoreErrorForSSLUncleanShutdown", testWrongContentLengthWithIgnoreErrorForSSLUncleanShutdown), ("testEventLoopArgument", testEventLoopArgument), + ("testLoopDetectionRedirectLimit", testLoopDetectionRedirectLimit), + ("testCountRedirectLimit", testCountRedirectLimit), ] } } From 99096c5e8f948fd0b598aa5511cc7423594403ed Mon Sep 17 00:00:00 2001 From: Artem Redkin <aredkin@apple.com> Date: Mon, 30 Sep 2019 16:48:15 +0100 Subject: [PATCH 03/10] review fix: guard instead of if --- Sources/AsyncHTTPClient/HTTPHandler.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AsyncHTTPClient/HTTPHandler.swift b/Sources/AsyncHTTPClient/HTTPHandler.swift index 6f26c8e61..b1f093e53 100644 --- a/Sources/AsyncHTTPClient/HTTPHandler.swift +++ b/Sources/AsyncHTTPClient/HTTPHandler.swift @@ -824,7 +824,7 @@ internal struct RedirectHandler<ResponseType> { case .none: nextLimit = .none case .count(let left): - if left == 0 { + guard left > 0 else { return promise.fail(HTTPClientError.redirectLimitReached) } nextLimit = .count(left: left - 1) From b3ffde6865d592a8ba12f2fe646dd445733ba283 Mon Sep 17 00:00:00 2001 From: Artem Redkin <aredkin@apple.com> Date: Tue, 1 Oct 2019 12:30:19 +0100 Subject: [PATCH 04/10] review fixes --- Sources/AsyncHTTPClient/HTTPClient.swift | 68 +++++++------------ Sources/AsyncHTTPClient/HTTPHandler.swift | 30 ++++---- .../HTTPClientTests.swift | 10 +-- 3 files changed, 43 insertions(+), 65 deletions(-) diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index 09846ce87..9ff7db684 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -221,22 +221,24 @@ public class HTTPClient { } private func execute<Delegate: HTTPClientResponseDelegate>(request: Request, - redirectLimit: Configuration.RedirectLimit.Next? = nil, + redirectState: Configuration.RedirectPolicy.State? = nil, delegate: Delegate, eventLoop delegateEL: EventLoop, channelEL: EventLoop? = nil, deadline: NIODeadline? = nil) -> Task<Delegate.Response> { let redirectHandler: RedirectHandler<Delegate.Response>? - if let next = redirectLimit ?? self.configuration.followRedirects.next { - redirectHandler = RedirectHandler<Delegate.Response>(limit: next, request: request) { newRequest, limit in + switch self.configuration.redirects { + case .allow(let max, let allowCycles): + let state = redirectState ?? .init(count: max, visited: allowCycles ? nil : Set()) + redirectHandler = RedirectHandler<Delegate.Response>(state: state, request: request) { newRequest, state in self.execute(request: newRequest, - redirectLimit: limit, + redirectState: state, delegate: delegate, eventLoop: delegateEL, channelEL: channelEL, deadline: deadline) } - } else { + case .disallow: redirectHandler = nil } @@ -319,7 +321,7 @@ public class HTTPClient { /// - `305: Use Proxy` /// - `307: Temporary Redirect` /// - `308: Permanent Redirect` - public var followRedirects: FollowRedirects + public var redirects: RedirectPolicy /// Default client timeout, defaults to no timeouts. public var timeout: Timeout /// Upstream proxy, defaults to no proxy. @@ -327,25 +329,25 @@ public class HTTPClient { /// Ignore TLS unclean shutdown error, defaults to `false`. public var ignoreUncleanSSLShutdown: Bool - public init(tlsConfiguration: TLSConfiguration? = nil, followRedirects: FollowRedirects = .disabled, timeout: Timeout = Timeout(), proxy: Proxy? = nil) { - self.init(tlsConfiguration: tlsConfiguration, followRedirects: followRedirects, timeout: timeout, proxy: proxy, ignoreUncleanSSLShutdown: false) + public init(tlsConfiguration: TLSConfiguration? = nil, redirects: RedirectPolicy = .allow(max: 5, allowCycles: false), timeout: Timeout = Timeout(), proxy: Proxy? = nil) { + self.init(tlsConfiguration: tlsConfiguration, redirects: redirects, timeout: timeout, proxy: proxy, ignoreUncleanSSLShutdown: false) } - public init(tlsConfiguration: TLSConfiguration? = nil, followRedirects: FollowRedirects = .disabled, timeout: Timeout = Timeout(), proxy: Proxy? = nil, ignoreUncleanSSLShutdown: Bool = false) { + public init(tlsConfiguration: TLSConfiguration? = nil, redirects: RedirectPolicy = .allow(max: 5, allowCycles: false), timeout: Timeout = Timeout(), proxy: Proxy? = nil, ignoreUncleanSSLShutdown: Bool = false) { self.tlsConfiguration = tlsConfiguration - self.followRedirects = followRedirects + self.redirects = redirects self.timeout = timeout self.proxy = proxy self.ignoreUncleanSSLShutdown = ignoreUncleanSSLShutdown } - public init(certificateVerification: CertificateVerification, followRedirects: FollowRedirects = .disabled, timeout: Timeout = Timeout(), proxy: Proxy? = nil) { - self.init(certificateVerification: certificateVerification, followRedirects: followRedirects, timeout: timeout, proxy: proxy, ignoreUncleanSSLShutdown: false) + public init(certificateVerification: CertificateVerification, redirects: RedirectPolicy = .allow(max: 5, allowCycles: false), timeout: Timeout = Timeout(), proxy: Proxy? = nil) { + self.init(certificateVerification: certificateVerification, redirects: redirects, timeout: timeout, proxy: proxy, ignoreUncleanSSLShutdown: false) } - public init(certificateVerification: CertificateVerification, followRedirects: FollowRedirects = .disabled, timeout: Timeout = Timeout(), proxy: Proxy? = nil, ignoreUncleanSSLShutdown: Bool = false) { + public init(certificateVerification: CertificateVerification, redirects: RedirectPolicy = .allow(max: 5, allowCycles: false), timeout: Timeout = Timeout(), proxy: Proxy? = nil, ignoreUncleanSSLShutdown: Bool = false) { self.tlsConfiguration = TLSConfiguration.forClient(certificateVerification: certificateVerification) - self.followRedirects = followRedirects + self.redirects = redirects self.timeout = timeout self.proxy = proxy self.ignoreUncleanSSLShutdown = ignoreUncleanSSLShutdown @@ -426,38 +428,16 @@ extension HTTPClient.Configuration { } } - /// Specifies redirect limit. - public struct RedirectLimit { - enum Next { - case none - case loop(visited: Set<URL>) - case count(left: Int) - } - - var next: Next - - /// No redirect limit. - public static let none = RedirectLimit(next: .none) - /// Request execution will be stopped if redirect loop is detected. - public static let detectLoop = RedirectLimit(next: .loop(visited: Set())) - /// Specifies maximum number of redirects for a single request. - public static func count(_ value: Int) -> RedirectLimit { return RedirectLimit(next: .count(left: value)) } - } - /// Specifies redirect processing settings. - public enum FollowRedirects { + public enum RedirectPolicy { /// Redirects are not followed. - case disabled - /// Redirecets are followed with a specified limit. - case enabled(limit: RedirectLimit) - - var next: RedirectLimit.Next? { - switch self { - case .disabled: - return nil - case .enabled(let limit): - return limit.next - } + case disallow + /// Redirects are followed with a specified limit. Cycle detection reqiures that all visited URL's are kept in memory. + case allow(max: Int, allowCycles: Bool) + + struct State { + var count: Int + var visited: Set<URL>? } } } diff --git a/Sources/AsyncHTTPClient/HTTPHandler.swift b/Sources/AsyncHTTPClient/HTTPHandler.swift index b1f093e53..efb8230a6 100644 --- a/Sources/AsyncHTTPClient/HTTPHandler.swift +++ b/Sources/AsyncHTTPClient/HTTPHandler.swift @@ -787,9 +787,9 @@ extension TaskHandler: ChannelDuplexHandler { // MARK: - RedirectHandler internal struct RedirectHandler<ResponseType> { - let limit: HTTPClient.Configuration.RedirectLimit.Next + let state: HTTPClient.Configuration.RedirectPolicy.State let request: HTTPClient.Request - let execute: (HTTPClient.Request, HTTPClient.Configuration.RedirectLimit.Next) -> HTTPClient.Task<ResponseType> + let execute: (HTTPClient.Request, HTTPClient.Configuration.RedirectPolicy.State) -> HTTPClient.Task<ResponseType> func redirectTarget(status: HTTPResponseStatus, headers: HTTPHeaders) -> URL? { switch status { @@ -819,22 +819,20 @@ internal struct RedirectHandler<ResponseType> { } func redirect(status: HTTPResponseStatus, to redirectURL: URL, promise: EventLoopPromise<ResponseType>) { - let nextLimit: HTTPClient.Configuration.RedirectLimit.Next - switch self.limit { - case .none: - nextLimit = .none - case .count(let left): - guard left > 0 else { - return promise.fail(HTTPClientError.redirectLimitReached) - } - nextLimit = .count(left: left - 1) - case .loop(let visited): - if visited.contains(redirectURL) { + var newState = self.state + guard newState.count > 0 else { + return promise.fail(HTTPClientError.redirectLimitReached) + } + + newState.count -= 1 + + if var visited = newState.visited { + guard !visited.contains(redirectURL) else { return promise.fail(HTTPClientError.redirectLimitReached) } - var visited = visited + visited.insert(redirectURL) - nextLimit = .loop(visited: visited) + newState.visited = visited } let originalRequest = self.request @@ -866,7 +864,7 @@ internal struct RedirectHandler<ResponseType> { do { let newRequest = try HTTPClient.Request(url: redirectURL, method: method, headers: headers, body: body) - return self.execute(newRequest, nextLimit).futureResult.cascade(to: promise) + return self.execute(newRequest, newState).futureResult.cascade(to: promise) } catch { return promise.fail(error) } diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift index 3e40dc5f7..615269e14 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift @@ -130,7 +130,7 @@ class HTTPClientTests: XCTestCase { let httpBin = HTTPBin(ssl: false) let httpsBin = HTTPBin(ssl: true) let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, - configuration: HTTPClient.Configuration(certificateVerification: .none, followRedirects: .enabled(limit: .none))) + configuration: HTTPClient.Configuration(certificateVerification: .none, redirects: .allow(max: 10, allowCycles: true))) defer { XCTAssertNoThrow(try httpClient.syncShutdown()) @@ -148,7 +148,7 @@ class HTTPClientTests: XCTestCase { func testHttpHostRedirect() throws { let httpBin = HTTPBin(ssl: false) let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, - configuration: HTTPClient.Configuration(certificateVerification: .none, followRedirects: .enabled(limit: .none))) + configuration: HTTPClient.Configuration(certificateVerification: .none, redirects: .allow(max: 10, allowCycles: true))) defer { XCTAssertNoThrow(try httpClient.syncShutdown()) @@ -525,7 +525,7 @@ class HTTPClientTests: XCTestCase { let httpBin = HTTPBin() let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 5) let httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoopGroup), - configuration: HTTPClient.Configuration(followRedirects: .enabled(limit: .none))) + configuration: HTTPClient.Configuration(redirects: .allow(max: 10, allowCycles: true))) defer { XCTAssertNoThrow(try httpClient.syncShutdown()) XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) @@ -567,7 +567,7 @@ class HTTPClientTests: XCTestCase { func testLoopDetectionRedirectLimit() throws { let httpBin = HTTPBin(ssl: true) let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, - configuration: HTTPClient.Configuration(certificateVerification: .none, followRedirects: .enabled(limit: .detectLoop))) + configuration: HTTPClient.Configuration(certificateVerification: .none, redirects: .allow(max: 5, allowCycles: false))) defer { XCTAssertNoThrow(try httpClient.syncShutdown()) XCTAssertNoThrow(try httpBin.shutdown()) @@ -581,7 +581,7 @@ class HTTPClientTests: XCTestCase { func testCountRedirectLimit() throws { let httpBin = HTTPBin(ssl: true) let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, - configuration: HTTPClient.Configuration(certificateVerification: .none, followRedirects: .enabled(limit: .count(5)))) + configuration: HTTPClient.Configuration(certificateVerification: .none, redirects: .allow(max: 5, allowCycles: true))) defer { XCTAssertNoThrow(try httpClient.syncShutdown()) XCTAssertNoThrow(try httpBin.shutdown()) From bbad11ffcbfc42aa955118c119f0c1ed4e71e836 Mon Sep 17 00:00:00 2001 From: Artem Redkin <aredkin@apple.com> Date: Tue, 8 Oct 2019 13:18:02 +0100 Subject: [PATCH 05/10] review fix: make redirect state be part for request --- Sources/AsyncHTTPClient/HTTPClient.swift | 14 +++----- Sources/AsyncHTTPClient/HTTPHandler.swift | 41 +++++++++++++++-------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index 9ff7db684..272b35065 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -221,7 +221,6 @@ public class HTTPClient { } private func execute<Delegate: HTTPClientResponseDelegate>(request: Request, - redirectState: Configuration.RedirectPolicy.State? = nil, delegate: Delegate, eventLoop delegateEL: EventLoop, channelEL: EventLoop? = nil, @@ -229,10 +228,12 @@ public class HTTPClient { let redirectHandler: RedirectHandler<Delegate.Response>? switch self.configuration.redirects { case .allow(let max, let allowCycles): - let state = redirectState ?? .init(count: max, visited: allowCycles ? nil : Set()) - redirectHandler = RedirectHandler<Delegate.Response>(state: state, request: request) { newRequest, state in + var request = request + if request.redirectState == nil { + request.redirectState = .init(count: max, visited: allowCycles ? nil : Set()) + } + redirectHandler = RedirectHandler<Delegate.Response>(request: request) { newRequest in self.execute(request: newRequest, - redirectState: state, delegate: delegate, eventLoop: delegateEL, channelEL: channelEL, @@ -434,11 +435,6 @@ extension HTTPClient.Configuration { case disallow /// Redirects are followed with a specified limit. Cycle detection reqiures that all visited URL's are kept in memory. case allow(max: Int, allowCycles: Bool) - - struct State { - var count: Int - var visited: Set<URL>? - } } } diff --git a/Sources/AsyncHTTPClient/HTTPHandler.swift b/Sources/AsyncHTTPClient/HTTPHandler.swift index efb8230a6..0e6529d8b 100644 --- a/Sources/AsyncHTTPClient/HTTPHandler.swift +++ b/Sources/AsyncHTTPClient/HTTPHandler.swift @@ -100,6 +100,13 @@ extension HTTPClient { /// Request body, defaults to no body. public var body: Body? + struct RedirectState { + var count: Int + var visited: Set<URL>? + } + + var redirectState: RedirectState? + /// Create HTTP request. /// /// - parameters: @@ -152,6 +159,8 @@ extension HTTPClient { self.host = host self.headers = headers self.body = body + + self.redirectState = nil } /// Whether request will be executed using secure socket. @@ -787,9 +796,8 @@ extension TaskHandler: ChannelDuplexHandler { // MARK: - RedirectHandler internal struct RedirectHandler<ResponseType> { - let state: HTTPClient.Configuration.RedirectPolicy.State let request: HTTPClient.Request - let execute: (HTTPClient.Request, HTTPClient.Configuration.RedirectPolicy.State) -> HTTPClient.Task<ResponseType> + let execute: (HTTPClient.Request) -> HTTPClient.Task<ResponseType> func redirectTarget(status: HTTPResponseStatus, headers: HTTPHeaders) -> URL? { switch status { @@ -819,20 +827,24 @@ internal struct RedirectHandler<ResponseType> { } func redirect(status: HTTPResponseStatus, to redirectURL: URL, promise: EventLoopPromise<ResponseType>) { - var newState = self.state - guard newState.count > 0 else { - return promise.fail(HTTPClientError.redirectLimitReached) - } + var nextState: HTTPClient.Request.RedirectState? + if var state = request.redirectState { + guard state.count > 0 else { + return promise.fail(HTTPClientError.redirectLimitReached) + } - newState.count -= 1 + state.count -= 1 - if var visited = newState.visited { - guard !visited.contains(redirectURL) else { - return promise.fail(HTTPClientError.redirectLimitReached) + if var visited = state.visited { + guard !visited.contains(redirectURL) else { + return promise.fail(HTTPClientError.redirectLimitReached) + } + + visited.insert(redirectURL) + state.visited = visited } - visited.insert(redirectURL) - newState.visited = visited + nextState = state } let originalRequest = self.request @@ -863,8 +875,9 @@ internal struct RedirectHandler<ResponseType> { } do { - let newRequest = try HTTPClient.Request(url: redirectURL, method: method, headers: headers, body: body) - return self.execute(newRequest, newState).futureResult.cascade(to: promise) + var newRequest = try HTTPClient.Request(url: redirectURL, method: method, headers: headers, body: body) + newRequest.redirectState = nextState + return self.execute(newRequest).futureResult.cascade(to: promise) } catch { return promise.fail(error) } From 8b0e613b00c40016a4550f7fcb738e88a6380fa0 Mon Sep 17 00:00:00 2001 From: Artem Redkin <aredkin@apple.com> Date: Tue, 8 Oct 2019 13:22:23 +0100 Subject: [PATCH 06/10] review fix: add a separate error for redirect cycles --- Sources/AsyncHTTPClient/HTTPClient.swift | 3 +++ Sources/AsyncHTTPClient/HTTPHandler.swift | 2 +- Tests/AsyncHTTPClientTests/HTTPClientTests.swift | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index 272b35065..5bbefa6dc 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -486,6 +486,7 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible { case contentLengthMissing case proxyAuthenticationRequired case redirectLimitReached + case redirectCycleDetected } private var code: Code @@ -526,4 +527,6 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible { public static let proxyAuthenticationRequired = HTTPClientError(code: .proxyAuthenticationRequired) /// Redirect Limit reached. public static let redirectLimitReached = HTTPClientError(code: .redirectLimitReached) + /// Redirect Cycle detected. + public static let redirectCycleDetected = HTTPClientError(code: .redirectCycleDetected) } diff --git a/Sources/AsyncHTTPClient/HTTPHandler.swift b/Sources/AsyncHTTPClient/HTTPHandler.swift index 0e6529d8b..47a85def2 100644 --- a/Sources/AsyncHTTPClient/HTTPHandler.swift +++ b/Sources/AsyncHTTPClient/HTTPHandler.swift @@ -837,7 +837,7 @@ internal struct RedirectHandler<ResponseType> { if var visited = state.visited { guard !visited.contains(redirectURL) else { - return promise.fail(HTTPClientError.redirectLimitReached) + return promise.fail(HTTPClientError.redirectCycleDetected) } visited.insert(redirectURL) diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift index 615269e14..41e797ad7 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift @@ -574,7 +574,7 @@ class HTTPClientTests: XCTestCase { } XCTAssertThrowsError(try httpClient.get(url: "https://localhost:\(httpBin.port)/redirect/infinite1").wait(), "Should fail with redirect limit") { error in - XCTAssertEqual(error as! HTTPClientError, HTTPClientError.redirectLimitReached) + XCTAssertEqual(error as! HTTPClientError, HTTPClientError.redirectCycleDetected) } } From aa843dea4465fd4ba660fc6b361d7677b35c17ec Mon Sep 17 00:00:00 2001 From: Artem Redkin <aredkin@apple.com> Date: Fri, 11 Oct 2019 13:19:26 +0100 Subject: [PATCH 07/10] Update Sources/AsyncHTTPClient/HTTPClient.swift Co-Authored-By: Johannes Weiss <johannesweiss@apple.com> --- Sources/AsyncHTTPClient/HTTPClient.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index 5bbefa6dc..bb5a1340a 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -433,7 +433,13 @@ extension HTTPClient.Configuration { public enum RedirectPolicy { /// Redirects are not followed. case disallow - /// Redirects are followed with a specified limit. Cycle detection reqiures that all visited URL's are kept in memory. + /// Redirects are followed with a specified limit. + /// + /// - parameters: + /// - max: The maximum number of allowed redirects. + /// - allowCycles: Whether cycles are allowed. + /// + /// - warning: Cycle detection will keep all visited URLs in memory which means a malicious server could use this as a denial-of-service vector. case allow(max: Int, allowCycles: Bool) } } From 7e2edf88aa77f401c277533fbbdf023062f15f9f Mon Sep 17 00:00:00 2001 From: Artem Redkin <aredkin@apple.com> Date: Fri, 11 Oct 2019 14:54:08 +0100 Subject: [PATCH 08/10] review fixes --- Sources/AsyncHTTPClient/HTTPClient.swift | 46 +++++++++++++------ .../HTTPClientTests.swift | 10 ++-- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/Sources/AsyncHTTPClient/HTTPClient.swift b/Sources/AsyncHTTPClient/HTTPClient.swift index bb5a1340a..245738936 100644 --- a/Sources/AsyncHTTPClient/HTTPClient.swift +++ b/Sources/AsyncHTTPClient/HTTPClient.swift @@ -226,8 +226,8 @@ public class HTTPClient { channelEL: EventLoop? = nil, deadline: NIODeadline? = nil) -> Task<Delegate.Response> { let redirectHandler: RedirectHandler<Delegate.Response>? - switch self.configuration.redirects { - case .allow(let max, let allowCycles): + switch self.configuration.redirectConfiguration.configuration { + case .follow(let max, let allowCycles): var request = request if request.redirectState == nil { request.redirectState = .init(count: max, visited: allowCycles ? nil : Set()) @@ -322,7 +322,7 @@ public class HTTPClient { /// - `305: Use Proxy` /// - `307: Temporary Redirect` /// - `308: Permanent Redirect` - public var redirects: RedirectPolicy + public var redirectConfiguration: RedirectConfiguration /// Default client timeout, defaults to no timeouts. public var timeout: Timeout /// Upstream proxy, defaults to no proxy. @@ -330,25 +330,25 @@ public class HTTPClient { /// Ignore TLS unclean shutdown error, defaults to `false`. public var ignoreUncleanSSLShutdown: Bool - public init(tlsConfiguration: TLSConfiguration? = nil, redirects: RedirectPolicy = .allow(max: 5, allowCycles: false), timeout: Timeout = Timeout(), proxy: Proxy? = nil) { - self.init(tlsConfiguration: tlsConfiguration, redirects: redirects, timeout: timeout, proxy: proxy, ignoreUncleanSSLShutdown: false) + public init(tlsConfiguration: TLSConfiguration? = nil, redirectConfiguration: RedirectConfiguration? = nil, timeout: Timeout = Timeout(), proxy: Proxy? = nil) { + self.init(tlsConfiguration: tlsConfiguration, redirectConfiguration: redirectConfiguration, timeout: timeout, proxy: proxy, ignoreUncleanSSLShutdown: false) } - public init(tlsConfiguration: TLSConfiguration? = nil, redirects: RedirectPolicy = .allow(max: 5, allowCycles: false), timeout: Timeout = Timeout(), proxy: Proxy? = nil, ignoreUncleanSSLShutdown: Bool = false) { + public init(tlsConfiguration: TLSConfiguration? = nil, redirectConfiguration: RedirectConfiguration? = nil, timeout: Timeout = Timeout(), proxy: Proxy? = nil, ignoreUncleanSSLShutdown: Bool = false) { self.tlsConfiguration = tlsConfiguration - self.redirects = redirects + self.redirectConfiguration = redirectConfiguration ?? RedirectConfiguration() self.timeout = timeout self.proxy = proxy self.ignoreUncleanSSLShutdown = ignoreUncleanSSLShutdown } - public init(certificateVerification: CertificateVerification, redirects: RedirectPolicy = .allow(max: 5, allowCycles: false), timeout: Timeout = Timeout(), proxy: Proxy? = nil) { - self.init(certificateVerification: certificateVerification, redirects: redirects, timeout: timeout, proxy: proxy, ignoreUncleanSSLShutdown: false) + public init(certificateVerification: CertificateVerification, redirectConfiguration: RedirectConfiguration? = nil, timeout: Timeout = Timeout(), proxy: Proxy? = nil) { + self.init(certificateVerification: certificateVerification, redirectConfiguration: redirectConfiguration, timeout: timeout, proxy: proxy, ignoreUncleanSSLShutdown: false) } - public init(certificateVerification: CertificateVerification, redirects: RedirectPolicy = .allow(max: 5, allowCycles: false), timeout: Timeout = Timeout(), proxy: Proxy? = nil, ignoreUncleanSSLShutdown: Bool = false) { + public init(certificateVerification: CertificateVerification, redirectConfiguration: RedirectConfiguration? = nil, timeout: Timeout = Timeout(), proxy: Proxy? = nil, ignoreUncleanSSLShutdown: Bool = false) { self.tlsConfiguration = TLSConfiguration.forClient(certificateVerification: certificateVerification) - self.redirects = redirects + self.redirectConfiguration = redirectConfiguration ?? RedirectConfiguration() self.timeout = timeout self.proxy = proxy self.ignoreUncleanSSLShutdown = ignoreUncleanSSLShutdown @@ -430,9 +430,27 @@ extension HTTPClient.Configuration { } /// Specifies redirect processing settings. - public enum RedirectPolicy { + public struct RedirectConfiguration { + enum Configuration { + /// Redirects are not followed. + case disallow + /// Redirects are followed with a specified limit. + case follow(max: Int, allowCycles: Bool) + } + + var configuration: Configuration + + init() { + self.configuration = .follow(max: 5, allowCycles: false) + } + + init(configuration: Configuration) { + self.configuration = configuration + } + /// Redirects are not followed. - case disallow + public static let disallow = RedirectConfiguration(configuration: .disallow) + /// Redirects are followed with a specified limit. /// /// - parameters: @@ -440,7 +458,7 @@ extension HTTPClient.Configuration { /// - allowCycles: Whether cycles are allowed. /// /// - warning: Cycle detection will keep all visited URLs in memory which means a malicious server could use this as a denial-of-service vector. - case allow(max: Int, allowCycles: Bool) + public static func follow(max: Int, allowCycles: Bool) -> RedirectConfiguration { return .init(configuration: .follow(max: max, allowCycles: allowCycles)) } } } diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift index 41e797ad7..9e1931a0c 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift @@ -130,7 +130,7 @@ class HTTPClientTests: XCTestCase { let httpBin = HTTPBin(ssl: false) let httpsBin = HTTPBin(ssl: true) let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, - configuration: HTTPClient.Configuration(certificateVerification: .none, redirects: .allow(max: 10, allowCycles: true))) + configuration: HTTPClient.Configuration(certificateVerification: .none, redirectConfiguration: .follow(max: 10, allowCycles: true))) defer { XCTAssertNoThrow(try httpClient.syncShutdown()) @@ -148,7 +148,7 @@ class HTTPClientTests: XCTestCase { func testHttpHostRedirect() throws { let httpBin = HTTPBin(ssl: false) let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, - configuration: HTTPClient.Configuration(certificateVerification: .none, redirects: .allow(max: 10, allowCycles: true))) + configuration: HTTPClient.Configuration(certificateVerification: .none, redirectConfiguration: .follow(max: 10, allowCycles: true))) defer { XCTAssertNoThrow(try httpClient.syncShutdown()) @@ -525,7 +525,7 @@ class HTTPClientTests: XCTestCase { let httpBin = HTTPBin() let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 5) let httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoopGroup), - configuration: HTTPClient.Configuration(redirects: .allow(max: 10, allowCycles: true))) + configuration: HTTPClient.Configuration(redirectConfiguration: .follow(max: 10, allowCycles: true))) defer { XCTAssertNoThrow(try httpClient.syncShutdown()) XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) @@ -567,7 +567,7 @@ class HTTPClientTests: XCTestCase { func testLoopDetectionRedirectLimit() throws { let httpBin = HTTPBin(ssl: true) let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, - configuration: HTTPClient.Configuration(certificateVerification: .none, redirects: .allow(max: 5, allowCycles: false))) + configuration: HTTPClient.Configuration(certificateVerification: .none, redirectConfiguration: .follow(max: 5, allowCycles: false))) defer { XCTAssertNoThrow(try httpClient.syncShutdown()) XCTAssertNoThrow(try httpBin.shutdown()) @@ -581,7 +581,7 @@ class HTTPClientTests: XCTestCase { func testCountRedirectLimit() throws { let httpBin = HTTPBin(ssl: true) let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, - configuration: HTTPClient.Configuration(certificateVerification: .none, redirects: .allow(max: 5, allowCycles: true))) + configuration: HTTPClient.Configuration(certificateVerification: .none, redirectConfiguration: .follow(max: 5, allowCycles: true))) defer { XCTAssertNoThrow(try httpClient.syncShutdown()) XCTAssertNoThrow(try httpBin.shutdown()) From 5209dc38e7dff0d8a9633cdf12e73c97ae48975f Mon Sep 17 00:00:00 2001 From: Artem Redkin <aredkin@apple.com> Date: Mon, 14 Oct 2019 11:08:10 +0100 Subject: [PATCH 09/10] rewrite backpressure test --- Sources/AsyncHTTPClient/HTTPHandler.swift | 2 +- .../HTTPClientInternalTests.swift | 51 ++++++++++++++----- .../HTTPClientTestUtils.swift | 2 +- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/Sources/AsyncHTTPClient/HTTPHandler.swift b/Sources/AsyncHTTPClient/HTTPHandler.swift index 47a85def2..79c9cafe4 100644 --- a/Sources/AsyncHTTPClient/HTTPHandler.swift +++ b/Sources/AsyncHTTPClient/HTTPHandler.swift @@ -456,7 +456,7 @@ extension HTTPClient { public let eventLoop: EventLoop let promise: EventLoopPromise<Response> - private var channel: Channel? + var channel: Channel? private var cancelled: Bool private let lock: Lock diff --git a/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift index f75bbb087..614d6afa1 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift @@ -155,17 +155,29 @@ class HTTPClientInternalTests: XCTestCase { XCTAssertThrowsError(try httpClient.post(url: "http://localhost:\(httpBin.port)/post", body: body).wait()) } + // In order to test backpressure we need to make sure that reads will not happen + // until the backpressure promise is succeeded. Since we cannot guarantee when + // messages will be delivered to a client pipeline and we need this test to be + // fast (no waiting for arbitrary amounts of time), we do the following. + // First, we enforce NIO to send us only 1 byte at a time. Then we send a message + // of 4 bytes. This will guarantee that if we see first byte of the message, other + // bytes a ready to be read as well. This will allow us to test if subsequent reads + // are waiting for backpressure promise. func testUploadStreamingBackpressure() throws { class BackpressureTestDelegate: HTTPClientResponseDelegate { typealias Response = Void var _reads = 0 let lock: Lock - let promise: EventLoopPromise<Void> + let backpressurePromise: EventLoopPromise<Void> + let optionsApplied: EventLoopPromise<Void> + let messageReceived: EventLoopPromise<Void> - init(promise: EventLoopPromise<Void>) { + init(eventLoop: EventLoop) { self.lock = Lock() - self.promise = promise + self.backpressurePromise = eventLoop.makePromise() + self.optionsApplied = eventLoop.makePromise() + self.messageReceived = eventLoop.makePromise() } var reads: Int { @@ -174,18 +186,30 @@ class HTTPClientInternalTests: XCTestCase { } } + func didReceiveHead(task: HTTPClient.Task<Void>, _ head: HTTPResponseHead) -> EventLoopFuture<Void> { + // This is to force NIO to send only 1 byte at a time. + let future = task.channel!.setOption(ChannelOptions.maxMessagesPerRead, value: 1).flatMap { + task.channel!.setOption(ChannelOptions.recvAllocator, value: FixedSizeRecvByteBufferAllocator(capacity: 1)) + } + future.cascade(to: self.optionsApplied) + return future + } + func didReceiveBodyPart(task: HTTPClient.Task<Response>, _ buffer: ByteBuffer) -> EventLoopFuture<Void> { + // We count a number of reads received. self.lock.withLockVoid { self._reads += 1 } - return self.promise.futureResult + // We need to notify the test when first byte of the message is arrived. + self.messageReceived.succeed(()) + return self.backpressurePromise.futureResult } func didFinishRequest(task: HTTPClient.Task<Response>) throws {} } let httpClient = HTTPClient(eventLoopGroupProvider: .createNew) - let promise: EventLoopPromise<Channel> = httpClient.eventLoopGroup.next().makePromise() + let promise = httpClient.eventLoopGroup.next().makePromise(of: Channel.self) let httpBin = HTTPBin(channelPromise: promise) defer { @@ -194,27 +218,30 @@ class HTTPClientInternalTests: XCTestCase { } let request = try Request(url: "http://localhost:\(httpBin.port)/custom") - let delegate = BackpressureTestDelegate(promise: httpClient.eventLoopGroup.next().makePromise()) + let delegate = BackpressureTestDelegate(eventLoop: httpClient.eventLoopGroup.next()) let future = httpClient.execute(request: request, delegate: delegate).futureResult let channel = try promise.futureResult.wait() + // We need to wait for channel options that limit NIO to sending only one byte at a time. + try delegate.optionsApplied.futureResult.wait() - // Send 3 parts, but only one should be received until the future is complete + // Send 4 bytes, but only one should be received until the backpressure promise is succeeded. let buffer = ByteBuffer.of(string: "1234") try channel.writeAndFlush(HTTPServerResponsePart.body(.byteBuffer(buffer))).wait() - try channel.writeAndFlush(HTTPServerResponsePart.body(.byteBuffer(buffer))).wait() - try channel.writeAndFlush(HTTPServerResponsePart.body(.byteBuffer(buffer))).wait() + // Now we wait until message is delivered to client channel pipeline + try delegate.messageReceived.futureResult.wait() XCTAssertEqual(delegate.reads, 1) - delegate.promise.succeed(()) + // Succeed the backpressure promise. + delegate.backpressurePromise.succeed(()) try channel.writeAndFlush(HTTPServerResponsePart.end(nil)).wait() try future.wait() - XCTAssertEqual(delegate.reads, 3) + // At this point all other bytes should be delivered. + XCTAssertEqual(delegate.reads, 4) } - func testRequestURITrailingSlash() throws { let request1 = try Request(url: "https://someserver.com:8888/some/path?foo=bar#ref") XCTAssertEqual(request1.url.uri, "/some/path?foo=bar") diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift b/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift index 54cdf721d..1a51f4fe1 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift @@ -305,7 +305,7 @@ internal final class HttpBinHandler: ChannelInboundHandler { context.close(promise: nil) return case "/custom": - context.write(wrapOutboundOut(.head(HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: .ok))), promise: nil) + context.writeAndFlush(wrapOutboundOut(.head(HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: .ok))), promise: nil) return case "/events/10/1": // TODO: parse path context.write(wrapOutboundOut(.head(HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: .ok))), promise: nil) From cf2d4d615f9e2b3bd6ea6f7f4b2cd8c7d206cee9 Mon Sep 17 00:00:00 2001 From: Artem Redkin <aredkin@apple.com> Date: Mon, 14 Oct 2019 11:12:39 +0100 Subject: [PATCH 10/10] fix formatting --- Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift index 614d6afa1..40ebad75e 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientInternalTests.swift @@ -242,6 +242,7 @@ class HTTPClientInternalTests: XCTestCase { // At this point all other bytes should be delivered. XCTAssertEqual(delegate.reads, 4) } + func testRequestURITrailingSlash() throws { let request1 = try Request(url: "https://someserver.com:8888/some/path?foo=bar#ref") XCTAssertEqual(request1.url.uri, "/some/path?foo=bar")