Skip to content

Commit 28c56b2

Browse files
authored
Merge branch 'main' into ts-tls-config2
2 parents 7a6826b + ca722d8 commit 28c56b2

9 files changed

+183
-8
lines changed

Diff for: Package.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ let package = Package(
2222
],
2323
dependencies: [
2424
.package(url: "https://github.com/apple/swift-nio.git", from: "2.27.0"),
25-
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.8.0"),
25+
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.13.0"),
2626
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.3.0"),
2727
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.5.1"),
2828
.package(url: "https://github.com/apple/swift-log.git", from: "1.4.0"),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIOSSL
16+
17+
/// Wrapper around `TLSConfiguration` from NIOSSL to provide a best effort implementation of `Hashable`
18+
struct BestEffortHashableTLSConfiguration: Hashable {
19+
let base: TLSConfiguration
20+
21+
init(wrapping base: TLSConfiguration) {
22+
self.base = base
23+
}
24+
25+
func hash(into hasher: inout Hasher) {
26+
self.base.bestEffortHash(into: &hasher)
27+
}
28+
29+
static func == (lhs: BestEffortHashableTLSConfiguration, rhs: BestEffortHashableTLSConfiguration) -> Bool {
30+
return lhs.base.bestEffortEquals(rhs.base)
31+
}
32+
}

Diff for: Sources/AsyncHTTPClient/ConnectionPool.swift

+14-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ final class ConnectionPool {
8282
} else {
8383
let provider = HTTP1ConnectionProvider(key: key,
8484
eventLoop: taskEventLoop,
85-
configuration: self.configuration,
85+
configuration: key.config(overriding: self.configuration),
8686
pool: self,
8787
backgroundActivityLogger: self.backgroundActivityLogger)
8888
let enqueued = provider.enqueue()
@@ -139,12 +139,16 @@ final class ConnectionPool {
139139
self.port = request.port
140140
self.host = request.host
141141
self.unixPath = request.socketPath
142+
if let tls = request.tlsConfiguration {
143+
self.tlsConfiguration = BestEffortHashableTLSConfiguration(wrapping: tls)
144+
}
142145
}
143146

144147
var scheme: Scheme
145148
var host: String
146149
var port: Int
147150
var unixPath: String
151+
var tlsConfiguration: BestEffortHashableTLSConfiguration?
148152

149153
enum Scheme: Hashable {
150154
case http
@@ -162,6 +166,15 @@ final class ConnectionPool {
162166
}
163167
}
164168
}
169+
170+
/// Returns a key-specific `HTTPClient.Configuration` by overriding the properties of `base`
171+
func config(overriding base: HTTPClient.Configuration) -> HTTPClient.Configuration {
172+
var config = base
173+
if let tlsConfiguration = self.tlsConfiguration {
174+
config.tlsConfiguration = tlsConfiguration.base
175+
}
176+
return config
177+
}
165178
}
166179
}
167180

Diff for: Sources/AsyncHTTPClient/HTTPHandler.swift

