Skip to content

Commit 47de4bb

Browse files
vkillweissi
authored andcommitted
Add authorization to proxy (#94)
1 parent 244aea6 commit 47de4bb

File tree

6 files changed

+139
-26
lines changed

6 files changed

+139
-26
lines changed

Sources/AsyncHTTPClient/HTTPClient.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,8 @@ public class HTTPClient {
227227
switch self.configuration.proxy {
228228
case .none:
229229
return channel.pipeline.addSSLHandlerIfNeeded(for: request, tlsConfiguration: self.configuration.tlsConfiguration)
230-
case .some:
231-
return channel.pipeline.addProxyHandler(for: request, decoder: decoder, encoder: encoder, tlsConfiguration: self.configuration.tlsConfiguration)
230+
case .some(let proxy):
231+
return channel.pipeline.addProxyHandler(for: request, decoder: decoder, encoder: encoder, tlsConfiguration: self.configuration.tlsConfiguration, proxy: proxy)
232232
}
233233
}.flatMap {
234234
if let timeout = self.resolve(timeout: self.configuration.timeout.read, deadline: deadline) {
@@ -383,8 +383,8 @@ extension HTTPClient.Configuration {
383383
}
384384

385385
private extension ChannelPipeline {
386-
func addProxyHandler(for request: HTTPClient.Request, decoder: ByteToMessageHandler<HTTPResponseDecoder>, encoder: HTTPRequestEncoder, tlsConfiguration: TLSConfiguration?) -> EventLoopFuture<Void> {
387-
let handler = HTTPClientProxyHandler(host: request.host, port: request.port, onConnect: { channel in
386+
func addProxyHandler(for request: HTTPClient.Request, decoder: ByteToMessageHandler<HTTPResponseDecoder>, encoder: HTTPRequestEncoder, tlsConfiguration: TLSConfiguration?, proxy: HTTPClient.Configuration.Proxy?) -> EventLoopFuture<Void> {
387+
let handler = HTTPClientProxyHandler(host: request.host, port: request.port, authorization: proxy?.authorization, onConnect: { channel in
388388
channel.pipeline.removeHandler(decoder).flatMap {
389389
return channel.pipeline.addHandler(
390390
ByteToMessageHandler(HTTPResponseDecoder(leftOverBytesStrategy: .forwardBytes)),
@@ -428,6 +428,7 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
428428
case chunkedSpecifiedMultipleTimes
429429
case invalidProxyResponse
430430
case contentLengthMissing
431+
case proxyAuthenticationRequired
431432
}
432433

433434
private var code: Code
@@ -464,4 +465,6 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
464465
public static let invalidProxyResponse = HTTPClientError(code: .invalidProxyResponse)
465466
/// Request does not contain `Content-Length` header.
466467
public static let contentLengthMissing = HTTPClientError(code: .contentLengthMissing)
468+
/// Proxy Authentication Required
469+
public static let proxyAuthenticationRequired = HTTPClientError(code: .proxyAuthenticationRequired)
467470
}

Sources/AsyncHTTPClient/HTTPClientProxyHandler.swift

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,26 @@ public extension HTTPClient.Configuration {
3131
public var host: String
3232
/// Specifies Proxy server port.
3333
public var port: Int
34+
/// Specifies Proxy server authorization.
35+
public var authorization: HTTPClient.Authorization?
3436

3537
/// Create proxy.
3638
///
3739
/// - parameters:
3840
/// - host: proxy server host.
3941
/// - port: proxy server port.
4042
public static func server(host: String, port: Int) -> Proxy {
41-
return .init(host: host, port: port)
43+
return .init(host: host, port: port, authorization: nil)
44+
}
45+
46+
/// Create proxy.
47+
///
48+
/// - parameters:
49+
/// - host: proxy server host.
50+
/// - port: proxy server port.
51+
/// - authorization: proxy server authorization.
52+
public static func server(host: String, port: Int, authorization: HTTPClient.Authorization? = nil) -> Proxy {
53+
return .init(host: host, port: port, authorization: authorization)
4254
}
4355
}
4456
}
@@ -61,14 +73,16 @@ internal final class HTTPClientProxyHandler: ChannelDuplexHandler, RemovableChan
6173

6274
private let host: String
6375
private let port: Int
76+
private let authorization: HTTPClient.Authorization?
6477
private var onConnect: (Channel) -> EventLoopFuture<Void>
6578
private var writeBuffer: CircularBuffer<WriteItem>
6679
private var readBuffer: CircularBuffer<NIOAny>
6780
private var readState: ReadState
6881

69-
init(host: String, port: Int, onConnect: @escaping (Channel) -> EventLoopFuture<Void>) {
82+
init(host: String, port: Int, authorization: HTTPClient.Authorization?, onConnect: @escaping (Channel) -> EventLoopFuture<Void>) {
7083
self.host = host
7184
self.port = port
85+
self.authorization = authorization
7286
self.onConnect = onConnect
7387
self.writeBuffer = .init()
7488
self.readBuffer = .init()
@@ -87,6 +101,8 @@ internal final class HTTPClientProxyHandler: ChannelDuplexHandler, RemovableChan
87101
// inbound proxies) will switch to tunnel mode immediately after the
88102
// blank line that concludes the successful response's header section
89103
break
104+
case 407:
105+
context.fireErrorCaught(HTTPClientError.proxyAuthenticationRequired)
90106
default:
91107
// Any response other than a successful response
92108
// indicates that the tunnel has not yet been formed and that the
@@ -150,6 +166,9 @@ internal final class HTTPClientProxyHandler: ChannelDuplexHandler, RemovableChan
150166
uri: "\(self.host):\(self.port)"
151167
)
152168
head.headers.add(name: "proxy-connection", value: "keep-alive")
169+
if let authorization = authorization {
170+
head.headers.add(name: "proxy-authorization", value: authorization.headerValue)
171+
}
153172
context.write(self.wrapOutboundOut(.head(head)), promise: nil)
154173
context.write(self.wrapOutboundOut(.end(nil)), promise: nil)
155174
context.flush()

Sources/AsyncHTTPClient/HTTPHandler.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,41 @@ extension HTTPClient {
194194
self.body = body
195195
}
196196
}
197+
198+
/// HTTP authentication
199+
public struct Authorization {
200+
private enum Scheme {
201+
case Basic(String)
202+
case Bearer(String)
203+
}
204+
205+
private let scheme: Scheme
206+
207+
private init(scheme: Scheme) {
208+
self.scheme = scheme
209+
}
210+
211+
public static func basic(username: String, password: String) -> HTTPClient.Authorization {
212+
return .basic(credentials: Data("\(username):\(password)".utf8).base64EncodedString())
213+
}
214+
215+
public static func basic(credentials: String) -> HTTPClient.Authorization {
216+
return .init(scheme: .Basic(credentials))
217+
}
218+
219+
public static func bearer(tokens: String) -> HTTPClient.Authorization {
220+
return .init(scheme: .Bearer(tokens))
221+
}
222+
223+
public var headerValue: String {
224+
switch self.scheme {
225+
case .Basic(let credentials):
226+
return "Basic \(credentials)"
227+
case .Bearer(let tokens):
228+
return "Bearer \(tokens)"
229+
}
230+
}
231+
}
197232
}
198233

199234
internal class ResponseAccumulator: HTTPClientResponseDelegate {

Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,10 @@ internal class HttpBin {
116116
.childChannelInitializer { channel in
117117
channel.pipeline.configureHTTPServerPipeline(withPipeliningAssistance: true, withErrorHandling: true).flatMap {
118118
if let simulateProxy = simulateProxy {
119-
return channel.pipeline.addHandler(HTTPProxySimulator(option: simulateProxy), position: .first)
119+
let responseEncoder = HTTPResponseEncoder()
120+
let requestDecoder = ByteToMessageHandler(HTTPRequestDecoder(leftOverBytesStrategy: .forwardBytes))
121+
122+
return channel.pipeline.addHandlers([responseEncoder, requestDecoder, HTTPProxySimulator(option: simulateProxy, encoder: responseEncoder, decoder: requestDecoder)], position: .first)
120123
} else {
121124
return channel.eventLoop.makeSucceededFuture(())
122125
}
@@ -138,43 +141,54 @@ internal class HttpBin {
138141
}
139142

140143
final class HTTPProxySimulator: ChannelInboundHandler, RemovableChannelHandler {
141-
typealias InboundIn = ByteBuffer
142-
typealias InboundOut = ByteBuffer
143-
typealias OutboundOut = ByteBuffer
144+
typealias InboundIn = HTTPServerRequestPart
145+
typealias InboundOut = HTTPServerResponsePart
146+
typealias OutboundOut = HTTPServerResponsePart
144147

145148
enum Option {
146149
case plaintext
147150
case tls
148151
}
149152

150153
let option: Option
154+
let encoder: HTTPResponseEncoder
155+
let decoder: ByteToMessageHandler<HTTPRequestDecoder>
156+
var head: HTTPResponseHead
151157

152-
init(option: Option) {
158+
init(option: Option, encoder: HTTPResponseEncoder, decoder: ByteToMessageHandler<HTTPRequestDecoder>) {
153159
self.option = option
160+
self.encoder = encoder
161+
self.decoder = decoder
162+
self.head = HTTPResponseHead(version: .init(major: 1, minor: 1), status: .ok, headers: .init([("Content-Length", "0"), ("Connection", "close")]))
154163
}
155164

156165
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
157-
let response = """
158-
HTTP/1.1 200 OK\r\n\
159-
Content-Length: 0\r\n\
160-
Connection: close\r\n\
161-
\r\n
162-
"""
163-
var buffer = self.unwrapInboundIn(data)
164-
let request = buffer.readString(length: buffer.readableBytes)!
165-
if request.hasPrefix("CONNECT") {
166-
var buffer = context.channel.allocator.buffer(capacity: 0)
167-
buffer.writeString(response)
168-
context.write(self.wrapInboundOut(buffer), promise: nil)
169-
context.flush()
166+
let request = self.unwrapInboundIn(data)
167+
switch request {
168+
case .head(let head):
169+
guard head.method == .CONNECT else {
170+
fatalError("Expected a CONNECT request")
171+
}
172+
if head.headers.contains(name: "proxy-authorization") {
173+
if head.headers["proxy-authorization"].first != "Basic YWxhZGRpbjpvcGVuc2VzYW1l" {
174+
self.head.status = .proxyAuthenticationRequired
175+
}
176+
}
177+
case .body:
178+
()
179+
case .end:
180+
context.write(self.wrapOutboundOut(.head(self.head)), promise: nil)
181+
context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil)
182+
170183
context.channel.pipeline.removeHandler(self, promise: nil)
184+
context.channel.pipeline.removeHandler(self.decoder, promise: nil)
185+
context.channel.pipeline.removeHandler(self.encoder, promise: nil)
186+
171187
switch self.option {
172188
case .tls:
173189
_ = HttpBin.configureTLS(channel: context.channel)
174190
case .plaintext: break
175191
}
176-
} else {
177-
fatalError("Expected a CONNECT request")
178192
}
179193
}
180194
}

Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,11 @@ extension HTTPClientTests {
4242
("testReadTimeout", testReadTimeout),
4343
("testDeadline", testDeadline),
4444
("testCancel", testCancel),
45+
("testHTTPClientAuthorization", testHTTPClientAuthorization),
4546
("testProxyPlaintext", testProxyPlaintext),
4647
("testProxyTLS", testProxyTLS),
48+
("testProxyPlaintextWithCorrectlyAuthorization", testProxyPlaintextWithCorrectlyAuthorization),
49+
("testProxyPlaintextWithIncorrectlyAuthorization", testProxyPlaintextWithIncorrectlyAuthorization),
4750
("testUploadStreaming", testUploadStreaming),
4851
("testNoContentLengthForSSLUncleanShutdown", testNoContentLengthForSSLUncleanShutdown),
4952
("testNoContentLengthWithIgnoreErrorForSSLUncleanShutdown", testNoContentLengthWithIgnoreErrorForSSLUncleanShutdown),

Tests/AsyncHTTPClientTests/HTTPClientTests.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,14 @@ class HTTPClientTests: XCTestCase {
290290
}
291291
}
292292

293+
func testHTTPClientAuthorization() {
294+
var authorization = HTTPClient.Authorization.basic(username: "aladdin", password: "opensesame")
295+
XCTAssertEqual(authorization.headerValue, "Basic YWxhZGRpbjpvcGVuc2VzYW1l")
296+
297+
authorization = HTTPClient.Authorization.bearer(tokens: "mF_9.B5f-4.1JqM")
298+
XCTAssertEqual(authorization.headerValue, "Bearer mF_9.B5f-4.1JqM")
299+
}
300+
293301
func testProxyPlaintext() throws {
294302
let httpBin = HttpBin(simulateProxy: .plaintext)
295303
let httpClient = HTTPClient(
@@ -321,6 +329,37 @@ class HTTPClientTests: XCTestCase {
321329
XCTAssertEqual(res.status, .ok)
322330
}
323331

332+
func testProxyPlaintextWithCorrectlyAuthorization() throws {
333+
let httpBin = HttpBin(simulateProxy: .plaintext)
334+
let httpClient = HTTPClient(
335+
eventLoopGroupProvider: .createNew,
336+
configuration: .init(proxy: .server(host: "localhost", port: httpBin.port, authorization: .basic(username: "aladdin", password: "opensesame")))
337+
)
338+
defer {
339+
try! httpClient.syncShutdown()
340+
httpBin.shutdown()
341+
}
342+
let res = try httpClient.get(url: "http://test/ok").wait()
343+
XCTAssertEqual(res.status, .ok)
344+
}
345+
346+
func testProxyPlaintextWithIncorrectlyAuthorization() throws {
347+
let httpBin = HttpBin(simulateProxy: .plaintext)
348+
let httpClient = HTTPClient(
349+
eventLoopGroupProvider: .createNew,
350+
configuration: .init(proxy: .server(host: "localhost", port: httpBin.port, authorization: .basic(username: "aladdin", password: "opensesamefoo")))
351+
)
352+
defer {
353+
try! httpClient.syncShutdown()
354+
httpBin.shutdown()
355+
}
356+
XCTAssertThrowsError(try httpClient.get(url: "http://test/ok").wait(), "Should fail") { error in
357+
guard case let error = error as? HTTPClientError, error == .proxyAuthenticationRequired else {
358+
return XCTFail("Should fail with HTTPClientError.proxyAuthenticationRequired")
359+
}
360+
}
361+
}
362+
324363
func testUploadStreaming() throws {
325364
let httpBin = HttpBin()
326365
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)

0 commit comments

Comments
 (0)