Skip to content

Commit 47de4bb

Browse files
vkillweissi
authored andcommitted
Add authorization to proxy (swift-server#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
switchself.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
iflet timeout =self.resolve(timeout:self.configuration.timeout.read, deadline: deadline){
@@ -383,8 +383,8 @@ extension HTTPClient.Configuration{
383383
}
384384

385385
privateextensionChannelPipeline{
386-
func addProxyHandler(for request:HTTPClient.Request, decoder:ByteToMessageHandler<HTTPResponseDecoder>, encoder:HTTPRequestEncoder, tlsConfiguration:TLSConfiguration?)->EventLoopFuture<Void>{
387-
lethandler=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+
lethandler=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
privatevarcode:Code
@@ -464,4 +465,6 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible{
464465
publicstaticletinvalidProxyResponse=HTTPClientError(code:.invalidProxyResponse)
465466
/// Request does not contain `Content-Length` header.
466467
publicstaticletcontentLengthMissing=HTTPClientError(code:.contentLengthMissing)
468+
/// Proxy Authentication Required
469+
publicstaticletproxyAuthenticationRequired=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
publicvarhost:String
3232
/// Specifies Proxy server port.
3333
publicvarport:Int
34+
/// Specifies Proxy server authorization.
35+
publicvarauthorization:HTTPClient.Authorization?
3436

3537
/// Create proxy.
3638
///
3739
/// - parameters:
3840
/// - host: proxy server host.
3941
/// - port: proxy server port.
4042
publicstaticfunc 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+
publicstaticfunc 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
privatelethost:String
6375
privateletport:Int
76+
privateletauthorization:HTTPClient.Authorization?
6477
privatevaronConnect:(Channel)->EventLoopFuture<Void>
6578
privatevarwriteBuffer:CircularBuffer<WriteItem>
6679
privatevarreadBuffer:CircularBuffer<NIOAny>
6780
privatevarreadState: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+
case407:
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+
iflet 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+
publicstructAuthorization{
200+
privateenumScheme{
201+
case Basic(String)
202+
case Bearer(String)
203+
}
204+
205+
privateletscheme:Scheme
206+
207+
privateinit(scheme:Scheme){
208+
self.scheme = scheme
209+
}
210+
211+
publicstaticfunc basic(username:String, password:String)->HTTPClient.Authorization{
212+
return.basic(credentials:Data("\(username):\(password)".utf8).base64EncodedString())
213+
}
214+
215+
publicstaticfunc basic(credentials:String)->HTTPClient.Authorization{
216+
return.init(scheme:.Basic(credentials))
217+
}
218+
219+
publicstaticfunc bearer(tokens:String)->HTTPClient.Authorization{
220+
return.init(scheme:.Bearer(tokens))
221+
}
222+
223+
publicvarheaderValue:String{
224+
switchself.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
internalclassResponseAccumulator: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
iflet simulateProxy = simulateProxy {
119-
return channel.pipeline.addHandler(HTTPProxySimulator(option: simulateProxy), position:.first)
119+
letresponseEncoder=HTTPResponseEncoder()
120+
letrequestDecoder=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
finalclassHTTPProxySimulator:ChannelInboundHandler,RemovableChannelHandler{
141-
typealiasInboundIn=ByteBuffer
142-
typealiasInboundOut=ByteBuffer
143-
typealiasOutboundOut=ByteBuffer
144+
typealiasInboundIn=HTTPServerRequestPart
145+
typealiasInboundOut=HTTPServerResponsePart
146+
typealiasOutboundOut=HTTPServerResponsePart
144147

145148
enumOption{
146149
case plaintext
147150
case tls
148151
}
149152

150153
letoption:Option
154+
letencoder:HTTPResponseEncoder
155+
letdecoder:ByteToMessageHandler<HTTPRequestDecoder>
156+
varhead: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-
letresponse="""
158-
HTTP/1.1 200 OK\r\n\
159-
Content-Length: 0\r\n\
160-
Connection: close\r\n\
161-
\r\n
162-
"""
163-
varbuffer=self.unwrapInboundIn(data)
164-
letrequest= buffer.readString(length: buffer.readableBytes)!
165-
if request.hasPrefix("CONNECT"){
166-
varbuffer= context.channel.allocator.buffer(capacity:0)
167-
buffer.writeString(response)
168-
context.write(self.wrapInboundOut(buffer), promise:nil)
169-
context.flush()
166+
letrequest=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
switchself.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+
varauthorization=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
lethttpBin=HttpBin(simulateProxy:.plaintext)
295303
lethttpClient=HTTPClient(
@@ -321,6 +329,37 @@ class HTTPClientTests: XCTestCase{
321329
XCTAssertEqual(res.status,.ok)
322330
}
323331

332+
func testProxyPlaintextWithCorrectlyAuthorization()throws{
333+
lethttpBin=HttpBin(simulateProxy:.plaintext)
334+
lethttpClient=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+
letres=try httpClient.get(url:"http://test/ok").wait()
343+
XCTAssertEqual(res.status,.ok)
344+
}
345+
346+
func testProxyPlaintextWithIncorrectlyAuthorization()throws{
347+
lethttpBin=HttpBin(simulateProxy:.plaintext)
348+
lethttpClient=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+
returnXCTFail("Should fail with HTTPClientError.proxyAuthenticationRequired")
359+
}
360+
}
361+
}
362+
324363
func testUploadStreaming()throws{
325364
lethttpBin=HttpBin()
326365
lethttpClient=HTTPClient(eventLoopGroupProvider:.createNew)

0 commit comments

Comments
(0)