Skip to content

Commit 3fc7ced

Browse files
committed
Add support for TLS configuration
1 parent d41064e commit 3fc7ced

File tree

4 files changed

+219
-3
lines changed

4 files changed

+219
-3
lines changed

Sources/ClientRuntime/Networking/Http/NIO/NIOHTTPClient.swift

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,20 @@
66
//
77

88
import AsyncHTTPClient
9+
import Foundation
910
import NIOCore
1011
import NIOPosix
12+
import NIOSSL
1113
import SmithyHTTPAPI
14+
import NIOHTTP1
1215

1316
/// AsyncHTTPClient-based HTTP client implementation that conforms to SmithyHTTPAPI.HTTPClient
1417
/// This implementation is thread-safe and supports concurrent request execution.
1518
public final class NIOHTTPClient: SmithyHTTPAPI.HTTPClient {
1619
private let client: AsyncHTTPClient.HTTPClient
1720
private let config: HttpClientConfiguration
21+
private let tlsConfiguration: NIOHTTPClientTLSOptions?
22+
private let allocator: ByteBufferAllocator
1823

1924
/// Creates a new `NIOHTTPClient`.
2025
///
@@ -25,13 +30,22 @@ public final class NIOHTTPClient: SmithyHTTPAPI.HTTPClient {
2530
httpClientConfiguration: HttpClientConfiguration
2631
) throws {
2732
self.config = httpClientConfiguration
28-
self.client = AsyncHTTPClient.HTTPClient(
29-
configuration: .init() // TODO
30-
)
33+
self.tlsConfiguration = httpClientConfiguration.tlsConfiguration as? NIOHTTPClientTLSOptions
34+
self.allocator = ByteBufferAllocator()
35+
36+
var clientConfig = AsyncHTTPClient.HTTPClient.Configuration()
37+
38+
// Configure TLS if options are provided
39+
if let tlsOptions = tlsConfiguration {
40+
clientConfig.tlsConfiguration = try tlsOptions.makeNIOSSLConfiguration()
41+
}
42+
43+
self.client = AsyncHTTPClient.HTTPClient(configuration: clientConfig)
3144
}
3245

3346
public func send(request: SmithyHTTPAPI.HTTPRequest) async throws -> SmithyHTTPAPI.HTTPResponse {
3447
// TODO
3548
return HTTPResponse()
3649
}
50+
3751
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
import NIOSSL
10+
11+
public struct NIOHTTPClientTLSOptions: TLSConfiguration, Sendable {
12+
13+
/// Optional path to a PEM certificate
14+
public var certificate: String?
15+
16+
/// Optional path to certificate directory
17+
public var certificateDir: String?
18+
19+
/// Optional path to a PEM format private key
20+
public var privateKey: String?
21+
22+
/// Optional path to PKCS #12 certificate, in PEM format
23+
public var pkcs12Path: String?
24+
25+
/// Optional PKCS#12 password
26+
public var pkcs12Password: String?
27+
28+
/// Information is provided to use custom trust store
29+
public var useSelfSignedCertificate: Bool {
30+
return certificate != nil || certificateDir != nil
31+
}
32+
33+
/// Information is provided to use custom key store
34+
public var useProvidedKeystore: Bool {
35+
return (pkcs12Path != nil && pkcs12Password != nil) ||
36+
(certificate != nil && privateKey != nil)
37+
}
38+
39+
public init(
40+
certificate: String? = nil,
41+
certificateDir: String? = nil,
42+
privateKey: String? = nil,
43+
pkcs12Path: String? = nil,
44+
pkcs12Password: String? = nil
45+
) {
46+
self.certificate = certificate
47+
self.certificateDir = certificateDir
48+
self.privateKey = privateKey
49+
self.pkcs12Path = pkcs12Path
50+
self.pkcs12Password = pkcs12Password
51+
}
52+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
import NIOSSL
10+
11+
extension NIOHTTPClientTLSOptions {
12+
13+
func makeNIOSSLConfiguration() throws -> NIOSSL.TLSConfiguration {
14+
var tlsConfig = NIOSSL.TLSConfiguration.makeClientConfiguration()
15+
16+
if useSelfSignedCertificate {
17+
if let certificateDir = certificateDir, let certificate = certificate {
18+
let certificatePath = "\(certificateDir)/\(certificate)"
19+
let certificates = try NIOHTTPClientTLSOptions.loadCertificates(from: certificatePath)
20+
tlsConfig.trustRoots = .certificates(certificates)
21+
} else if let certificate = certificate {
22+
let certificates = try NIOHTTPClientTLSOptions.loadCertificates(from: certificate)
23+
tlsConfig.trustRoots = .certificates(certificates)
24+
}
25+
}
26+
27+
if useProvidedKeystore {
28+
if let pkcs12Path = pkcs12Path, let pkcs12Password = pkcs12Password {
29+
let bundle = try NIOHTTPClientTLSOptions.loadPKCS12Bundle(from: pkcs12Path, password: pkcs12Password)
30+
tlsConfig.certificateChain = bundle.certificateChain.map { .certificate($0) }
31+
tlsConfig.privateKey = .privateKey(bundle.privateKey)
32+
} else if let certificate = certificate, let privateKey = privateKey {
33+
let cert = try NIOHTTPClientTLSOptions.loadCertificate(from: certificate)
34+
let key = try NIOHTTPClientTLSOptions.loadPrivateKey(from: privateKey)
35+
tlsConfig.certificateChain = [.certificate(cert)]
36+
tlsConfig.privateKey = .privateKey(key)
37+
}
38+
}
39+
40+
return tlsConfig
41+
}
42+
}
43+
44+
extension NIOHTTPClientTLSOptions {
45+
46+
static func loadCertificates(from filePath: String) throws -> [NIOSSLCertificate] {
47+
let fileData = try Data(contentsOf: URL(fileURLWithPath: filePath))
48+
return try NIOSSLCertificate.fromPEMBytes(Array(fileData))
49+
}
50+
51+
static func loadCertificate(from filePath: String) throws -> NIOSSLCertificate {
52+
let certificates = try loadCertificates(from: filePath)
53+
guard let certificate = certificates.first else {
54+
throw NIOHTTPClientTLSError.noCertificateFound(filePath)
55+
}
56+
return certificate
57+
}
58+
59+
static func loadPrivateKey(from filePath: String) throws -> NIOSSLPrivateKey {
60+
let fileData = try Data(contentsOf: URL(fileURLWithPath: filePath))
61+
return try NIOSSLPrivateKey(bytes: Array(fileData), format: .pem)
62+
}
63+
64+
static func loadPKCS12Bundle(from filePath: String, password: String) throws -> NIOSSLPKCS12Bundle {
65+
do {
66+
return try NIOSSLPKCS12Bundle(file: filePath, passphrase: password.utf8)
67+
} catch {
68+
throw NIOHTTPClientTLSError.invalidPKCS12(filePath, underlying: error)
69+
}
70+
}
71+
}
72+
73+
public enum NIOHTTPClientTLSError: Error, LocalizedError {
74+
case noCertificateFound(String)
75+
case invalidPKCS12(String, underlying: Error)
76+
77+
public var errorDescription: String? {
78+
switch self {
79+
case .noCertificateFound(let path):
80+
return "No certificate found at path: \(path)"
81+
case .invalidPKCS12(let path, let underlying):
82+
return "Failed to load PKCS#12 file at path: \(path). Error: \(underlying.localizedDescription)"
83+
}
84+
}
85+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
import XCTest
10+
@testable import ClientRuntime
11+
12+
class NIOHTTPClientTLSOptionsTests: XCTestCase {
13+
14+
func test_init_withDefaults() {
15+
let tlsOptions = NIOHTTPClientTLSOptions()
16+
17+
XCTAssertNil(tlsOptions.certificate)
18+
XCTAssertNil(tlsOptions.certificateDir)
19+
XCTAssertNil(tlsOptions.privateKey)
20+
XCTAssertNil(tlsOptions.pkcs12Path)
21+
XCTAssertNil(tlsOptions.pkcs12Password)
22+
XCTAssertFalse(tlsOptions.useSelfSignedCertificate)
23+
XCTAssertFalse(tlsOptions.useProvidedKeystore)
24+
}
25+
26+
func test_init_withCertificate() {
27+
let tlsOptions = NIOHTTPClientTLSOptions(certificate: "/path/to/cert.pem")
28+
29+
XCTAssertEqual(tlsOptions.certificate, "/path/to/cert.pem")
30+
XCTAssertTrue(tlsOptions.useSelfSignedCertificate)
31+
XCTAssertFalse(tlsOptions.useProvidedKeystore)
32+
}
33+
34+
func test_init_withCertificateDir() {
35+
let tlsOptions = NIOHTTPClientTLSOptions(certificateDir: "/path/to/certs/")
36+
37+
XCTAssertEqual(tlsOptions.certificateDir, "/path/to/certs/")
38+
XCTAssertTrue(tlsOptions.useSelfSignedCertificate)
39+
XCTAssertFalse(tlsOptions.useProvidedKeystore)
40+
}
41+
42+
func test_init_withPKCS12() {
43+
let tlsOptions = NIOHTTPClientTLSOptions(
44+
pkcs12Path: "/path/to/cert.p12",
45+
pkcs12Password: "password"
46+
)
47+
48+
XCTAssertEqual(tlsOptions.pkcs12Path, "/path/to/cert.p12")
49+
XCTAssertEqual(tlsOptions.pkcs12Password, "password")
50+
XCTAssertFalse(tlsOptions.useSelfSignedCertificate)
51+
XCTAssertTrue(tlsOptions.useProvidedKeystore)
52+
}
53+
54+
func test_init_withCertificateAndPrivateKey() {
55+
let tlsOptions = NIOHTTPClientTLSOptions(
56+
certificate: "/path/to/cert.pem",
57+
privateKey: "/path/to/key.pem"
58+
)
59+
60+
XCTAssertEqual(tlsOptions.certificate, "/path/to/cert.pem")
61+
XCTAssertEqual(tlsOptions.privateKey, "/path/to/key.pem")
62+
XCTAssertTrue(tlsOptions.useSelfSignedCertificate)
63+
XCTAssertTrue(tlsOptions.useProvidedKeystore)
64+
}
65+
}

0 commit comments

Comments
 (0)