Skip to content

Commit 014dbdb

Browse files
committed
Migrate HTTP1ProxyConnectHandler into nio-extras
Motivation: Moving the HTTP1ProxyConnectHandler into swift-nio-extras will make the code which is generally useful when dealing with HTTP1 proxies available more easily to a wider audience. Modifications: The code and tests are copied over from https://github.com/swift-server/async-http-client/blob/0b5bec741bfcf941e208d937de2ec29affe750a7/Sources/AsyncHTTPClient/ConnectionPool/ChannelHandler/HTTP1ProxyConnectHandler.swift. Result: HTTP1ProxyConnectHandler will be surfaced via the NIOExtras library
1 parent b89549b commit 014dbdb

File tree

6 files changed

+481
-0
lines changed

6 files changed

+481
-0
lines changed

NOTICE.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,14 @@ This product contains a derivation of the Tony Stone's 'process_test_files.rb'.
3232
* https://www.apache.org/licenses/LICENSE-2.0
3333
* HOMEPAGE:
3434
* https://codegists.com/snippet/ruby/generate_xctest_linux_runnerrb_tonystone_ruby
35+
36+
---
37+
38+
This product contains a derivation of "HTTP1ProxyConnectHandler.swift" and accompanying tests from AsyncHTTPClient.
39+
40+
* LICENSE (Apache License 2.0):
41+
* https://www.apache.org/licenses/LICENSE-2.0
42+
* HOMEPAGE:
43+
* https://github.com/swift-server/async-http-client
44+
45+
---

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ var targets: [PackageDescription.Target] = [
2121
dependencies: [
2222
.product(name: "NIO", package: "swift-nio"),
2323
.product(name: "NIOCore", package: "swift-nio"),
24+
.product(name: "NIOHTTP1", package: "swift-nio"),
2425
]),
2526
.target(
2627
name: "NIOHTTPCompression",
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftNIO open source project
4+
//
5+
// Copyright (c) 2022 Apple Inc. and the SwiftNIO project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIOCore
16+
import NIOHTTP1
17+
18+
public final class HTTP1ProxyConnectHandler: ChannelDuplexHandler, RemovableChannelHandler {
19+
public typealias OutboundIn = Never
20+
public typealias OutboundOut = HTTPClientRequestPart
21+
public typealias InboundIn = HTTPClientResponsePart
22+
23+
enum State {
24+
// transitions to `.connectSent` or `.failed`
25+
case initialized
26+
// transitions to `.headReceived` or `.failed`
27+
case connectSent(Scheduled<Void>)
28+
// transitions to `.completed` or `.failed`
29+
case headReceived(Scheduled<Void>)
30+
// final error state
31+
case failed(Error)
32+
// final success state
33+
case completed
34+
}
35+
36+
private var state: State = .initialized
37+
38+
private let targetHost: String
39+
private let targetPort: Int
40+
private let headers: HTTPHeaders
41+
private let deadline: NIODeadline
42+
43+
private var proxyEstablishedPromise: EventLoopPromise<Void>?
44+
var proxyEstablishedFuture: EventLoopFuture<Void>? {
45+
return self.proxyEstablishedPromise?.futureResult
46+
}
47+
48+
init(targetHost: String,
49+
targetPort: Int,
50+
headers: HTTPHeaders,
51+
deadline: NIODeadline) {
52+
self.targetHost = targetHost
53+
self.targetPort = targetPort
54+
self.headers = headers
55+
self.deadline = deadline
56+
}
57+
58+
public func handlerAdded(context: ChannelHandlerContext) {
59+
self.proxyEstablishedPromise = context.eventLoop.makePromise(of: Void.self)
60+
61+
self.sendConnect(context: context)
62+
}
63+
64+
public func handlerRemoved(context: ChannelHandlerContext) {
65+
switch self.state {
66+
case .failed, .completed:
67+
break
68+
case .initialized, .connectSent, .headReceived:
69+
self.state = .failed(Error.noResult)
70+
self.proxyEstablishedPromise?.fail(Error.noResult)
71+
}
72+
}
73+
74+
public func channelActive(context: ChannelHandlerContext) {
75+
self.sendConnect(context: context)
76+
}
77+
78+
public func channelInactive(context: ChannelHandlerContext) {
79+
switch self.state {
80+
case .initialized:
81+
preconditionFailure("How can we receive a channelInactive before a channelActive?")
82+
case .connectSent(let timeout), .headReceived(let timeout):
83+
timeout.cancel()
84+
self.failWithError(Error.remoteConnectionClosed, context: context, closeConnection: false)
85+
86+
case .failed, .completed:
87+
break
88+
}
89+
}
90+
91+
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
92+
preconditionFailure("We don't support outgoing traffic during HTTP Proxy update.")
93+
}
94+
95+
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
96+
switch self.unwrapInboundIn(data) {
97+
case .head(let head):
98+
self.handleHTTPHeadReceived(head, context: context)
99+
case .body:
100+
self.handleHTTPBodyReceived(context: context)
101+
case .end:
102+
self.handleHTTPEndReceived(context: context)
103+
}
104+
}
105+
106+
private func sendConnect(context: ChannelHandlerContext) {
107+
guard case .initialized = self.state else {
108+
// we might run into this handler twice, once in handlerAdded and once in channelActive.
109+
return
110+
}
111+
112+
let timeout = context.eventLoop.scheduleTask(deadline: self.deadline) {
113+
switch self.state {
114+
case .initialized:
115+
preconditionFailure("How can we have a scheduled timeout, if the connection is not even up?")
116+
117+
case .connectSent, .headReceived:
118+
self.failWithError(Error.httpProxyHandshakeTimeout, context: context)
119+
120+
case .failed, .completed:
121+
break
122+
}
123+
}
124+
125+
self.state = .connectSent(timeout)
126+
127+
let head = HTTPRequestHead(
128+
version: .init(major: 1, minor: 1),
129+
method: .CONNECT,
130+
uri: "\(self.targetHost):\(self.targetPort)",
131+
headers: self.headers
132+
)
133+
134+
context.write(self.wrapOutboundOut(.head(head)), promise: nil)
135+
context.write(self.wrapOutboundOut(.end(nil)), promise: nil)
136+
context.flush()
137+
}
138+
139+
private func handleHTTPHeadReceived(_ head: HTTPResponseHead, context: ChannelHandlerContext) {
140+
guard case .connectSent(let scheduled) = self.state else {
141+
preconditionFailure("HTTPDecoder should throw an error, if we have not send a request")
142+
}
143+
144+
switch head.status.code {
145+
case 200..<300:
146+
// Any 2xx (Successful) response indicates that the sender (and all
147+
// inbound proxies) will switch to tunnel mode immediately after the
148+
// blank line that concludes the successful response's header section
149+
self.state = .headReceived(scheduled)
150+
case 407:
151+
self.failWithError(Error.proxyAuthenticationRequired, context: context)
152+
153+
default:
154+
// Any response other than a successful response indicates that the tunnel
155+
// has not yet been formed and that the connection remains governed by HTTP.
156+
self.failWithError(Error.invalidProxyResponse, context: context)
157+
}
158+
}
159+
160+
private func handleHTTPBodyReceived(context: ChannelHandlerContext) {
161+
switch self.state {
162+
case .headReceived(let timeout):
163+
timeout.cancel()
164+
// we don't expect a body
165+
self.failWithError(Error.invalidProxyResponse, context: context)
166+
case .failed:
167+
// ran into an error before... ignore this one
168+
break
169+
case .completed, .connectSent, .initialized:
170+
preconditionFailure("Invalid state: \(self.state)")
171+
}
172+
}
173+
174+
private func handleHTTPEndReceived(context: ChannelHandlerContext) {
175+
switch self.state {
176+
case .headReceived(let timeout):
177+
timeout.cancel()
178+
self.state = .completed
179+
self.proxyEstablishedPromise?.succeed(())
180+
181+
case .failed:
182+
// ran into an error before... ignore this one
183+
break
184+
case .initialized, .connectSent, .completed:
185+
preconditionFailure("Invalid state: \(self.state)")
186+
}
187+
}
188+
189+
private func failWithError(_ error: Error, context: ChannelHandlerContext, closeConnection: Bool = true) {
190+
self.state = .failed(error)
191+
self.proxyEstablishedPromise?.fail(error)
192+
context.fireErrorCaught(error)
193+
if closeConnection {
194+
context.close(mode: .all, promise: nil)
195+
}
196+
}
197+
198+
/// Error types for ``HTTP1ProxyConnectHandler``
199+
public struct Error: Swift.Error, CustomStringConvertible, Equatable {
200+
fileprivate enum ErrorEnum: String {
201+
case proxyAuthenticationRequired
202+
case invalidProxyResponse
203+
case remoteConnectionClosed
204+
case httpProxyHandshakeTimeout
205+
case noResult
206+
}
207+
fileprivate let error: ErrorEnum
208+
209+
/// return as String
210+
public var description: String { return error.rawValue }
211+
212+
/// Proxy response status `407` indicates that authentication is required
213+
public static let proxyAuthenticationRequired = Error(error: .proxyAuthenticationRequired)
214+
215+
/// Proxy response contains unexpected status or body
216+
public static let invalidProxyResponse = Error(error: .invalidProxyResponse)
217+
218+
/// Connection has been closed for ongoing request
219+
public static let remoteConnectionClosed = Error(error: .remoteConnectionClosed)
220+
221+
/// Proxy connection handshake has timed out
222+
public static let httpProxyHandshakeTimeout = Error(error: .httpProxyHandshakeTimeout)
223+
224+
/// Handler was removed before we received a result for the request
225+
public static let noResult = Error(error: .noResult)
226+
}}

