Skip to content

Commit 5fbae0e

Browse files
mayoootdkovba
andauthored
feat: add proxy utility (#288)
Signed-off-by: Harry Li <[email protected]> Co-authored-by: Dmitry Kovba <[email protected]>
1 parent 31bfef4 commit 5fbae0e

File tree

10 files changed

+182
-16
lines changed

10 files changed

+182
-16
lines changed

Sources/Containerization/ContainerManager.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ public struct ContainerManager: Sendable {
4545
@available(macOS 26.0, *)
4646
public struct VmnetNetwork: Network {
4747
private var allocator: Allocator
48-
// `reference` isn't used concurrently.
4948
nonisolated(unsafe) private let reference: vmnet_network_ref
5049

5150
/// The IPv4 subnet of this network.
@@ -95,7 +94,6 @@ public struct ContainerManager: Sendable {
9594
public let gateway: String?
9695
public let macAddress: String?
9796

98-
// `reference` isn't used concurrently.
9997
nonisolated(unsafe) private let reference: vmnet_network_ref
10098

10199
public init(
@@ -480,4 +478,9 @@ extension CIDRAddress {
480478
}
481479
}
482480

481+
@available(macOS 26.0, *)
482+
private struct SendableReference: Sendable {
483+
nonisolated(unsafe) private let reference: vmnet_network_ref
484+
}
485+
483486
#endif

Sources/Containerization/Image/ImageStore/ImageStore+Export.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ import Crypto
2424
import Foundation
2525

2626
extension ImageStore {
27-
public struct ExportOperation: Sendable {
27+
internal struct ExportOperation {
2828
let name: String
2929
let tag: String
3030
let contentStore: ContentStore
3131
let client: ContentClient
3232
let progress: ProgressHandler?
3333

34-
public init(name: String, tag: String, contentStore: ContentStore, client: ContentClient, progress: ProgressHandler? = nil) {
34+
init(name: String, tag: String, contentStore: ContentStore, client: ContentClient, progress: ProgressHandler? = nil) {
3535
self.contentStore = contentStore
3636
self.client = client
3737
self.progress = progress
@@ -40,7 +40,7 @@ extension ImageStore {
4040
}
4141

4242
@discardableResult
43-
public func export(index: Descriptor, platforms: (Platform) -> Bool) async throws -> Descriptor {
43+
internal func export(index: Descriptor, platforms: (Platform) -> Bool) async throws -> Descriptor {
4444
var pushQueue: [[Descriptor]] = []
4545
var current: [Descriptor] = [index]
4646
while !current.isEmpty {

Sources/Containerization/Image/ImageStore/ImageStore+Import.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import ContainerizationOCI
2222
import Foundation
2323

2424
extension ImageStore {
25-
public struct ImportOperation: Sendable {
25+
internal struct ImportOperation {
2626
static let decoder = JSONDecoder()
2727

2828
let client: ContentClient
@@ -31,7 +31,7 @@ extension ImageStore {
3131
let progress: ProgressHandler?
3232
let name: String
3333

34-
public init(name: String, contentStore: ContentStore, client: ContentClient, ingestDir: URL, progress: ProgressHandler? = nil) {
34+
init(name: String, contentStore: ContentStore, client: ContentClient, ingestDir: URL, progress: ProgressHandler? = nil) {
3535
self.client = client
3636
self.ingestDir = ingestDir
3737
self.contentStore = contentStore
@@ -40,7 +40,7 @@ extension ImageStore {
4040
}
4141

4242
/// Pull the required image layers for the provided descriptor and platform(s) into the given directory using the provided client. Returns a descriptor to the Index manifest.
43-
public func `import`(root: Descriptor, matcher: (ContainerizationOCI.Platform) -> Bool) async throws -> Descriptor {
43+
internal func `import`(root: Descriptor, matcher: (ContainerizationOCI.Platform) -> Bool) async throws -> Descriptor {
4444
var toProcess = [root]
4545
while !toProcess.isEmpty {
4646
// Count the total number of blobs and their size
@@ -123,14 +123,14 @@ extension ImageStore {
123123
for _ in 0..<8 {
124124
if let desc = iterator.next() {
125125
group.addTask {
126-
try await self.fetch(desc)
126+
try await fetch(desc)
127127
}
128128
}
129129
}
130130
for try await _ in group {
131131
if let desc = iterator.next() {
132132
group.addTask {
133-
try await self.fetch(desc)
133+
try await fetch(desc)
134134
}
135135
}
136136
}

Sources/Containerization/NATNetworkInterface.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ public final class NATNetworkInterface: Interface, Sendable {
3131
public let macAddress: String?
3232

3333
@available(macOS 26, *)
34-
// `reference` isn't used concurrently.
3534
public nonisolated(unsafe) let reference: vmnet_network_ref!
3635

3736
@available(macOS 26, *)

Sources/Containerization/UnixSocketRelay.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,6 @@ extension SocketRelay {
267267
)
268268
}
269269

270-
// `buf1` isn't used concurrently.
271270
nonisolated(unsafe) let buf1 = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: Int(getpagesize()))
272271
connSource.setEventHandler {
273272
Self.fdCopyHandler(
@@ -279,7 +278,6 @@ extension SocketRelay {
279278
}
280279

281280
nonisolated(unsafe) let buf2 = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: Int(getpagesize()))
282-
// `buf2` isn't used concurrently.
283281
vsockConnectionSource.setEventHandler {
284282
Self.fdCopyHandler(
285283
buffer: buf2,

Sources/Containerization/VZVirtualMachineInstance.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ struct VZVirtualMachineInstance: VirtualMachineInstance, Sendable {
6868
}
6969
}
7070

71-
// `vm` isn't used concurrently.
7271
private nonisolated(unsafe) let vm: VZVirtualMachine
7372
private let queue: DispatchQueue
7473
private let group: MultiThreadedEventLoopGroup
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import Foundation
18+
19+
/// A small utility to resolve proxy settings (HTTP(S)_PROXY / NO_PROXY).
20+
public enum ProxyUtils {
21+
/// Resolves the proxy URL for a given host based on environment variables.
22+
///
23+
/// - Parameters:
24+
/// - host: The target hostname (without scheme).
25+
/// - env: Optional environment dictionary; defaults to process environment.
26+
/// - Returns: The proxy URL to use, or `nil` for direct connection.
27+
public static func proxy(for host: String, env: [String: String]? = nil) -> URL? {
28+
let env = env ?? ProcessInfo.processInfo.environment
29+
30+
// Case-insensitive lookup for both upper/lower keys
31+
let httpProxy = env["HTTP_PROXY"] ?? env["http_proxy"]
32+
let httpsProxy = env["HTTPS_PROXY"] ?? env["https_proxy"]
33+
34+
let noProxy = env["NO_PROXY"] ?? env["no_proxy"]
35+
36+
// If NO_PROXY matches → skip proxy
37+
if let noProxy, shouldBypassProxy(host: host, noProxy: noProxy) {
38+
return nil
39+
}
40+
41+
// Prefer HTTPS proxy if set, otherwise fall back to HTTP proxy
42+
let proxyStr = httpsProxy ?? httpProxy
43+
guard let proxyStr, let url = URL(string: proxyStr) else {
44+
return nil
45+
}
46+
return url
47+
}
48+
49+
/// Check if a host should bypass proxy according to NO_PROXY.
50+
/// - Example: NO_PROXY=".example.com,localhost,127.0.0.1"
51+
private static func shouldBypassProxy(host: String, noProxy: String) -> Bool {
52+
let entries = noProxy.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }
53+
for entry in entries {
54+
if entry.isEmpty { continue }
55+
if entry == "*" { return true }
56+
if host == entry { return true }
57+
if entry.hasPrefix(".") && host.hasSuffix(entry) { return true }
58+
}
59+
return false
60+
}
61+
}

Sources/ContainerizationOS/AsyncSignalHandler.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ public final class AsyncSignalHandler: Sendable {
5353

5454
struct State: Sendable {
5555
var conts: [AsyncStream<Int32>.Continuation] = []
56-
// `sources` isn't used concurrently.
5756
nonisolated(unsafe) var sources: [any DispatchSourceSignal] = []
5857
}
5958

Sources/Integration/ProcessTests.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ extension IntegrationSuite {
6262
}
6363

6464
final class BufferWriter: Writer {
65-
// `data` isn't used concurrently.
6665
nonisolated(unsafe) var data = Data()
6766

6867
func write(_ data: Data) throws {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import Foundation
18+
import Testing
19+
20+
@testable import ContainerizationExtras
21+
22+
struct ProxyUtilsTests {
23+
24+
@Test("HTTP proxy resolution")
25+
func testHttpProxy() {
26+
let env = ["http_proxy": "http://proxy.local:8080"]
27+
let proxy = ProxyUtils.proxy(for: "example.com", env: env)
28+
#expect(proxy?.absoluteString == "http://proxy.local:8080")
29+
}
30+
31+
@Test("HTTPS proxy resolution")
32+
func testHttpsProxy() {
33+
let env = ["https_proxy": "https://secureproxy.local:8443"]
34+
let proxy = ProxyUtils.proxy(for: "secure.com", env: env)
35+
#expect(proxy?.absoluteString == "https://secureproxy.local:8443")
36+
}
37+
38+
@Test("NO_PROXY exact match")
39+
func testNoProxyExactMatch() {
40+
let env = [
41+
"http_proxy": "http://proxy.local:8080",
42+
"NO_PROXY": "example.com",
43+
]
44+
let proxy = ProxyUtils.proxy(for: "example.com", env: env)
45+
#expect(proxy == nil)
46+
}
47+
48+
@Test("Uppercase HTTP_PROXY is respected")
49+
func testUppercaseHttpProxy() {
50+
let env = ["HTTP_PROXY": "http://upper.local:8081"]
51+
let proxy = ProxyUtils.proxy(for: "upper.com", env: env)
52+
#expect(proxy?.absoluteString == "http://upper.local:8081")
53+
}
54+
55+
@Test("Lowercase no_proxy is respected")
56+
func testLowercaseNoProxy() {
57+
let env = [
58+
"http_proxy": "http://proxy.local:8080",
59+
"no_proxy": "lower.com",
60+
]
61+
let proxy = ProxyUtils.proxy(for: "lower.com", env: env)
62+
#expect(proxy == nil)
63+
}
64+
65+
@Test("HTTPS proxy has higher priority than HTTP proxy")
66+
func testHttpsPreferredOverHttp() {
67+
let env = [
68+
"http_proxy": "http://proxy.local:8080",
69+
"https_proxy": "https://secureproxy.local:8443",
70+
]
71+
let proxy = ProxyUtils.proxy(for: "secure.com", env: env)
72+
#expect(proxy?.absoluteString == "https://secureproxy.local:8443")
73+
}
74+
75+
@Test("Uppercase HTTP_PROXY overrides lowercase http_proxy")
76+
func testUppercaseOverridesLowercaseHttp() {
77+
let env = [
78+
"http_proxy": "http://lower.local:8080",
79+
"HTTP_PROXY": "http://upper.local:8081",
80+
]
81+
let proxy = ProxyUtils.proxy(for: "example.com", env: env)
82+
#expect(proxy?.absoluteString == "http://upper.local:8081")
83+
}
84+
85+
@Test("Uppercase HTTPS_PROXY overrides lowercase https_proxy")
86+
func testUppercaseOverridesLowercaseHttps() {
87+
let env = [
88+
"https_proxy": "https://lower.local:8443",
89+
"HTTPS_PROXY": "https://upper.local:8444",
90+
]
91+
let proxy = ProxyUtils.proxy(for: "secure.com", env: env)
92+
#expect(proxy?.absoluteString == "https://upper.local:8444")
93+
}
94+
95+
@Test("Uppercase NO_PROXY overrides lowercase no_proxy")
96+
func testUppercaseOverridesLowercaseNoProxy() {
97+
let env = [
98+
"http_proxy": "http://proxy.local:8080",
99+
"no_proxy": "foo.com",
100+
"NO_PROXY": "bar.com",
101+
]
102+
let proxyFoo = ProxyUtils.proxy(for: "foo.com", env: env)
103+
let proxyBar = ProxyUtils.proxy(for: "bar.com", env: env)
104+
105+
#expect(proxyFoo != nil)
106+
#expect(proxyBar == nil)
107+
}
108+
}

0 commit comments

Comments
 (0)