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 24 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"]
),
]
)
10 changes: 9 additions & 1 deletion Sources/AsyncHTTPClient/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import NIO
import NIOConcurrencyHelpers
import NIOHTTP1
import NIOHTTPCompression
import NIOSOCKS
import NIOSSL
import NIOTLS
import NIOTransportServices
Expand Down Expand Up @@ -883,7 +884,7 @@ extension HTTPClient.Configuration {
}

extension ChannelPipeline {
func syncAddProxyHandler(host: String, port: Int, authorization: HTTPClient.Authorization?) throws {
func syncAddHTTPProxyHandler(host: String, port: Int, authorization: HTTPClient.Authorization?) throws {
let encoder = HTTPRequestEncoder()
let decoder = ByteToMessageHandler(HTTPResponseDecoder(leftOverBytesStrategy: .forwardBytes))
let handler = HTTPClientProxyHandler(host: host, port: port, authorization: authorization) { channel in
Expand All @@ -900,6 +901,13 @@ extension ChannelPipeline {
try sync.addHandler(handler)
}

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

func syncAddLateSSLHandlerIfNeeded(for key: ConnectionPool.Key,
sslContext: NIOSSLContext,
handshakePromise: EventLoopPromise<Void>) {
Expand Down
44 changes: 37 additions & 7 deletions Sources/AsyncHTTPClient/HTTPClientProxyHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@

import NIO
import NIOHTTP1
import NIOSOCKS

public extension HTTPClient.Configuration {
extension HTTPClient.Configuration {
/// Proxy server configuration
/// Specifies the remote address of an HTTP proxy.
///
Expand All @@ -26,31 +27,60 @@ public extension HTTPClient.Configuration {
/// If a `TLSConfiguration` is used in conjunction with `HTTPClient.Configuration.Proxy`,
/// TLS will be established _after_ successful proxy, between your client
/// and the destination server.
struct Proxy {
public struct Proxy {
enum ProxyType: Hashable {
case http(HTTPClient.Authorization?)
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?
public var authorization: HTTPClient.Authorization? {
set {
precondition(self.type == .http(self.authorization), "SOCKS authorization support is not yet implemented.")
self.type = .http(newValue)
}

get {
switch self.type {
case .http(let authorization):
return authorization
case .socks:
return nil
}
}
}

var type: ProxyType

/// Create proxy.
/// Create a HTTP 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, type: .http(nil))
}

/// Create proxy.
/// Create a HTTP proxy.
///
/// - parameters:
/// - host: proxy server host.
/// - 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, type: .http(authorization))
}

/// Create a SOCKSv5 proxy.
/// - parameter host: The SOCKSv5 proxy address.
/// - parameter port: The SOCKSv5 proxy port, defaults to 1080.
/// - returns: A new instance of `Proxy` configured to connect to a `SOCKSv5` server.
public static func socksServer(host: String, port: Int = 1080) -> Proxy {
return .init(host: host, port: port, type: .socks)
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/AsyncHTTPClient/HTTPHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this added?

Copy link
Collaborator

Choose a reason for hiding this comment

The 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)
}
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.syncAddHTTPProxyHandler(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
35 changes: 35 additions & 0 deletions Tests/AsyncHTTPClientTests/HTTPClient+SOCKSTests+XCTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2018-2019 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
//
//===----------------------------------------------------------------------===//
//
// HTTPClient+SOCKSTests+XCTest.swift
//
import XCTest

///
/// NOTE: This file was generated by generate_linux_tests.rb
///
/// Do NOT edit this file directly as it will be regenerated automatically when needed.
///

extension HTTPClientSOCKSTests {
static var allTests: [(String, (HTTPClientSOCKSTests) -> () throws -> Void)] {
return [
("testProxySOCKS", testProxySOCKS),
("testProxySOCKSBogusAddress", testProxySOCKSBogusAddress),
("testProxySOCKSFailureNoServer", testProxySOCKSFailureNoServer),
("testProxySOCKSFailureInvalidServer", testProxySOCKSFailureInvalidServer),
("testProxySOCKSMisbehavingServer", testProxySOCKSMisbehavingServer),
]
}
}
139 changes: 139 additions & 0 deletions Tests/AsyncHTTPClientTests/HTTPClient+SOCKSTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2018-2019 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
//
//===----------------------------------------------------------------------===//

/* NOT @testable */ import AsyncHTTPClient // Tests that need @testable go into HTTPClientInternalTests.swift
import NIO
import NIOSOCKS
import XCTest
import Logging

class HTTPClientSOCKSTests: XCTestCase {

typealias Request = HTTPClient.Request

var clientGroup: EventLoopGroup!
var serverGroup: EventLoopGroup!
var defaultHTTPBin: HTTPBin!
var defaultClient: HTTPClient!
var backgroundLogStore: CollectEverythingLogHandler.LogStore!

var defaultHTTPBinURLPrefix: String {
return "http://localhost:\(self.defaultHTTPBin.port)/"
}

override func setUp() {
XCTAssertNil(self.clientGroup)
XCTAssertNil(self.serverGroup)
XCTAssertNil(self.defaultHTTPBin)
XCTAssertNil(self.defaultClient)
XCTAssertNil(self.backgroundLogStore)

self.clientGroup = getDefaultEventLoopGroup(numberOfThreads: 1)
self.serverGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
self.defaultHTTPBin = HTTPBin()
self.backgroundLogStore = CollectEverythingLogHandler.LogStore()
var backgroundLogger = Logger(label: "\(#function)", factory: { _ in
CollectEverythingLogHandler(logStore: self.backgroundLogStore!)
})
backgroundLogger.logLevel = .trace
self.defaultClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup),
backgroundActivityLogger: backgroundLogger)
}

override func tearDown() {
if let defaultClient = self.defaultClient {
XCTAssertNoThrow(try defaultClient.syncShutdown())
self.defaultClient = nil
}

XCTAssertNotNil(self.defaultHTTPBin)
XCTAssertNoThrow(try self.defaultHTTPBin.shutdown())
self.defaultHTTPBin = nil

XCTAssertNotNil(self.clientGroup)
XCTAssertNoThrow(try self.clientGroup.syncShutdownGracefully())
self.clientGroup = nil

XCTAssertNotNil(self.serverGroup)
XCTAssertNoThrow(try self.serverGroup.syncShutdownGracefully())
self.serverGroup = nil

XCTAssertNotNil(self.backgroundLogStore)
self.backgroundLogStore = nil
}

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")))

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

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)
}

func testProxySOCKSBogusAddress() throws {
let localClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup),
configuration: .init(proxy: .socksServer(host: "127.0..")))

defer {
XCTAssertNoThrow(try localClient.syncShutdown())
}
XCTAssertThrowsError(try localClient.get(url: "http://127.0.0.1/socks/test").wait())
}

// 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())
}

}
1 change: 1 addition & 0 deletions Tests/AsyncHTTPClientTests/HTTPClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import NIOConcurrencyHelpers
import NIOFoundationCompat
import NIOHTTP1
import NIOHTTPCompression
import NIOSOCKS
Copy link
Member

Choose a reason for hiding this comment

The 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
Expand Down
Loading