Tests/LinuxMain.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import XCTest
3434
testCase(DebugInboundEventsHandlerTest.allTests),
3535
testCase(DebugOutboundEventsHandlerTest.allTests),
3636
testCase(FixedLengthFrameDecoderTest.allTests),
37+
testCase(HTTP1ProxyConnectHandlerTests.allTests),
3738
testCase(HTTPRequestCompressorTest.allTests),
3839
testCase(HTTPRequestDecompressorTest.allTests),
3940
testCase(HTTPResponseCompressorTest.allTests),
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftNIO open source project
4+
//
5+
// Copyright (c) 2017-2022 Apple Inc. and the SwiftNIO project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
//
15+
// HTTP1ProxyConnectHandlerTests+XCTest.swift
16+
//
17+
import XCTest
18+
19+
///
20+
/// NOTE: This file was generated by generate_linux_tests.rb
21+
///
22+
/// Do NOT edit this file directly as it will be regenerated automatically when needed.
23+
///
24+
25+
extension HTTP1ProxyConnectHandlerTests {
26+
27+
static var allTests : [(String, (HTTP1ProxyConnectHandlerTests) -> () throws -> Void)] {
28+
return [
29+
("testProxyConnectWithoutAuthorizationSuccess", testProxyConnectWithoutAuthorizationSuccess),
30+
("testProxyConnectWithAuthorization", testProxyConnectWithAuthorization),
31+
("testProxyConnectWithoutAuthorizationFailure500", testProxyConnectWithoutAuthorizationFailure500),
32+
("testProxyConnectWithoutAuthorizationButAuthorizationNeeded", testProxyConnectWithoutAuthorizationButAuthorizationNeeded),
33+
("testProxyConnectReceivesBody", testProxyConnectReceivesBody),
34+
]
35+
}
36+
}
37+

0 commit comments

Comments
 (0)