Skip to content

Commit e47325d

Browse files
authored
Add ability to read http_proxy from environment (#38)
* Add ability to read http_proxy from environment * Add WebSocketProxySettings.environment * Pass URL to getProxyEnvironmentValues * Add support for no_proxy * Fix swift 6.0
1 parent 248dbab commit e47325d

File tree

4 files changed

+166
-11
lines changed

4 files changed

+166
-11
lines changed

Sources/WSClient/Client/ClientConnection.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public struct ClientConnection<ClientChannel: ClientConnectionChannel>: Sendable
7171
_ clientChannel: ClientChannel,
7272
address: Address,
7373
transportServicesTLSOptions: TSTLSOptions,
74-
eventLoopGroup: EventLoopGroup = MultiThreadedEventLoopGroup.singleton,
74+
eventLoopGroup: any EventLoopGroup = MultiThreadedEventLoopGroup.singleton,
7575
logger: Logger
7676
) throws {
7777
self.clientChannel = clientChannel

Sources/WSClient/WebSocketClient.swift

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,25 @@ public struct WebSocketClient {
153153
guard var host = url.host else { throw WebSocketClientError.invalidURL }
154154
let requiresTLS = self.url.scheme == .wss || self.url.scheme == .https
155155
var port = self.url.port ?? (requiresTLS ? 443 : 80)
156-
if let proxySettings = self.proxySettings {
157-
host = proxySettings.host
158-
port = proxySettings.port
156+
var proxySettings = self.proxySettings
157+
if let validProxySettings = proxySettings {
158+
switch validProxySettings.address {
159+
case .hostname(let proxyHost, let proxyPort):
160+
self.logger.debug("Using proxy: \(proxyHost):\(proxyPort)")
161+
host = proxyHost
162+
port = proxyPort
163+
case .environment:
164+
if let (proxyHost, proxyPort) = WebSocketProxySettings.getProxyEnvironmentValues(for: self.url) {
165+
self.logger.debug("Using proxy: \(proxyHost):\(proxyPort)")
166+
proxySettings = .init(host: proxyHost, port: proxyPort, type: .http(), timeout: validProxySettings.timeout)
167+
host = proxyHost
168+
port = proxyPort
169+
} else {
170+
proxySettings = nil
171+
}
172+
}
159173
}
174+
160175
var tlsConfiguration: TLSConfiguration? = nil
161176
if requiresTLS {
162177
switch self.tlsConfiguration {
@@ -190,7 +205,7 @@ public struct WebSocketClient {
190205
url: url,
191206
configuration: self.configuration,
192207
tlsConfiguration: tlsConfiguration,
193-
proxySettings: self.proxySettings
208+
proxySettings: proxySettings
194209
),
195210
address: .hostname(host, port: port),
196211
eventLoopGroup: self.eventLoopGroup,

Sources/WSClient/WebSocketClientConfiguration.swift

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ import NIOCore
1111
import NIOSSL
1212
import WSCore
1313

14+
#if canImport(FoundationEssentials)
15+
import FoundationEssentials
16+
#else
17+
import Foundation
18+
#endif
19+
1420
/// Configuration for a client connecting to a WebSocket
1521
public struct WebSocketClientConfiguration: Sendable {
1622
/// Max websocket frame size that can be sent/received
@@ -70,10 +76,12 @@ public struct WebSocketProxySettings: Sendable {
7076
/// HTTP proxy
7177
public static func http(connectHeaders: HTTPFields = [:]) -> ProxyType { .init(value: .http(connectHeaders: connectHeaders)) }
7278
}
73-
/// Proxy endpoint hostname
74-
public var host: String
75-
/// Proxy port
76-
public var port: Int
79+
public enum ProxyAddress: Sendable {
80+
case hostname(String, port: Int)
81+
case environment
82+
}
83+
/// Network address
84+
public var address: ProxyAddress
7785
/// Proxy type
7886
public var type: ProxyType
7987
/// Timeout for CONNECT response
@@ -91,9 +99,61 @@ public struct WebSocketProxySettings: Sendable {
9199
type: ProxyType,
92100
timeout: Duration = .seconds(30)
93101
) {
94-
self.host = host
95-
self.port = port
102+
self.address = .hostname(host, port: port)
96103
self.type = type
97104
self.timeout = timeout
98105
}
106+
107+
/// Return proxy settings that will use environment variables for settings
108+
/// - Parameter timeout: Timeout for CONNECT request
109+
/// - Returns: proxy settings
110+
public static func environment(timeout: Duration = .seconds(30)) -> WebSocketProxySettings {
111+
.init(address: .environment, type: .http(), timeout: timeout)
112+
}
113+
114+
/// Internal init
115+
internal init(address: ProxyAddress, type: ProxyType, timeout: Duration) {
116+
self.address = address
117+
self.type = type
118+
self.timeout = timeout
119+
}
120+
121+
/// Get proxy settings from environment
122+
static func getProxyEnvironmentValues(for url: URI) -> (host: String, port: Int)? {
123+
let requiresTLS = url.scheme == .wss || url.scheme == .https
124+
let environment = ProcessInfo.processInfo.environment
125+
let proxy =
126+
if !requiresTLS {
127+
environment["http_proxy"]
128+
} else {
129+
environment["https_proxy"] ?? environment["HTTPS_PROXY"] ?? environment["http_proxy"]
130+
}
131+
guard let proxy else { return nil }
132+
// Check if URL matches domain in no_proxy environment variable
133+
if let noProxy = environment["no_proxy"] ?? environment["NO_PROXY"] {
134+
let filters = noProxy.split(separator: ",")
135+
for filter in filters {
136+
var filter = filter[...]
137+
// drop whitespace
138+
filter = filter.trimmingPrefix { $0.isWhitespace }
139+
if let lastIndex = filter.lastIndex(of: " ") {
140+
filter = filter[..<lastIndex]
141+
}
142+
// Drop leading dot so `.github.com` will match `github.com`
143+
if filter.first == "." {
144+
filter = filter.dropFirst()
145+
}
146+
if filter == "*" {
147+
return nil
148+
} else if url.host?.hasSuffix(filter) == true {
149+
return nil
150+
}
151+
}
152+
}
153+
let proxyURL = URI(proxy)
154+
guard proxyURL.scheme == .http else { return nil }
155+
guard let host = proxyURL.host else { return nil }
156+
guard let port = proxyURL.port else { return nil }
157+
return (host: host, port: port)
158+
}
99159
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//
2+
// This source file is part of the Hummingbird server framework project
3+
// Copyright (c) the Hummingbird authors
4+
//
5+
// See LICENSE.txt for license information
6+
// SPDX-License-Identifier: Apache-2.0
7+
//
8+
9+
import Foundation
10+
import Testing
11+
12+
@testable import WSClient
13+
14+
@Suite("ProxySettings Tests", .serialized)
15+
struct ProxySettingsTests {
16+
@Test func testHTTPProxyEnvVar() async throws {
17+
setenv("http_proxy", "http://test.com:8888", 1)
18+
defer { unsetenv("http_proxy") }
19+
let values = WebSocketProxySettings.getProxyEnvironmentValues(for: "ws://echo.websocket.org/")
20+
#expect(values?.host == "test.com")
21+
#expect(values?.port == 8888)
22+
}
23+
24+
@Test func testHTTPSProxyEnvVar() async throws {
25+
setenv("http_proxy", "http://test.com:8888", 1)
26+
defer { unsetenv("http_proxy") }
27+
setenv("https_proxy", "http://test2.com:8888", 1)
28+
defer { unsetenv("https_proxy") }
29+
let values = WebSocketProxySettings.getProxyEnvironmentValues(for: "wss://echo.websocket.org/")
30+
#expect(values?.host == "test2.com")
31+
#expect(values?.port == 8888)
32+
}
33+
34+
@Test func testNoProxyWildcard() async throws {
35+
setenv("http_proxy", "http://test.com:8888", 1)
36+
defer { unsetenv("http_proxy") }
37+
setenv("no_proxy", "*", 1)
38+
defer { unsetenv("no_proxy") }
39+
let values = WebSocketProxySettings.getProxyEnvironmentValues(for: "ws://echo.websocket.org/")
40+
#expect(values == nil)
41+
}
42+
43+
@Test func testNoProxyMatch() async throws {
44+
setenv("http_proxy", "http://test.com:8888", 1)
45+
defer { unsetenv("http_proxy") }
46+
setenv("no_proxy", "websocket.org", 1)
47+
defer { unsetenv("no_proxy") }
48+
let values = WebSocketProxySettings.getProxyEnvironmentValues(for: "ws://echo.websocket.org/")
49+
#expect(values == nil)
50+
}
51+
52+
@Test func testNoProxyMatchWithLeadingDot() async throws {
53+
setenv("http_proxy", "http://test.com:8888", 1)
54+
defer { unsetenv("http_proxy") }
55+
setenv("no_proxy", ".websocket.org", 1)
56+
defer { unsetenv("no_proxy") }
57+
let values = WebSocketProxySettings.getProxyEnvironmentValues(for: "ws://websocket.org/")
58+
#expect(values == nil)
59+
}
60+
61+
@Test func testNoProxyMultipleDomains() async throws {
62+
setenv("http_proxy", "http://test.com:8888", 1)
63+
defer { unsetenv("http_proxy") }
64+
setenv("no_proxy", "ws.org,websocket.org", 1)
65+
defer { unsetenv("no_proxy") }
66+
let values = WebSocketProxySettings.getProxyEnvironmentValues(for: "ws://echo.websocket.org/")
67+
#expect(values == nil)
68+
}
69+
70+
@Test func testNoProxyMultipleDomainsWithWhitespace() async throws {
71+
setenv("http_proxy", "http://test.com:8888", 1)
72+
defer { unsetenv("http_proxy") }
73+
setenv("no_proxy", "ws.org , websocket.org", 1)
74+
defer { unsetenv("no_proxy") }
75+
let values = WebSocketProxySettings.getProxyEnvironmentValues(for: "ws://echo.websocket.org/")
76+
#expect(values == nil)
77+
let values2 = WebSocketProxySettings.getProxyEnvironmentValues(for: "ws://echo.ws.org/")
78+
#expect(values2 == nil)
79+
}
80+
}

0 commit comments

Comments
 (0)