+39-1
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ extension HTTPClient {
186186
public var headers: HTTPHeaders
187187
/// Request body, defaults to no body.
188188
public var body: Body?
189+
/// Request-specific TLS configuration, defaults to no request-specific TLS configuration.
190+
public var tlsConfiguration: TLSConfiguration?
189191

190192
struct RedirectState {
191193
var count: Int
@@ -209,11 +211,29 @@ extension HTTPClient {
209211
/// - `unsupportedScheme` if URL does contains unsupported HTTP scheme.
210212
/// - `emptyHost` if URL does not contains a host.
211213
public init(url: String, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: Body? = nil) throws {
214+
try self.init(url: url, method: method, headers: headers, body: body, tlsConfiguration: nil)
215+
}
216+
217+
/// Create HTTP request.
218+
///
219+
/// - parameters:
220+
/// - url: Remote `URL`.
221+
/// - version: HTTP version.
222+
/// - method: HTTP method.
223+
/// - headers: Custom HTTP headers.
224+
/// - body: Request body.
225+
/// - tlsConfiguration: Request TLS configuration
226+
/// - throws:
227+
/// - `invalidURL` if URL cannot be parsed.
228+
/// - `emptyScheme` if URL does not contain HTTP scheme.
229+
/// - `unsupportedScheme` if URL does contains unsupported HTTP scheme.
230+
/// - `emptyHost` if URL does not contains a host.
231+
public init(url: String, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: Body? = nil, tlsConfiguration: TLSConfiguration?) throws {
212232
guard let url = URL(string: url) else {
213233
throw HTTPClientError.invalidURL
214234
}
215235

216-
try self.init(url: url, method: method, headers: headers, body: body)
236+
try self.init(url: url, method: method, headers: headers, body: body, tlsConfiguration: tlsConfiguration)
217237
}
218238

219239
/// Create an HTTP `Request`.
@@ -229,6 +249,23 @@ extension HTTPClient {
229249
/// - `emptyHost` if URL does not contains a host.
230250
/// - `missingSocketPath` if URL does not contains a socketPath as an encoded host.
231251
public init(url: URL, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: Body? = nil) throws {
252+
try self.init(url: url, method: method, headers: headers, body: body, tlsConfiguration: nil)
253+
}
254+
255+
/// Create an HTTP `Request`.
256+
///
257+
/// - parameters:
258+
/// - url: Remote `URL`.
259+
/// - method: HTTP method.
260+
/// - headers: Custom HTTP headers.
261+
/// - body: Request body.
262+
/// - tlsConfiguration: Request TLS configuration
263+
/// - throws:
264+
/// - `emptyScheme` if URL does not contain HTTP scheme.
265+
/// - `unsupportedScheme` if URL does contains unsupported HTTP scheme.
266+
/// - `emptyHost` if URL does not contains a host.
267+
/// - `missingSocketPath` if URL does not contains a socketPath as an encoded host.
268+
public init(url: URL, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: Body? = nil, tlsConfiguration: TLSConfiguration?) throws {
232269
guard let scheme = url.scheme?.lowercased() else {
233270
throw HTTPClientError.emptyScheme
234271
}
@@ -244,6 +281,7 @@ extension HTTPClient {
244281
self.scheme = scheme
245282
self.headers = headers
246283
self.body = body
284+
self.tlsConfiguration = tlsConfiguration
247285
}
248286

249287
/// Whether request will be executed using secure socket.

Diff for: Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift

+1
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ extension HTTPClientTests {
133133
("testFileDownloadChunked", testFileDownloadChunked),
134134
("testCloseWhileBackpressureIsExertedIsFine", testCloseWhileBackpressureIsExertedIsFine),
135135
("testErrorAfterCloseWhileBackpressureExerted", testErrorAfterCloseWhileBackpressureExerted),
136+
("testRequestSpecificTLS", testRequestSpecificTLS),
136137
]
137138
}
138139
}

Diff for: Tests/AsyncHTTPClientTests/HTTPClientTests.swift

+56-2
Original file line numberDiff line numberDiff line change
@@ -2711,7 +2711,10 @@ class HTTPClientTests: XCTestCase {
27112711
if isTestingNIOTS() {
27122712
XCTAssertEqual(error as? ChannelError, .connectTimeout(.milliseconds(100)))
27132713
} else {
2714-
XCTAssertEqual(error as? NIOSSLError, NIOSSLError.uncleanShutdown)
2714+
switch error as? NIOSSLError {
2715+
case .some(.handshakeFailed(.sslError(_))): break
2716+
default: XCTFail("Handshake failed with unexpected error: \(String(describing: error))")
2717+
}
27152718
}
27162719
}
27172720
}
@@ -2755,7 +2758,10 @@ class HTTPClientTests: XCTestCase {
27552758
if isTestingNIOTS() {
27562759
XCTAssertEqual(error as? ChannelError, .connectTimeout(.milliseconds(200)))
27572760
} else {
2758-
XCTAssertEqual(error as? NIOSSLError, NIOSSLError.uncleanShutdown)
2761+
switch error as? NIOSSLError {
2762+
case .some(.handshakeFailed(.sslError(_))): break
2763+
default: XCTFail("Handshake failed with unexpected error: \(String(describing: error))")
2764+
}
27592765
}
27602766
}
27612767
}
@@ -2910,4 +2916,52 @@ class HTTPClientTests: XCTestCase {
29102916
XCTAssertEqual(error as? ExpectedError, .expected)
29112917
}
29122918
}
2919+
2920+
func testRequestSpecificTLS() throws {
2921+
let configuration = HTTPClient.Configuration(tlsConfiguration: nil,
2922+
timeout: .init(),
2923+
ignoreUncleanSSLShutdown: false,
2924+
decompression: .disabled)
2925+
let localHTTPBin = HTTPBin(ssl: true)
2926+
let localClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup),
2927+
configuration: configuration)
2928+
let decoder = JSONDecoder()
2929+
2930+
defer {
2931+
XCTAssertNoThrow(try localClient.syncShutdown())
2932+
XCTAssertNoThrow(try localHTTPBin.shutdown())
2933+
}
2934+
2935+
// First two requests use identical TLS configurations.
2936+
let firstRequest = try HTTPClient.Request(url: "https://localhost:\(localHTTPBin.port)/get", method: .GET, tlsConfiguration: .forClient(certificateVerification: .none))
2937+
let firstResponse = try localClient.execute(request: firstRequest).wait()
2938+
guard let firstBody = firstResponse.body else {
2939+
XCTFail("No request body found")
2940+
return
2941+
}
2942+
let firstConnectionNumber = try decoder.decode(RequestInfo.self, from: firstBody).connectionNumber
2943+
2944+
let secondRequest = try HTTPClient.Request(url: "https://localhost:\(localHTTPBin.port)/get", method: .GET, tlsConfiguration: .forClient(certificateVerification: .none))
2945+
let secondResponse = try localClient.execute(request: secondRequest).wait()
2946+
guard let secondBody = secondResponse.body else {
2947+
XCTFail("No request body found")
2948+
return
2949+
}
2950+
let secondConnectionNumber = try decoder.decode(RequestInfo.self, from: secondBody).connectionNumber
2951+
2952+
// Uses a differrent TLS config.
2953+
let thirdRequest = try HTTPClient.Request(url: "https://localhost:\(localHTTPBin.port)/get", method: .GET, tlsConfiguration: .forClient(maximumTLSVersion: .tlsv1, certificateVerification: .none))
2954+
let thirdResponse = try localClient.execute(request: thirdRequest).wait()
2955+
guard let thirdBody = thirdResponse.body else {
2956+
XCTFail("No request body found")
2957+
return
2958+
}
2959+
let thirdConnectionNumber = try decoder.decode(RequestInfo.self, from: thirdBody).connectionNumber
2960+
2961+
XCTAssertEqual(firstResponse.status, .ok)
2962+
XCTAssertEqual(secondResponse.status, .ok)
2963+
XCTAssertEqual(thirdResponse.status, .ok)
2964+
XCTAssertEqual(firstConnectionNumber, secondConnectionNumber, "Identical TLS configurations did not use the same connection")
2965+
XCTAssertNotEqual(thirdConnectionNumber, firstConnectionNumber, "Different TLS configurations did not use different connections.")
2966+
}
29132967
}

