-
Notifications
You must be signed in to change notification settings - Fork 125
Implement SOCKS proxy functionality #375
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 18 commits
5a49c2c
b45f4d9
8e1232a
974677f
122f650
f1598be
481e1b1
064edcb
210c4d7
18c7103
323eefd
c273847
3c0e120
ba629a4
e4c1b52
d718299
eee59e3
112d5b1
976fe2e
ea04cfa
ddc009a
221420f
91a1636
15e5334
c61184e
97c6b7f
dd405af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -342,8 +342,8 @@ extension HTTPClient { | |
} | ||
|
||
/// HTTP authentication | ||
public struct Authorization { | ||
private enum Scheme { | ||
public struct Authorization: Hashable { | ||
private enum Scheme: Hashable { | ||
Comment on lines
+345
to
+346
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why was this added? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Getting equatable conformance on the internal Proxy Type enum. |
||
case Basic(String) | ||
case Bearer(String) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,7 @@ import NIOConcurrencyHelpers | |
import NIOFoundationCompat | ||
import NIOHTTP1 | ||
import NIOHTTPCompression | ||
import NIOSOCKS | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need this here :) |
||
import NIOSSL | ||
import NIOTestUtils | ||
import NIOTransportServices | ||
|
@@ -709,6 +710,59 @@ class HTTPClientTests: XCTestCase { | |
} | ||
} | ||
|
||
func testProxySOCKS() throws { | ||
Davidde94 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let socksBin = try MockSOCKSServer(expectedURL: "/socks/test", expectedResponse: "it works!") | ||
let localClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), | ||
configuration: .init(proxy: .socksServer(host: "127.0.0.1"))) | ||
|
||
defer { | ||
XCTAssertNoThrow(try localClient.syncShutdown()) | ||
XCTAssertNoThrow(try socksBin.shutdown()) | ||
} | ||
|
||
Davidde94 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
var response: HTTPClient.Response? | ||
XCTAssertNoThrow(response = try localClient.get(url: "http://127.0.0.1/socks/test").wait()) | ||
XCTAssertEqual(.ok, response?.status) | ||
XCTAssertEqual(ByteBuffer(string: "it works!"), response?.body) | ||
} | ||
|
||
// there is no socks server, so we should fail | ||
func testProxySOCKSFailureNoServer() throws { | ||
let localHTTPBin = HTTPBin() | ||
let localClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), | ||
configuration: .init(proxy: .socksServer(host: "127.0.0.1", port: localHTTPBin.port))) | ||
defer { | ||
XCTAssertNoThrow(try localClient.syncShutdown()) | ||
XCTAssertNoThrow(try localHTTPBin.shutdown()) | ||
} | ||
XCTAssertThrowsError(try localClient.get(url: "http://127.0.0.1/socks/test").wait()) | ||
} | ||
|
||
// speak to a server that doesn't speak SOCKS | ||
func testProxySOCKSFailureInvalidServer() throws { | ||
let localClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), | ||
configuration: .init(proxy: .socksServer(host: "127.0.0.1"))) | ||
defer { | ||
XCTAssertNoThrow(try localClient.syncShutdown()) | ||
} | ||
XCTAssertThrowsError(try localClient.get(url: "http://127.0.0.1/socks/test").wait()) | ||
} | ||
|
||
// test a handshake failure with a misbehaving server | ||
func testProxySOCKSMisbehavingServer() throws { | ||
let socksBin = try MockSOCKSServer(expectedURL: "/socks/test", expectedResponse: "it works!", misbehave: true) | ||
let localClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), | ||
configuration: .init(proxy: .socksServer(host: "127.0.0.1"))) | ||
|
||
defer { | ||
XCTAssertNoThrow(try localClient.syncShutdown()) | ||
XCTAssertNoThrow(try socksBin.shutdown()) | ||
} | ||
|
||
// the server will send a bogus message in response to the clients request | ||
XCTAssertThrowsError(try localClient.get(url: "http://127.0.0.1/socks/test").wait()) | ||
} | ||
|
||
func testUploadStreaming() throws { | ||
let body: HTTPClient.Body = .stream(length: 8) { writer in | ||
let buffer = ByteBuffer(string: "1234") | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the AsyncHTTPClient open source project | ||
// | ||
// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors | ||
// Licensed under Apache License v2.0 | ||
// | ||
// See LICENSE.txt for license information | ||
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import AsyncHTTPClient | ||
import NIO | ||
import NIOHTTP1 | ||
import NIOSOCKS | ||
import XCTest | ||
|
||
struct MockSOCKSError: Error, Hashable { | ||
var description: String | ||
} | ||
|
||
class MockSOCKSServer { | ||
let channel: Channel | ||
|
||
public init(expectedURL: String, expectedResponse: String, misbehave: Bool = false, file: String = #file, line: UInt = #line) throws { | ||
Davidde94 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) | ||
let bootstrap = ServerBootstrap(group: elg) | ||
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) | ||
.childChannelInitializer { channel in | ||
let handshakeHandler = SOCKSServerHandshakeHandler() | ||
return channel.pipeline.addHandlers([ | ||
handshakeHandler, | ||
SOCKSTestHandler(handshakeHandler: handshakeHandler, misbehave: misbehave), | ||
SOCKSTestHTTPClient(expectedURL: expectedURL, expectedResponse: expectedResponse, file: file, line: line), | ||
]) | ||
} | ||
self.channel = try bootstrap.bind(host: "127.0.0.1", port: 1080).wait() | ||
Davidde94 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
func shutdown() throws { | ||
try self.channel.close().wait() | ||
} | ||
} | ||
|
||
class SOCKSTestHTTPClient: ChannelInboundHandler { | ||
Davidde94 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
typealias InboundIn = HTTPServerRequestPart | ||
typealias OutboundOut = HTTPServerResponsePart | ||
|
||
let expectedURL: String | ||
let expectedResponse: String | ||
let file: String | ||
let line: UInt | ||
var requestCount = 0 | ||
|
||
init(expectedURL: String, expectedResponse: String, file: String, line: UInt) { | ||
self.expectedURL = expectedURL | ||
self.expectedResponse = expectedResponse | ||
self.file = file | ||
self.line = line | ||
} | ||
|
||
func channelRead(context: ChannelHandlerContext, data: NIOAny) { | ||
let message = self.unwrapInboundIn(data) | ||
switch message { | ||
case .head(let head): | ||
guard self.requestCount == 0 else { | ||
return | ||
} | ||
XCTAssertEqual(head.uri, self.expectedURL) | ||
Lukasa marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.requestCount += 1 | ||
case .body: | ||
break | ||
case .end: | ||
context.write(self.wrapOutboundOut(.head(.init(version: .http1_1, status: .ok))), promise: nil) | ||
context.write(self.wrapOutboundOut(.body(.byteBuffer(context.channel.allocator.buffer(string: self.expectedResponse)))), promise: nil) | ||
context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil) | ||
} | ||
} | ||
|
||
func errorCaught(context: ChannelHandlerContext, error: Error) { | ||
context.fireErrorCaught(error) | ||
context.close(promise: nil) | ||
} | ||
} | ||
|
||
class SOCKSTestHandler: ChannelInboundHandler, RemovableChannelHandler { | ||
typealias InboundIn = ClientMessage | ||
|
||
let handshakeHandler: SOCKSServerHandshakeHandler | ||
let misbehave: Bool | ||
|
||
init(handshakeHandler: SOCKSServerHandshakeHandler, misbehave: Bool) { | ||
self.handshakeHandler = handshakeHandler | ||
self.misbehave = misbehave | ||
} | ||
|
||
func channelRead(context: ChannelHandlerContext, data: NIOAny) { | ||
guard context.channel.isActive else { | ||
return | ||
} | ||
|
||
let message = self.unwrapInboundIn(data) | ||
switch message { | ||
case .greeting: | ||
context.writeAndFlush(.init( | ||
Davidde94 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
ServerMessage.selectedAuthenticationMethod(.init(method: .noneRequired))), promise: nil) | ||
case .authenticationData: | ||
context.fireErrorCaught(MockSOCKSError(description: "Received authentication data but didn't receive any.")) | ||
case .request(let request): | ||
guard !self.misbehave else { | ||
context.writeAndFlush( | ||
.init(ServerMessage.authenticationData(context.channel.allocator.buffer(string: "bad server!"), complete: true)), promise: nil | ||
) | ||
return | ||
} | ||
context.writeAndFlush(.init( | ||
ServerMessage.response(.init(reply: .succeeded, boundAddress: request.addressType))), promise: nil) | ||
context.channel.pipeline.addHandlers([ | ||
ByteToMessageHandler(HTTPRequestDecoder()), | ||
HTTPResponseEncoder(), | ||
], position: .after(self)).whenSuccess { | ||
context.channel.pipeline.removeHandler(self, promise: nil) | ||
context.channel.pipeline.removeHandler(self.handshakeHandler, promise: nil) | ||
} | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.