Skip to content

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

Merged
merged 27 commits into from
Jun 18, 2021
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5a49c2c
Basic socks functionality
Davidde94 Jun 16, 2021
b45f4d9
Remove auth field
Davidde94 Jun 16, 2021
8e1232a
Remove test addresses
Davidde94 Jun 16, 2021
974677f
Working tests
Davidde94 Jun 16, 2021
122f650
Update packages
Davidde94 Jun 16, 2021
f1598be
Merge branch 'main' into de/socks-client
Davidde94 Jun 16, 2021
481e1b1
Rename to MockSOCKSServer
Davidde94 Jun 17, 2021
064edcb
Code review cleanup
Davidde94 Jun 17, 2021
210c4d7
Cleanup public authorization
Davidde94 Jun 17, 2021
18c7103
Throw error on auth data
Davidde94 Jun 17, 2021
323eefd
Add missing socks server test
Davidde94 Jun 17, 2021
c273847
Fabians test cleanup
Davidde94 Jun 17, 2021
3c0e120
Remove redundant auth complete write
Davidde94 Jun 17, 2021
ba629a4
Add test for speaking to wrong server type
Davidde94 Jun 17, 2021
e4c1b52
Add misbehaving server test
Davidde94 Jun 17, 2021
d718299
Guard against multiple requests
Davidde94 Jun 17, 2021
eee59e3
Soundness
Davidde94 Jun 17, 2021
112d5b1
Soundness
Davidde94 Jun 17, 2021
976fe2e
Update Sources/AsyncHTTPClient/HTTPClientProxyHandler.swift
Davidde94 Jun 17, 2021
ea04cfa
Support bogus addresses
Davidde94 Jun 17, 2021
ddc009a
Merge branch 'de/socks-client' of github.com:Davidde94/async-http-cli…
Davidde94 Jun 17, 2021
221420f
Swift format
Davidde94 Jun 17, 2021
91a1636
Soundness
Davidde94 Jun 17, 2021
15e5334
Address PR comments
Davidde94 Jun 18, 2021
c61184e
Cory PR nits
Davidde94 Jun 18, 2021
97c6b7f
Soundness
Davidde94 Jun 18, 2021
dd405af
Fabian nit
Davidde94 Jun 18, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,22 @@ let package = Package(
.library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.27.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.29.0"),
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.13.0"),
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.3.0"),
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.9.0"),
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.5.1"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.4.0"),
],
targets: [
.target(
name: "AsyncHTTPClient",
dependencies: ["NIO", "NIOHTTP1", "NIOSSL", "NIOConcurrencyHelpers", "NIOHTTPCompression",
"NIOFoundationCompat", "NIOTransportServices", "Logging"]
"NIOFoundationCompat", "NIOTransportServices", "Logging", "NIOSOCKS"]
),
.testTarget(
name: "AsyncHTTPClientTests",
dependencies: ["NIO", "NIOConcurrencyHelpers", "NIOSSL", "AsyncHTTPClient", "NIOFoundationCompat",
"NIOTestUtils", "Logging"]
"NIOTestUtils", "Logging", "NIOSOCKS"]
),
]
)
8 changes: 8 additions & 0 deletions Sources/AsyncHTTPClient/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import NIOHTTPCompression
import NIOSSL
import NIOTLS
import NIOTransportServices
import NIOSOCKS