Diff for: docker/Dockerfile

+4-3
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ RUN apt-get update && apt-get install -y wget
1717
RUN apt-get update && apt-get install -y lsof dnsutils netcat-openbsd net-tools libz-dev curl jq # used by integration tests
1818

1919
# ruby and jazzy for docs generation
20-
RUN apt-get update && apt-get install -y ruby ruby-dev libsqlite3-dev
21-
# jazzy no longer works on xenial as ruby is too old.
22-
RUN if [ "${ubuntu_version}" != "xenial" ] ; then gem install jazzy --no-ri --no-rdoc ; fi
20+
RUN apt-get update && apt-get install -y ruby ruby-dev libsqlite3-dev build-essential
21+
# switch of gem docs building
22+
RUN echo "gem: --no-document" > ~/.gemrc
23+
RUN if [ "${ubuntu_version}" != "xenial" ] ; then gem install jazzy ; fi
2324

2425
# tools
2526
RUN mkdir -p $HOME/.tools

Diff for: docker/docker-compose.2004.54.yaml

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
version: "3"
2+
3+
services:
4+
5+
runtime-setup:
6+
image: async-http-client:20.04-5.4
7+
build:
8+
args:
9+
ubuntu_version: "focal"
10+
swift_version: "5.4"
11+
12+
test:
13+
image: async-http-client:20.04-5.4
14+
environment: []
15+
#- SANITIZER_ARG=--sanitize=thread
16+
17+
shell:
18+
image: async-http-client:20.04-5.4

Diff for: docker/docker-compose.2004.main.yaml

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
version: "3"
2+
3+
services:
4+
5+
runtime-setup:
6+
image: async-http-client:20.04-main
7+
build:
8+
args:
9+
base_image: "swiftlang/swift:nightly-main-focal"
10+
11+
12+
test:
13+
image: async-http-client:20.04-main
14+
environment: []
15+
#- SANITIZER_ARG=--sanitize=thread
16+
17+
shell:
18+
image: async-http-client:20.04-main

0 commit comments

Comments
 (0)