From 952af61f8d6b42453a8f1c745b28ae374efed240 Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Fri, 5 Apr 2019 12:26:52 -0400 Subject: [PATCH 1/6] add proxy support --- .gitignore | 1 + .../HTTPClientProxyHandler.swift | 114 ++++++++++++++++++ Sources/NIOHTTPClient/HTTPHandler.swift | 2 + Sources/NIOHTTPClient/SwiftNIOHTTP.swift | 87 +++++++++---- .../HTTPClientTestUtils.swift | 67 +++++++++- .../SwiftNIOHTTPTests+XCTest.swift | 23 ++++ .../SwiftNIOHTTPTests.swift | 31 +++++ 7 files changed, 296 insertions(+), 29 deletions(-) create mode 100644 Sources/NIOHTTPClient/HTTPClientProxyHandler.swift diff --git a/.gitignore b/.gitignore index f1b4020f3..cca8e27d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .build Package.resolved *.xcodeproj +DerivedData diff --git a/Sources/NIOHTTPClient/HTTPClientProxyHandler.swift b/Sources/NIOHTTPClient/HTTPClientProxyHandler.swift new file mode 100644 index 000000000..79915678c --- /dev/null +++ b/Sources/NIOHTTPClient/HTTPClientProxyHandler.swift @@ -0,0 +1,114 @@ +import Foundation +import NIO +import NIOHTTP1 + +/// Specifies the remote address of an HTTP proxy. +/// +/// Adding an `HTTPClientProxy` to your client's `HTTPClientConfiguration` +/// will cause requests to be passed through the specified proxy using the +/// HTTP `CONNECT` method. +/// +/// If a `TLSConfiguration` is used in conjunction with `HTTPClientProxy`, +/// TLS will be established _after_ successful proxy, between your client +/// and the destination server. +public struct HTTPClientProxy { + internal let host: String + internal let port: Int + + public static func server(host: String, port: Int) -> HTTPClientProxy { + return .init(host: host, port: port) + } +} + +internal final class HTTPClientProxyHandler: ChannelDuplexHandler, RemovableChannelHandler { + typealias InboundIn = HTTPClientResponsePart + typealias OutboundIn = HTTPClientRequestPart + typealias OutboundOut = HTTPClientRequestPart + + enum BufferItem { + case write(NIOAny, EventLoopPromise?) + case flush + } + + private let host: String + private let port: Int + private var onConnect: (Channel) -> EventLoopFuture + private var buffer: [BufferItem] + + init(host: String, port: Int, onConnect: @escaping (Channel) -> EventLoopFuture) { + self.host = host + self.port = port + self.onConnect = onConnect + self.buffer = [] + } + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + let res = self.unwrapInboundIn(data) + switch res { + case .head(let head): + switch head.status.code { + case 200..<300: + // Any 2xx (Successful) response indicates that the sender (and all + // inbound proxies) will switch to tunnel mode immediately after the + // blank line that concludes the successful response's header section + break + default: + // Any response other than a successful response + // indicates that the tunnel has not yet been formed and that the + // connection remains governed by HTTP. + context.fireErrorCaught(HTTPClientErrors.InvalidProxyResponseError()) + } + case .end: + _ = self.handleConnect(context: context) + case .body: + break + } + } + + func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { + self.buffer.append(.write(data, promise)) + } + + func flush(context: ChannelHandlerContext) { + self.buffer.append(.flush) + } + + func channelActive(context: ChannelHandlerContext) { + self.sendConnect(context: context) + context.fireChannelActive() + } + + // MARK: Private + + private func handleConnect(context: ChannelHandlerContext) -> EventLoopFuture { + return self.onConnect(context.channel).flatMap { + while self.buffer.count > 0 { + // make a copy of the current buffer and clear it in case any + // calls to context.write cause more requests to be buffered + let buffer = self.buffer + self.buffer = [] + buffer.forEach { item in + switch item { + case .flush: + context.flush() + case .write(let data, let promise): + context.write(data, promise: promise) + } + } + } + return context.pipeline.removeHandler(self) + } + } + + private func sendConnect(context: ChannelHandlerContext) { + var head = HTTPRequestHead( + version: .init(major: 1, minor: 1), + method: .CONNECT, + uri: "\(self.host):\(self.port)" + ) + head.headers.add(name: "proxy-connection", value: "keep-alive") + context.write(self.wrapOutboundOut(.head(head)), promise: nil) + context.write(self.wrapOutboundOut(.end(nil)), promise: nil) + context.flush() + } +} diff --git a/Sources/NIOHTTPClient/HTTPHandler.swift b/Sources/NIOHTTPClient/HTTPHandler.swift index 9afc6eaf3..a0804628e 100644 --- a/Sources/NIOHTTPClient/HTTPHandler.swift +++ b/Sources/NIOHTTPClient/HTTPHandler.swift @@ -38,6 +38,8 @@ public struct HTTPClientErrors { public struct RemoteConnectionClosedError: HTTPClientError {} public struct CancelledError: HTTPClientError {} + + public struct InvalidProxyResponseError : HTTPClientError {} } public enum HTTPBody: Equatable { diff --git a/Sources/NIOHTTPClient/SwiftNIOHTTP.swift b/Sources/NIOHTTPClient/SwiftNIOHTTP.swift index 07d8640d2..9d39f38fa 100644 --- a/Sources/NIOHTTPClient/SwiftNIOHTTP.swift +++ b/Sources/NIOHTTPClient/SwiftNIOHTTP.swift @@ -37,17 +37,20 @@ public struct HTTPClientConfiguration { public var tlsConfiguration: TLSConfiguration? public var followRedirects: Bool public var timeout: Timeout + public var proxy: HTTPClientProxy? - public init(tlsConfiguration: TLSConfiguration? = nil, followRedirects: Bool = false, timeout: Timeout = Timeout()) { + public init(tlsConfiguration: TLSConfiguration? = nil, followRedirects: Bool = false, timeout: Timeout = Timeout(), proxy: HTTPClientProxy? = nil) { self.tlsConfiguration = tlsConfiguration self.followRedirects = followRedirects self.timeout = timeout + self.proxy = proxy } - public init(certificateVerification: CertificateVerification, followRedirects: Bool = false, timeout: Timeout = Timeout()) { + public init(certificateVerification: CertificateVerification, followRedirects: Bool = false, timeout: Timeout = Timeout(), proxy: HTTPClientProxy? = nil) { self.tlsConfiguration = TLSConfiguration.forClient(certificateVerification: certificateVerification) self.followRedirects = followRedirects self.timeout = timeout + self.proxy = proxy } } @@ -152,24 +155,44 @@ public class HTTPClient { var bootstrap = ClientBootstrap(group: group) .channelOption(ChannelOptions.socket(SocketOptionLevel(IPPROTO_TCP), TCP_NODELAY), value: 1) .channelInitializer { channel in - channel.pipeline.addHTTPClientHandlers().flatMap { - self.configureSSL(channel: channel, useTLS: request.useTLS, hostname: request.host) - }.flatMap { - if let readTimeout = timeout.read { - return channel.pipeline.addHandler(IdleStateHandler(readTimeout: readTimeout)) - } else { - return channel.eventLoop.makeSucceededFuture(()) + let encoder = HTTPRequestEncoder() + let decoder = ByteToMessageHandler(HTTPResponseDecoder(leftOverBytesStrategy: .forwardBytes)) + return channel.pipeline.addHandlers([encoder, decoder], position: .first).flatMap { + switch self.configuration.proxy { + case .none: + return channel.pipeline.addSSLHandlerIfNeeded(for: request, tlsConfiguration: self.configuration.tlsConfiguration) + case .some: + return channel.pipeline.addProxyHandler(for: request, decoder: decoder, encoder: encoder, tlsConfiguration: self.configuration.tlsConfiguration) + } + }.flatMap { + if let readTimeout = timeout.read { + return channel.pipeline.addHandler(IdleStateHandler(readTimeout: readTimeout)) + } else { + return channel.eventLoop.makeSucceededFuture(()) + } + }.flatMap { + let taskHandler = HTTPTaskHandler(task: task, delegate: delegate, promise: promise, redirectHandler: redirectHandler) + return channel.pipeline.addHandler(taskHandler) } - }.flatMap { - channel.pipeline.addHandler(HTTPTaskHandler(task: task, delegate: delegate, promise: promise, redirectHandler: redirectHandler)) } - } if let connectTimeout = timeout.connect { bootstrap = bootstrap.connectTimeout(connectTimeout) } - bootstrap.connect(host: request.host, port: request.port) + let host: String + let port: Int + + switch self.configuration.proxy { + case .none: + host = request.host + port = request.port + case .some(let proxy): + host = proxy.host + port = proxy.port + } + + bootstrap.connect(host: host, port: port) .map { channel in task.setChannel(channel) } @@ -182,19 +205,35 @@ public class HTTPClient { return task } +} - private func configureSSL(channel: Channel, useTLS: Bool, hostname: String) -> EventLoopFuture { - if useTLS { - do { - let tlsConfiguration = self.configuration.tlsConfiguration ?? TLSConfiguration.forClient() - let context = try NIOSSLContext(configuration: tlsConfiguration) - return channel.pipeline.addHandler(try NIOSSLClientHandler(context: context, serverHostname: hostname), - position: .first) - } catch { - return channel.eventLoop.makeFailedFuture(error) +private extension ChannelPipeline { + func addProxyHandler(for request: HTTPRequest, decoder: ByteToMessageHandler, encoder: HTTPRequestEncoder, tlsConfiguration: TLSConfiguration?) -> EventLoopFuture { + let handler = HTTPClientProxyHandler(host: request.host, port: request.port, onConnect: { channel in + return channel.pipeline.removeHandler(decoder).flatMap { + return channel.pipeline.addHandler( + ByteToMessageHandler(HTTPResponseDecoder(leftOverBytesStrategy: .forwardBytes)), + position: .after(encoder) + ) + }.flatMap { + return channel.pipeline.addSSLHandlerIfNeeded(for: request, tlsConfiguration: tlsConfiguration) } - } else { - return channel.eventLoop.makeSucceededFuture(()) + }) + return self.addHandler(handler) + } + + func addSSLHandlerIfNeeded(for request: HTTPRequest, tlsConfiguration: TLSConfiguration?) -> EventLoopFuture { + guard request.useTLS else { + return self.eventLoop.makeSucceededFuture(()) + } + + do { + let tlsConfiguration = tlsConfiguration ?? TLSConfiguration.forClient() + let context = try NIOSSLContext(configuration: tlsConfiguration) + return self.addHandler(try NIOSSLClientHandler(context: context, serverHostname: request.host), + position: .first) + } catch { + return self.eventLoop.makeFailedFuture(error) } } } diff --git a/Tests/NIOHTTPClientTests/HTTPClientTestUtils.swift b/Tests/NIOHTTPClientTests/HTTPClientTestUtils.swift index aa537fd30..c229359fc 100644 --- a/Tests/NIOHTTPClientTests/HTTPClientTestUtils.swift +++ b/Tests/NIOHTTPClientTests/HTTPClientTestUtils.swift @@ -86,17 +86,32 @@ internal class HttpBin { return self.serverChannel.localAddress! } +<<<<<<< HEAD init(ssl: Bool = false) { self.serverChannel = try! ServerBootstrap(group: self.group) +======= + static func configureTLS(channel: Channel) -> EventLoopFuture { + let configuration = TLSConfiguration.forServer(certificateChain: [.certificate(try! NIOSSLCertificate(buffer: cert.utf8.map(Int8.init), format: .pem))], + privateKey: .privateKey(try! NIOSSLPrivateKey(buffer: key.utf8.map(Int8.init), format: .pem))) + let context = try! NIOSSLContext(configuration: configuration) + return channel.pipeline.addHandler(try! NIOSSLServerHandler(context: context), position: .first) + } + + init(ssl: Bool = false, simulateProxy: HTTPProxySimulator.Option? = nil) { + self.serverChannel = try! ServerBootstrap(group: group) +>>>>>>> add proxy support .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) .childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1) .childChannelInitializer { channel in - channel.pipeline.configureHTTPServerPipeline(withPipeliningAssistance: true, withErrorHandling: true).flatMap { + return channel.pipeline.configureHTTPServerPipeline(withPipeliningAssistance: true, withErrorHandling: true).flatMap { + if let simulateProxy = simulateProxy { + return channel.pipeline.addHandler(HTTPProxySimulator(option: simulateProxy), position: .first) + } else { + return channel.eventLoop.makeSucceededFuture(()) + } + }.flatMap { if ssl { - let configuration = TLSConfiguration.forServer(certificateChain: [.certificate(try! NIOSSLCertificate(buffer: cert.utf8.map(Int8.init), format: .pem))], - privateKey: .privateKey(try! NIOSSLPrivateKey(buffer: key.utf8.map(Int8.init), format: .pem))) - let context = try! NIOSSLContext(configuration: configuration) - return channel.pipeline.addHandler(try! NIOSSLServerHandler(context: context), position: .first).flatMap { + return HttpBin.configureTLS(channel: channel).flatMap { channel.pipeline.addHandler(HttpBinHandler()) } } else { @@ -111,6 +126,48 @@ internal class HttpBin { } } +final class HTTPProxySimulator: ChannelInboundHandler, RemovableChannelHandler { + typealias InboundIn = ByteBuffer + typealias InboundOut = ByteBuffer + typealias OutboundOut = ByteBuffer + + enum Option { + case plaintext + case tls + } + + let option: Option + + init(option: Option) { + self.option = option + } + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + let response = """ + HTTP/1.1 200 OK\r\n\ + Content-Length: 0\r\n\ + Connection: close\r\n\ + \r\n + """ + var buffer = self.unwrapInboundIn(data) + let request = buffer.readString(length: buffer.readableBytes)! + if request.hasPrefix("CONNECT") { + var buffer = context.channel.allocator.buffer(capacity: 0) + buffer.writeString(response) + context.write(self.wrapInboundOut(buffer), promise: nil) + context.flush() + context.channel.pipeline.removeHandler(self, promise: nil) + switch self.option { + case .tls: + _ = HttpBin.configureTLS(channel: context.channel) + case .plaintext: break + } + } else { + fatalError("Expected a CONNECT request") + } + } +} + internal struct HTTPResponseBuilder { let head: HTTPResponseHead var body: ByteBuffer? diff --git a/Tests/NIOHTTPClientTests/SwiftNIOHTTPTests+XCTest.swift b/Tests/NIOHTTPClientTests/SwiftNIOHTTPTests+XCTest.swift index 81367f387..cbc01ce6c 100644 --- a/Tests/NIOHTTPClientTests/SwiftNIOHTTPTests+XCTest.swift +++ b/Tests/NIOHTTPClientTests/SwiftNIOHTTPTests+XCTest.swift @@ -23,6 +23,7 @@ import XCTest /// extension SwiftHTTPTests { +<<<<<<< HEAD static var allTests: [(String, (SwiftHTTPTests) -> () throws -> Void)] { return [ ("testRequestURI", testRequestURI), @@ -40,4 +41,26 @@ extension SwiftHTTPTests { ("testCancel", testCancel), ] } +======= + + static var allTests : [(String, (SwiftHTTPTests) -> () throws -> Void)] { + return [ + ("testRequestURI", testRequestURI), + ("testHTTPPartsHandler", testHTTPPartsHandler), + ("testHTTPPartsHandlerMultiBody", testHTTPPartsHandlerMultiBody), + ("testGet", testGet), + ("testPost", testPost), + ("testGetHttps", testGetHttps), + ("testPostHttps", testPostHttps), + ("testHttpRedirect", testHttpRedirect), + ("testMultipleContentLengthHeaders", testMultipleContentLengthHeaders), + ("testStreaming", testStreaming), + ("testRemoteClose", testRemoteClose), + ("testReadTimeout", testReadTimeout), + ("testCancel", testCancel), + ("testProxyPlaintext", testProxyPlaintext), + ("testProxyTLS", testProxyTLS), + ] + } +>>>>>>> add proxy support } diff --git a/Tests/NIOHTTPClientTests/SwiftNIOHTTPTests.swift b/Tests/NIOHTTPClientTests/SwiftNIOHTTPTests.swift index 4bf9f7d2c..9d68010ed 100644 --- a/Tests/NIOHTTPClientTests/SwiftNIOHTTPTests.swift +++ b/Tests/NIOHTTPClientTests/SwiftNIOHTTPTests.swift @@ -270,4 +270,35 @@ class SwiftHTTPTests: XCTestCase { XCTFail("Unexpected error: \(error)") } } + + func testProxyPlaintext() throws { + let httpBin = HttpBin(simulateProxy: .plaintext) + let httpClient = HTTPClient( + eventLoopGroupProvider: .createNew, + configuration: .init(proxy: .server(host: "localhost", port: httpBin.port)) + ) + defer { + try! httpClient.syncShutdown() + httpBin.shutdown() + } + let res = try httpClient.get(url: "http://test/ok").wait() + XCTAssertEqual(res.status, .ok) + } + + func testProxyTLS() throws { + let httpBin = HttpBin(simulateProxy: .tls) + let httpClient = HTTPClient( + eventLoopGroupProvider: .createNew, + configuration: .init( + certificateVerification: .none, + proxy: .server(host: "localhost", port: httpBin.port) + ) + ) + defer { + try! httpClient.syncShutdown() + httpBin.shutdown() + } + let res = try httpClient.get(url: "https://test/ok").wait() + XCTAssertEqual(res.status, .ok) + } } From 3af37c91dc1f7f5dc69939255134d25e732bfc7f Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Thu, 11 Apr 2019 11:37:41 -0400 Subject: [PATCH 2/6] review comments --- .../HTTPClientProxyHandler.swift | 82 +++++++++++-------- Sources/NIOHTTPClient/SwiftNIOHTTP.swift | 26 +++--- 2 files changed, 60 insertions(+), 48 deletions(-) diff --git a/Sources/NIOHTTPClient/HTTPClientProxyHandler.swift b/Sources/NIOHTTPClient/HTTPClientProxyHandler.swift index 79915678c..ea0e5dc54 100644 --- a/Sources/NIOHTTPClient/HTTPClientProxyHandler.swift +++ b/Sources/NIOHTTPClient/HTTPClientProxyHandler.swift @@ -25,52 +25,66 @@ internal final class HTTPClientProxyHandler: ChannelDuplexHandler, RemovableChan typealias OutboundIn = HTTPClientRequestPart typealias OutboundOut = HTTPClientRequestPart - enum BufferItem { + enum WriteItem { case write(NIOAny, EventLoopPromise?) case flush } + enum ReadState { + case awaitingResponse + case connecting + } + private let host: String private let port: Int private var onConnect: (Channel) -> EventLoopFuture - private var buffer: [BufferItem] + private var writeBuffer: CircularBuffer + private var readBuffer: CircularBuffer + private var readState: ReadState init(host: String, port: Int, onConnect: @escaping (Channel) -> EventLoopFuture) { self.host = host self.port = port self.onConnect = onConnect - self.buffer = [] + self.writeBuffer = .init() + self.readBuffer = .init() + self.readState = .awaitingResponse } func channelRead(context: ChannelHandlerContext, data: NIOAny) { - let res = self.unwrapInboundIn(data) - switch res { - case .head(let head): - switch head.status.code { - case 200..<300: - // Any 2xx (Successful) response indicates that the sender (and all - // inbound proxies) will switch to tunnel mode immediately after the - // blank line that concludes the successful response's header section + switch self.readState { + case .awaitingResponse: + let res = self.unwrapInboundIn(data) + switch res { + case .head(let head): + switch head.status.code { + case 200..<300: + // Any 2xx (Successful) response indicates that the sender (and all + // inbound proxies) will switch to tunnel mode immediately after the + // blank line that concludes the successful response's header section + break + default: + // Any response other than a successful response + // indicates that the tunnel has not yet been formed and that the + // connection remains governed by HTTP. + context.fireErrorCaught(HTTPClientErrors.InvalidProxyResponseError()) + } + case .end: + _ = self.handleConnect(context: context) + case .body: break - default: - // Any response other than a successful response - // indicates that the tunnel has not yet been formed and that the - // connection remains governed by HTTP. - context.fireErrorCaught(HTTPClientErrors.InvalidProxyResponseError()) } - case .end: - _ = self.handleConnect(context: context) - case .body: - break + case .connecting: + self.readBuffer.append(data) } } func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { - self.buffer.append(.write(data, promise)) + self.writeBuffer.append(.write(data, promise)) } func flush(context: ChannelHandlerContext) { - self.buffer.append(.flush) + self.writeBuffer.append(.flush) } func channelActive(context: ChannelHandlerContext) { @@ -82,18 +96,18 @@ internal final class HTTPClientProxyHandler: ChannelDuplexHandler, RemovableChan private func handleConnect(context: ChannelHandlerContext) -> EventLoopFuture { return self.onConnect(context.channel).flatMap { - while self.buffer.count > 0 { - // make a copy of the current buffer and clear it in case any - // calls to context.write cause more requests to be buffered - let buffer = self.buffer - self.buffer = [] - buffer.forEach { item in - switch item { - case .flush: - context.flush() - case .write(let data, let promise): - context.write(data, promise: promise) - } + // forward any buffered reads + while !self.readBuffer.isEmpty { + context.fireChannelRead(self.readBuffer.removeFirst()) + } + + // calls to context.write may be re-entrant + while !self.writeBuffer.isEmpty { + switch self.writeBuffer.removeFirst() { + case .flush: + context.flush() + case .write(let data, let promise): + context.write(data, promise: promise) } } return context.pipeline.removeHandler(self) diff --git a/Sources/NIOHTTPClient/SwiftNIOHTTP.swift b/Sources/NIOHTTPClient/SwiftNIOHTTP.swift index 9d39f38fa..e16352cc9 100644 --- a/Sources/NIOHTTPClient/SwiftNIOHTTP.swift +++ b/Sources/NIOHTTPClient/SwiftNIOHTTP.swift @@ -179,20 +179,9 @@ public class HTTPClient { if let connectTimeout = timeout.connect { bootstrap = bootstrap.connectTimeout(connectTimeout) } - - let host: String - let port: Int - - switch self.configuration.proxy { - case .none: - host = request.host - port = request.port - case .some(let proxy): - host = proxy.host - port = proxy.port - } - - bootstrap.connect(host: host, port: port) + + let address = self.resolveAddress(request: request, proxy: self.configuration.proxy) + bootstrap.connect(host: address.0, port: address.1) .map { channel in task.setChannel(channel) } @@ -205,6 +194,15 @@ public class HTTPClient { return task } + + private func resolveAddress(request: HTTPRequest, proxy:HTTPClientProxy?) -> (String, Int) { + switch self.configuration.proxy { + case .none: + return (request.host, request.port) + case .some(let proxy): + return (proxy.host, proxy.port) + } + } } private extension ChannelPipeline { From 5670c3cb477e8cc6de6ee95d1c05e5e0109dc560 Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Thu, 11 Apr 2019 11:40:28 -0400 Subject: [PATCH 3/6] change read state --- Sources/NIOHTTPClient/HTTPClientProxyHandler.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/NIOHTTPClient/HTTPClientProxyHandler.swift b/Sources/NIOHTTPClient/HTTPClientProxyHandler.swift index ea0e5dc54..ba35a0a71 100644 --- a/Sources/NIOHTTPClient/HTTPClientProxyHandler.swift +++ b/Sources/NIOHTTPClient/HTTPClientProxyHandler.swift @@ -70,6 +70,7 @@ internal final class HTTPClientProxyHandler: ChannelDuplexHandler, RemovableChan context.fireErrorCaught(HTTPClientErrors.InvalidProxyResponseError()) } case .end: + self.readState = .connecting _ = self.handleConnect(context: context) case .body: break From c3134971cdbec48975d581e9d56efe4544bd94ab Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Thu, 11 Apr 2019 11:44:32 -0400 Subject: [PATCH 4/6] add third read state --- Sources/NIOHTTPClient/HTTPClientProxyHandler.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/NIOHTTPClient/HTTPClientProxyHandler.swift b/Sources/NIOHTTPClient/HTTPClientProxyHandler.swift index ba35a0a71..3ba396e5d 100644 --- a/Sources/NIOHTTPClient/HTTPClientProxyHandler.swift +++ b/Sources/NIOHTTPClient/HTTPClientProxyHandler.swift @@ -33,6 +33,7 @@ internal final class HTTPClientProxyHandler: ChannelDuplexHandler, RemovableChan enum ReadState { case awaitingResponse case connecting + case connected } private let host: String @@ -77,6 +78,8 @@ internal final class HTTPClientProxyHandler: ChannelDuplexHandler, RemovableChan } case .connecting: self.readBuffer.append(data) + case .connected: + context.fireChannelRead(data) } } @@ -97,6 +100,8 @@ internal final class HTTPClientProxyHandler: ChannelDuplexHandler, RemovableChan private func handleConnect(context: ChannelHandlerContext) -> EventLoopFuture { return self.onConnect(context.channel).flatMap { + self.readState = .connected + // forward any buffered reads while !self.readBuffer.isEmpty { context.fireChannelRead(self.readBuffer.removeFirst()) From 629ac350e18331a461b525ce784ecdefb8700465 Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Fri, 12 Apr 2019 11:34:43 -0400 Subject: [PATCH 5/6] review comments --- .../HTTPClientProxyHandler.swift | 15 ++++++++++- Sources/NIOHTTPClient/SwiftNIOHTTP.swift | 4 +-- .../SwiftNIOHTTPTests+XCTest.swift | 25 ++----------------- 3 files changed, 18 insertions(+), 26 deletions(-) diff --git a/Sources/NIOHTTPClient/HTTPClientProxyHandler.swift b/Sources/NIOHTTPClient/HTTPClientProxyHandler.swift index 3ba396e5d..09b5748f9 100644 --- a/Sources/NIOHTTPClient/HTTPClientProxyHandler.swift +++ b/Sources/NIOHTTPClient/HTTPClientProxyHandler.swift @@ -1,4 +1,17 @@ -import Foundation +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIOHTTPClient open source project +// +// Copyright (c) 2018-2019 Swift Server Working Group and the SwiftNIOHTTPClient project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIOHTTPClient project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + import NIO import NIOHTTP1 diff --git a/Sources/NIOHTTPClient/SwiftNIOHTTP.swift b/Sources/NIOHTTPClient/SwiftNIOHTTP.swift index e16352cc9..250c8dfc8 100644 --- a/Sources/NIOHTTPClient/SwiftNIOHTTP.swift +++ b/Sources/NIOHTTPClient/SwiftNIOHTTP.swift @@ -181,7 +181,7 @@ public class HTTPClient { } let address = self.resolveAddress(request: request, proxy: self.configuration.proxy) - bootstrap.connect(host: address.0, port: address.1) + bootstrap.connect(host: address.host, port: address.port) .map { channel in task.setChannel(channel) } @@ -195,7 +195,7 @@ public class HTTPClient { return task } - private func resolveAddress(request: HTTPRequest, proxy:HTTPClientProxy?) -> (String, Int) { + private func resolveAddress(request: HTTPRequest, proxy: HTTPClientProxy?) -> (host: String, port: Int) { switch self.configuration.proxy { case .none: return (request.host, request.port) diff --git a/Tests/NIOHTTPClientTests/SwiftNIOHTTPTests+XCTest.swift b/Tests/NIOHTTPClientTests/SwiftNIOHTTPTests+XCTest.swift index cbc01ce6c..760e21cf9 100644 --- a/Tests/NIOHTTPClientTests/SwiftNIOHTTPTests+XCTest.swift +++ b/Tests/NIOHTTPClientTests/SwiftNIOHTTPTests+XCTest.swift @@ -23,7 +23,6 @@ import XCTest /// extension SwiftHTTPTests { -<<<<<<< HEAD static var allTests: [(String, (SwiftHTTPTests) -> () throws -> Void)] { return [ ("testRequestURI", testRequestURI), @@ -39,28 +38,8 @@ extension SwiftHTTPTests { ("testRemoteClose", testRemoteClose), ("testReadTimeout", testReadTimeout), ("testCancel", testCancel), + ("testProxyPlaintext", testProxyPlaintext), + ("testProxyTLS", testProxyTLS), ] } -======= - - static var allTests : [(String, (SwiftHTTPTests) -> () throws -> Void)] { - return [ - ("testRequestURI", testRequestURI), - ("testHTTPPartsHandler", testHTTPPartsHandler), - ("testHTTPPartsHandlerMultiBody", testHTTPPartsHandlerMultiBody), - ("testGet", testGet), - ("testPost", testPost), - ("testGetHttps", testGetHttps), - ("testPostHttps", testPostHttps), - ("testHttpRedirect", testHttpRedirect), - ("testMultipleContentLengthHeaders", testMultipleContentLengthHeaders), - ("testStreaming", testStreaming), - ("testRemoteClose", testRemoteClose), - ("testReadTimeout", testReadTimeout), - ("testCancel", testCancel), - ("testProxyPlaintext", testProxyPlaintext), - ("testProxyTLS", testProxyTLS), - ] - } ->>>>>>> add proxy support } From 9ad2d7bbf45ee3768b9064c6901ad1e6525db5dc Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Fri, 12 Apr 2019 11:37:26 -0400 Subject: [PATCH 6/6] fix merge conflict in tests --- Tests/NIOHTTPClientTests/HTTPClientTestUtils.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Tests/NIOHTTPClientTests/HTTPClientTestUtils.swift b/Tests/NIOHTTPClientTests/HTTPClientTestUtils.swift index c229359fc..47a2f95b7 100644 --- a/Tests/NIOHTTPClientTests/HTTPClientTestUtils.swift +++ b/Tests/NIOHTTPClientTests/HTTPClientTestUtils.swift @@ -86,10 +86,6 @@ internal class HttpBin { return self.serverChannel.localAddress! } -<<<<<<< HEAD - init(ssl: Bool = false) { - self.serverChannel = try! ServerBootstrap(group: self.group) -======= static func configureTLS(channel: Channel) -> EventLoopFuture { let configuration = TLSConfiguration.forServer(certificateChain: [.certificate(try! NIOSSLCertificate(buffer: cert.utf8.map(Int8.init), format: .pem))], privateKey: .privateKey(try! NIOSSLPrivateKey(buffer: key.utf8.map(Int8.init), format: .pem))) @@ -99,7 +95,6 @@ internal class HttpBin { init(ssl: Bool = false, simulateProxy: HTTPProxySimulator.Option? = nil) { self.serverChannel = try! ServerBootstrap(group: group) ->>>>>>> add proxy support .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) .childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1) .childChannelInitializer { channel in