extension Logger {
private func requestInfo(_ request: HTTPClient.Request) -> Logger.Metadata.Value {
Expand Down Expand Up @@ -899,6 +900,13 @@ extension ChannelPipeline {
try sync.addHandler(decoder)
try sync.addHandler(handler)
}

func syncAddSOCKSProxyHandler(host: String, port: Int) throws {
let address = try SocketAddress(ipAddress: host, port: port)
let handler = SOCKSClientHandler(targetAddress: .address(address))
let sync = self.syncOperations
try sync.addHandler(handler)
}

func syncAddLateSSLHandlerIfNeeded(for key: ConnectionPool.Key,
sslContext: NIOSSLContext,
Expand Down
17 changes: 15 additions & 2 deletions Sources/AsyncHTTPClient/HTTPClientProxyHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import NIO
import NIOHTTP1
import NIOSOCKS

public extension HTTPClient.Configuration {
/// Proxy server configuration
Expand All @@ -27,20 +28,28 @@ public extension HTTPClient.Configuration {
/// TLS will be established _after_ successful proxy, between your client
/// and the destination server.
struct Proxy {

enum ProxyType: Hashable {
case http
case socks
}

/// Specifies Proxy server host.
public var host: String
/// Specifies Proxy server port.
public var port: Int
/// Specifies Proxy server authorization.
public var authorization: HTTPClient.Authorization?

var type: ProxyType

/// Create proxy.
///
/// - parameters:
/// - host: proxy server host.
/// - port: proxy server port.
public static func server(host: String, port: Int) -> Proxy {
return .init(host: host, port: port, authorization: nil)
return .init(host: host, port: port, authorization: nil, type: .http)
}

/// Create proxy.
Expand All @@ -50,7 +59,11 @@ public extension HTTPClient.Configuration {
/// - port: proxy server port.
/// - authorization: proxy server authorization.
public static func server(host: String, port: Int, authorization: HTTPClient.Authorization? = nil) -> Proxy {
return .init(host: host, port: port, authorization: authorization)
return .init(host: host, port: port, authorization: authorization, type: .http)
}

public static func socksServer(host: String, port: Int = 1080) -> Proxy {
return .init(host: host, port: port, authorization: nil, type: .socks)
}
}
}
Expand Down
11 changes: 8 additions & 3 deletions Sources/AsyncHTTPClient/Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,14 @@ extension NIOClientTCPBootstrap {
return bootstrap.channelInitializer { channel in
do {
if let proxy = configuration.proxy {
try channel.pipeline.syncAddProxyHandler(host: host,
port: port,
authorization: proxy.authorization)
switch proxy.type {
case .http:
try channel.pipeline.syncAddProxyHandler(host: host,
port: port,
authorization: proxy.authorization)
case .socks:
try channel.pipeline.syncAddSOCKSProxyHandler(host: host, port: port)
}
} else if requiresTLS {
// We only add the handshake verifier if we need TLS and we're not going through a proxy.
// If we're going through a proxy we add it later (outside of this method).
Expand Down
18 changes: 18 additions & 0 deletions Tests/AsyncHTTPClientTests/HTTPClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import NIOSSL
import NIOTestUtils
import NIOTransportServices
import XCTest
import NIOSOCKS

class HTTPClientTests: XCTestCase {
typealias Request = HTTPClient.Request
Expand Down Expand Up @@ -708,6 +709,23 @@ class HTTPClientTests: XCTestCase {
}
}
}

func testProxySOCKS() throws {
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")))

do {
let response = try localClient.get(url: "http://127.0.0.1/socks/test").wait()
XCTAssertEqual(.ok, response.status)
XCTAssertEqual(ByteBuffer(string: "it works!"), response.body)
} catch {
XCTFail("\(error)")
}

XCTAssertNoThrow(try localClient.syncShutdown())
XCTAssertNoThrow(try socksBin.shutdown())
}

func testUploadStreaming() throws {
let body: HTTPClient.Body = .stream(length: 8) { writer in
Expand Down
111 changes: 111 additions & 0 deletions Tests/AsyncHTTPClientTests/SOCKSTestUtils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2020 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
//
//===----------------------------------------------------------------------===//

@testable import AsyncHTTPClient
import NIO
import NIOHTTP1
import NIOSOCKS
import XCTest

class MockSocksServer {

let channel: Channel

public init(expectedURL: String, expectedResponse: String, file: String = (#file), line: UInt = #line) throws {
let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let bootstrap = ServerBootstrap.init(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),
SOCKSTestHTTPClient(expectedURL: expectedURL, expectedResponse: expectedResponse, file: file, line: line)
])
}
self.channel = try bootstrap.bind(host: "127.0.0.1", port: 1080).wait()
}

func shutdown() throws {
try self.channel.close().wait()
}

}

class SOCKSTestHTTPClient: ChannelInboundHandler {

typealias InboundIn = HTTPServerRequestPart
typealias OutboundOut = HTTPServerResponsePart

let expectedURL: String
let expectedResponse: String
let file: String
let line: UInt

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):
XCTAssertEqual(head.uri, self.expectedURL)
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)
}
}
}

class SOCKSTestHandler: ChannelInboundHandler, RemovableChannelHandler {

typealias InboundIn = ClientMessage

let handshakeHandler: SOCKSServerHandshakeHandler

init(handshakeHandler: SOCKSServerHandshakeHandler) {
self.handshakeHandler = handshakeHandler
}

func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let message = self.unwrapInboundIn(data)
switch message {
case .greeting:
context.writeAndFlush(.init(
ServerMessage.selectedAuthenticationMethod(.init(method: .noneRequired))), promise: nil)
context.writeAndFlush(.init(
ServerMessage.authenticationData(context.channel.allocator.buffer(capacity: 0), complete: true)), promise: nil)
case .authenticationData:
break
case .request(let request):
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)
}
}
}

}