feat: add proxy utility (#288)

Signed-off-by: Harry Li <harrymingh@gmail.com>
Co-authored-by: Dmitry Kovba <dkovba@apple.com>
This commit is contained in:
Harry Li
2025-09-12 11:23:41 -07:00
committed by GitHub
co-authored by Dmitry Kovba
parent 31bfef4a8e
commit 5fbae0e590
10 changed files with 182 additions and 16 deletions
@@ -45,7 +45,6 @@ public struct ContainerManager: Sendable {
@available(macOS 26.0, *)
public struct VmnetNetwork: Network {
private var allocator: Allocator
// `reference` isn't used concurrently.
nonisolated(unsafe) private let reference: vmnet_network_ref
/// The IPv4 subnet of this network.
@@ -95,7 +94,6 @@ public struct ContainerManager: Sendable {
public let gateway: String?
public let macAddress: String?
// `reference` isn't used concurrently.
nonisolated(unsafe) private let reference: vmnet_network_ref
public init(
@@ -480,4 +478,9 @@ extension CIDRAddress {
}
}
@available(macOS 26.0, *)
private struct SendableReference: Sendable {
nonisolated(unsafe) private let reference: vmnet_network_ref
}
#endif
@@ -24,14 +24,14 @@ import Crypto
import Foundation
extension ImageStore {
public struct ExportOperation: Sendable {
internal struct ExportOperation {
let name: String
let tag: String
let contentStore: ContentStore
let client: ContentClient
let progress: ProgressHandler?
public init(name: String, tag: String, contentStore: ContentStore, client: ContentClient, progress: ProgressHandler? = nil) {
init(name: String, tag: String, contentStore: ContentStore, client: ContentClient, progress: ProgressHandler? = nil) {
self.contentStore = contentStore
self.client = client
self.progress = progress
@@ -40,7 +40,7 @@ extension ImageStore {
}
@discardableResult
public func export(index: Descriptor, platforms: (Platform) -> Bool) async throws -> Descriptor {
internal func export(index: Descriptor, platforms: (Platform) -> Bool) async throws -> Descriptor {
var pushQueue: [[Descriptor]] = []
var current: [Descriptor] = [index]
while !current.isEmpty {
@@ -22,7 +22,7 @@ import ContainerizationOCI
import Foundation
extension ImageStore {
public struct ImportOperation: Sendable {
internal struct ImportOperation {
static let decoder = JSONDecoder()
let client: ContentClient
@@ -31,7 +31,7 @@ extension ImageStore {
let progress: ProgressHandler?
let name: String
public init(name: String, contentStore: ContentStore, client: ContentClient, ingestDir: URL, progress: ProgressHandler? = nil) {
init(name: String, contentStore: ContentStore, client: ContentClient, ingestDir: URL, progress: ProgressHandler? = nil) {
self.client = client
self.ingestDir = ingestDir
self.contentStore = contentStore
@@ -40,7 +40,7 @@ extension ImageStore {
}
/// 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.
public func `import`(root: Descriptor, matcher: (ContainerizationOCI.Platform) -> Bool) async throws -> Descriptor {
internal func `import`(root: Descriptor, matcher: (ContainerizationOCI.Platform) -> Bool) async throws -> Descriptor {
var toProcess = [root]
while !toProcess.isEmpty {
// Count the total number of blobs and their size
@@ -123,14 +123,14 @@ extension ImageStore {
for _ in 0..<8 {
if let desc = iterator.next() {
group.addTask {
try await self.fetch(desc)
try await fetch(desc)
}
}
}
for try await _ in group {
if let desc = iterator.next() {
group.addTask {
try await self.fetch(desc)
try await fetch(desc)
}
}
}
@@ -31,7 +31,6 @@ public final class NATNetworkInterface: Interface, Sendable {
public let macAddress: String?
@available(macOS 26, *)
// `reference` isn't used concurrently.
public nonisolated(unsafe) let reference: vmnet_network_ref!
@available(macOS 26, *)
@@ -267,7 +267,6 @@ extension SocketRelay {
)
}
// `buf1` isn't used concurrently.
nonisolated(unsafe) let buf1 = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: Int(getpagesize()))
connSource.setEventHandler {
Self.fdCopyHandler(
@@ -279,7 +278,6 @@ extension SocketRelay {
}
nonisolated(unsafe) let buf2 = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: Int(getpagesize()))
// `buf2` isn't used concurrently.
vsockConnectionSource.setEventHandler {
Self.fdCopyHandler(
buffer: buf2,
@@ -68,7 +68,6 @@ struct VZVirtualMachineInstance: VirtualMachineInstance, Sendable {
}
}
// `vm` isn't used concurrently.
private nonisolated(unsafe) let vm: VZVirtualMachine
private let queue: DispatchQueue
private let group: MultiThreadedEventLoopGroup
@@ -0,0 +1,61 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Foundation
/// A small utility to resolve proxy settings (HTTP(S)_PROXY / NO_PROXY).
public enum ProxyUtils {
/// Resolves the proxy URL for a given host based on environment variables.
///
/// - Parameters:
/// - host: The target hostname (without scheme).
/// - env: Optional environment dictionary; defaults to process environment.
/// - Returns: The proxy URL to use, or `nil` for direct connection.
public static func proxy(for host: String, env: [String: String]? = nil) -> URL? {
let env = env ?? ProcessInfo.processInfo.environment
// Case-insensitive lookup for both upper/lower keys
let httpProxy = env["HTTP_PROXY"] ?? env["http_proxy"]
let httpsProxy = env["HTTPS_PROXY"] ?? env["https_proxy"]
let noProxy = env["NO_PROXY"] ?? env["no_proxy"]
// If NO_PROXY matches skip proxy
if let noProxy, shouldBypassProxy(host: host, noProxy: noProxy) {
return nil
}
// Prefer HTTPS proxy if set, otherwise fall back to HTTP proxy
let proxyStr = httpsProxy ?? httpProxy
guard let proxyStr, let url = URL(string: proxyStr) else {
return nil
}
return url
}
/// Check if a host should bypass proxy according to NO_PROXY.
/// - Example: NO_PROXY=".example.com,localhost,127.0.0.1"
private static func shouldBypassProxy(host: String, noProxy: String) -> Bool {
let entries = noProxy.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }
for entry in entries {
if entry.isEmpty { continue }
if entry == "*" { return true }
if host == entry { return true }
if entry.hasPrefix(".") && host.hasSuffix(entry) { return true }
}
return false
}
}
@@ -53,7 +53,6 @@ public final class AsyncSignalHandler: Sendable {
struct State: Sendable {
var conts: [AsyncStream<Int32>.Continuation] = []
// `sources` isn't used concurrently.
nonisolated(unsafe) var sources: [any DispatchSourceSignal] = []
}
-1
View File
@@ -62,7 +62,6 @@ extension IntegrationSuite {
}
final class BufferWriter: Writer {
// `data` isn't used concurrently.
nonisolated(unsafe) var data = Data()
func write(_ data: Data) throws {
@@ -0,0 +1,108 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Foundation
import Testing
@testable import ContainerizationExtras
struct ProxyUtilsTests {
@Test("HTTP proxy resolution")
func testHttpProxy() {
let env = ["http_proxy": "http://proxy.local:8080"]
let proxy = ProxyUtils.proxy(for: "example.com", env: env)
#expect(proxy?.absoluteString == "http://proxy.local:8080")
}
@Test("HTTPS proxy resolution")
func testHttpsProxy() {
let env = ["https_proxy": "https://secureproxy.local:8443"]
let proxy = ProxyUtils.proxy(for: "secure.com", env: env)
#expect(proxy?.absoluteString == "https://secureproxy.local:8443")
}
@Test("NO_PROXY exact match")
func testNoProxyExactMatch() {
let env = [
"http_proxy": "http://proxy.local:8080",
"NO_PROXY": "example.com",
]
let proxy = ProxyUtils.proxy(for: "example.com", env: env)
#expect(proxy == nil)
}
@Test("Uppercase HTTP_PROXY is respected")
func testUppercaseHttpProxy() {
let env = ["HTTP_PROXY": "http://upper.local:8081"]
let proxy = ProxyUtils.proxy(for: "upper.com", env: env)
#expect(proxy?.absoluteString == "http://upper.local:8081")
}
@Test("Lowercase no_proxy is respected")
func testLowercaseNoProxy() {
let env = [
"http_proxy": "http://proxy.local:8080",
"no_proxy": "lower.com",
]
let proxy = ProxyUtils.proxy(for: "lower.com", env: env)
#expect(proxy == nil)
}
@Test("HTTPS proxy has higher priority than HTTP proxy")
func testHttpsPreferredOverHttp() {
let env = [
"http_proxy": "http://proxy.local:8080",
"https_proxy": "https://secureproxy.local:8443",
]
let proxy = ProxyUtils.proxy(for: "secure.com", env: env)
#expect(proxy?.absoluteString == "https://secureproxy.local:8443")
}
@Test("Uppercase HTTP_PROXY overrides lowercase http_proxy")
func testUppercaseOverridesLowercaseHttp() {
let env = [
"http_proxy": "http://lower.local:8080",
"HTTP_PROXY": "http://upper.local:8081",
]
let proxy = ProxyUtils.proxy(for: "example.com", env: env)
#expect(proxy?.absoluteString == "http://upper.local:8081")
}
@Test("Uppercase HTTPS_PROXY overrides lowercase https_proxy")
func testUppercaseOverridesLowercaseHttps() {
let env = [
"https_proxy": "https://lower.local:8443",
"HTTPS_PROXY": "https://upper.local:8444",
]
let proxy = ProxyUtils.proxy(for: "secure.com", env: env)
#expect(proxy?.absoluteString == "https://upper.local:8444")
}
@Test("Uppercase NO_PROXY overrides lowercase no_proxy")
func testUppercaseOverridesLowercaseNoProxy() {
let env = [
"http_proxy": "http://proxy.local:8080",
"no_proxy": "foo.com",
"NO_PROXY": "bar.com",
]
let proxyFoo = ProxyUtils.proxy(for: "foo.com", env: env)
let proxyBar = ProxyUtils.proxy(for: "bar.com", env: env)
#expect(proxyFoo != nil)
#expect(proxyBar == nil)
}
}