diff --git a/Source/AwsCommonRuntimeKit/auth/credentials/CredentialsProvider.swift b/Source/AwsCommonRuntimeKit/auth/credentials/CredentialsProvider.swift index 34cea7c65..744c6747e 100644 --- a/Source/AwsCommonRuntimeKit/auth/credentials/CredentialsProvider.swift +++ b/Source/AwsCommonRuntimeKit/auth/credentials/CredentialsProvider.swift @@ -170,6 +170,7 @@ extension CredentialsProvider.Source { /// (by default ~/.aws/profile and ~/.aws/credentials) /// /// - Parameters: + /// - bootstrap: Connection bootstrap to use for any network connections made while sourcing credentials. /// - configFileNameOverride: (Optional) Override path to the profile config file (~/.aws/config by default) /// - profileFileNameOverride: (Optional) Override of what profile to use to source credentials from ('default' by default) /// - credentialsFileNameOverride: (Optional) Override path to the profile credentials file (~/.aws/credentials by default) @@ -177,7 +178,8 @@ extension CredentialsProvider.Source { /// - allocator: (Optional) allocator to override /// - Returns: `CredentialsProvider` /// - Throws: CommonRuntimeError.crtError - public static func `profile`(configFileNameOverride: String? = nil, + public static func `profile`(bootstrap: ClientBootstrap, + configFileNameOverride: String? = nil, profileFileNameOverride: String? = nil, credentialsFileNameOverride: String? = nil, shutdownCallback: ShutdownCallback? = nil, @@ -185,6 +187,7 @@ extension CredentialsProvider.Source { Self { allocator in let shutdownCallbackCore = ShutdownCallbackCore(shutdownCallback) var profileOptionsC = aws_credentials_provider_profile_options() + profileOptionsC.bootstrap = bootstrap.rawValue profileOptionsC.shutdown_options = shutdownCallbackCore.getRetainedCredentialProviderShutdownOptions() guard let provider: UnsafeMutablePointer = withByteCursorFromStrings( configFileNameOverride, diff --git a/Source/AwsCommonRuntimeKit/auth/signing/Signer.swift b/Source/AwsCommonRuntimeKit/auth/signing/Signer.swift index 6301e9c85..f1b4ec926 100644 --- a/Source/AwsCommonRuntimeKit/auth/signing/Signer.swift +++ b/Source/AwsCommonRuntimeKit/auth/signing/Signer.swift @@ -30,9 +30,9 @@ public class Signer { /// - `Throws`: An error of type `AwsCommonRuntimeError` which will pull last error found in the CRT /// - `Returns`: Returns a signed http request `HttpRequest` public static func signRequest( - request: HTTPRequest, + request: HTTPRequestBase, config: SigningConfig, - allocator: Allocator = defaultAllocator) async throws -> HTTPRequest { + allocator: Allocator = defaultAllocator) async throws -> HTTPRequestBase { guard let signable = aws_signable_new_http_request(allocator.rawValue, request.rawValue) else { throw CommonRunTimeError.crtError(.makeFromLastError()) @@ -41,7 +41,9 @@ public class Signer { aws_signable_destroy(signable) } - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation< + HTTPRequestBase, + Error>) in let signRequestCore = SignRequestCore(request: request, continuation: continuation, shouldSignHeader: config.shouldSignHeader, @@ -74,11 +76,11 @@ public class Signer { class SignRequestCore { let allocator: Allocator - let request: HTTPRequest - var continuation: CheckedContinuation + let request: HTTPRequestBase + var continuation: CheckedContinuation let shouldSignHeader: ((String) -> Bool)? - init(request: HTTPRequest, - continuation: CheckedContinuation, + init(request: HTTPRequestBase, + continuation: CheckedContinuation, shouldSignHeader: ((String) -> Bool)? = nil, allocator: Allocator) { self.allocator = allocator diff --git a/Source/AwsCommonRuntimeKit/crt/Allocator.swift b/Source/AwsCommonRuntimeKit/crt/Allocator.swift index eebf1c4ad..e5fa11595 100644 --- a/Source/AwsCommonRuntimeKit/crt/Allocator.swift +++ b/Source/AwsCommonRuntimeKit/crt/Allocator.swift @@ -71,7 +71,7 @@ public final class TracingAllocator: Allocator { * - Parameter framesPerStack: How many frames to record for each allocation * (8 as usually a good default to start with). */ - public convenience init(tracingStacksOf allocator: Allocator, framesPerStack: Int = 16) { + public convenience init(tracingStacksOf allocator: Allocator, framesPerStack: Int = 10) { self.init(allocator, level: .stacks, framesPerStack: framesPerStack) } diff --git a/Source/AwsCommonRuntimeKit/crt/CStruct.swift b/Source/AwsCommonRuntimeKit/crt/CStruct.swift index 9ecabefb7..22a5fdb34 100644 --- a/Source/AwsCommonRuntimeKit/crt/CStruct.swift +++ b/Source/AwsCommonRuntimeKit/crt/CStruct.swift @@ -106,7 +106,47 @@ func withOptionalCStructPointer( + _ arg1: Arg1Type?, + _ arg2: Arg2Type?, + _ arg3: Arg3Type?, + _ arg4: Arg4Type?, + _ arg5: Arg5Type?, + _ arg6: Arg6Type?, + _ body: (UnsafePointer?, + UnsafePointer?, + UnsafePointer?, + UnsafePointer?, + UnsafePointer?, + UnsafePointer?) -> Result +) -> Result { + return withOptionalCStructPointer(arg1, arg2) { arg1Pointer, arg2Pointer in + return withOptionalCStructPointer(arg3, arg4) { arg3Pointer, arg4Pointer in + return withOptionalCStructPointer(arg5, arg6) { arg5Pointer, arg6Pointer in + return body( + arg1Pointer, + arg2Pointer, + arg3Pointer, + arg4Pointer, + arg5Pointer, + arg6Pointer) } } } diff --git a/Source/AwsCommonRuntimeKit/crt/Utilities.swift b/Source/AwsCommonRuntimeKit/crt/Utilities.swift index 505eff52e..88fc1b9d2 100644 --- a/Source/AwsCommonRuntimeKit/crt/Utilities.swift +++ b/Source/AwsCommonRuntimeKit/crt/Utilities.swift @@ -66,9 +66,25 @@ extension Data { } } + func withAWSByteCursorPointer(_ body: (UnsafeMutablePointer) -> Result) -> Result { + let count = self.count + return self.withUnsafeBytes { rawBufferPointer -> Result in + var cursor = aws_byte_cursor_from_array(rawBufferPointer.baseAddress, count) + return withUnsafeMutablePointer(to: &cursor) { + body($0) + } + } + } + public func encodeToHexString() -> String { map { String(format: "%02x", $0) }.joined() } + + func chunked(into size: Int) -> [Data] { + return stride(from: 0, to: count, by: size).map { + self[$0 ..< Swift.min($0 + size, count)] + } + } } extension aws_date_time { diff --git a/Source/AwsCommonRuntimeKit/event-stream/EventStreamHeader.swift b/Source/AwsCommonRuntimeKit/event-stream/EventStreamHeader.swift index e4a305332..fc239a85f 100644 --- a/Source/AwsCommonRuntimeKit/event-stream/EventStreamHeader.swift +++ b/Source/AwsCommonRuntimeKit/event-stream/EventStreamHeader.swift @@ -15,6 +15,11 @@ public struct EventStreamHeader { /// value.count can not be greater than EventStreamHeader.maxValueLength for supported types. public var value: EventStreamHeaderValue + + public init(name: String, value: EventStreamHeaderValue) { + self.name = name + self.value = value + } } public enum EventStreamHeaderValue: Equatable { diff --git a/Source/AwsCommonRuntimeKit/event-stream/EventStreamMessage.swift b/Source/AwsCommonRuntimeKit/event-stream/EventStreamMessage.swift index f1dae9930..05aaf1600 100644 --- a/Source/AwsCommonRuntimeKit/event-stream/EventStreamMessage.swift +++ b/Source/AwsCommonRuntimeKit/event-stream/EventStreamMessage.swift @@ -9,6 +9,14 @@ public struct EventStreamMessage { var payload: Data = Data() var allocator: Allocator = defaultAllocator + public init(headers: [EventStreamHeader] = [EventStreamHeader](), + payload: Data = Data(), + allocator: Allocator = defaultAllocator) { + self.headers = headers + self.payload = payload + self.allocator = allocator + } + /// Get the binary format of this message (i.e. for sending across the wire manually) /// - Returns: binary Data. public func getEncoded() throws -> Data { diff --git a/Source/AwsCommonRuntimeKit/http/HTTP1Stream.swift b/Source/AwsCommonRuntimeKit/http/HTTP1Stream.swift new file mode 100644 index 000000000..dd68131e0 --- /dev/null +++ b/Source/AwsCommonRuntimeKit/http/HTTP1Stream.swift @@ -0,0 +1,23 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0. +import AwsCHttp +import Foundation + +/// An HTTP1Stream represents a single HTTP/1.1 specific Http Request/Response. +public class HTTP1Stream: HTTPStream { + /// Stream keeps a reference to HttpConnection to keep it alive + private let httpConnection: HTTPClientConnection + + // Called by HTTPClientConnection + init( + httpConnection: HTTPClientConnection, + options: aws_http_make_request_options, + callbackData: HTTPStreamCallbackCore) throws { + guard let rawValue = withUnsafePointer( + to: options, { aws_http_connection_make_request(httpConnection.rawValue, $0) }) else { + throw CommonRunTimeError.crtError(.makeFromLastError()) + } + self.httpConnection = httpConnection + super.init(rawValue: rawValue, callbackData: callbackData) + } +} diff --git a/Source/AwsCommonRuntimeKit/http/HTTP2ClientConnection.swift b/Source/AwsCommonRuntimeKit/http/HTTP2ClientConnection.swift new file mode 100644 index 000000000..c4e2c1321 --- /dev/null +++ b/Source/AwsCommonRuntimeKit/http/HTTP2ClientConnection.swift @@ -0,0 +1,126 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0. + +import AwsCHttp +import AwsCIo +import Foundation + +public class HTTP2ClientConnection: HTTPClientConnection { + + /// Creates a new http2 stream from the `HTTPRequestOptions` given. + /// - Parameter requestOptions: An `HTTPRequestOptions` struct containing callbacks on + /// the different events from the stream + /// - Returns: An `HTTP2Stream` + override public func makeRequest(requestOptions: HTTPRequestOptions) throws -> HTTPStream { + let httpStreamCallbackCore = HTTPStreamCallbackCore(requestOptions: requestOptions) + do { + return try HTTP2Stream(httpConnection: self, + options: httpStreamCallbackCore.getRetainedHttpMakeRequestOptions(), + callbackData: httpStreamCallbackCore) + } catch { + httpStreamCallbackCore.release() + throw error + } + } + + /// Send a SETTINGS frame (HTTP/2 only). + /// SETTINGS will be applied locally when settings ACK is received from peer. + /// - Parameter setting: The settings to change + public func updateSetting(setting: HTTP2Settings) async throws { + try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation<(), Error>) in + let continuationCore = ContinuationCore(continuation: continuation) + setting.withCStruct { settingList in + let count = settingList.count + settingList.withUnsafeBufferPointer { pointer in + guard aws_http2_connection_change_settings( + rawValue, + pointer.baseAddress!, + count, + onChangeSettingsComplete, + continuationCore.passRetained()) == AWS_OP_SUCCESS else { + continuationCore.release() + continuation.resume(throwing: CommonRunTimeError.crtError(.makeFromLastError())) + return + } + } + } + }) + } + + /// Send a PING frame. Round-trip-time is calculated when PING ACK is received from peer. + /// - Parameter data: (Optional) 8 Bytes data with the PING frame. Data count must be exact 8 bytes. + /// - Returns: The round trip time in nanoseconds for the connection. + public func sendPing(data: Data = Data()) async throws -> UInt64 { + try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) in + let continuationCore = ContinuationCore(continuation: continuation) + data.withAWSByteCursorPointer { dataPointer in + guard aws_http2_connection_ping( + rawValue, + data.isEmpty ? nil : dataPointer, + onPingComplete, + continuationCore.passRetained()) == AWS_OP_SUCCESS + else { + continuationCore.release() + continuation.resume(throwing: CommonRunTimeError.crtError(.makeFromLastError())) + return + } + } + }) + } + + /// Send a custom GOAWAY frame. + /// + /// Note that the connection automatically attempts to send a GOAWAY during + /// shutdown (unless a GOAWAY with a valid Last-Stream-ID has already been sent). + /// + /// This call can be used to gracefully warn the peer of an impending shutdown + /// (error=0, allowMoreStreams=true), or to customize the final GOAWAY + /// frame that is sent by this connection. + /// + /// The other end may not receive the goaway, if the connection already closed. + /// + /// - Parameters: + /// - error: The HTTP/2 error code to send. + /// - allowMoreStreams: If true, new peer-initiated streams will continue to be acknowledged and the GOAWAY's Last-Stream-ID will + /// be set to a max value. If false, new peer-initiated streams will be ignored and the GOAWAY's + /// Last-Stream-ID will be set to the latest acknowledged stream. + /// - debugData: (Optional) debug data to send. Size must not exceed 16KB. + public func sendGoAway(error: HTTP2Error, allowMoreStreams: Bool, debugData: Data = Data()) { + debugData.withAWSByteCursorPointer { dataPointer in + aws_http2_connection_send_goaway( + rawValue, + error.rawValue, + allowMoreStreams, + dataPointer) + } + } +} + +private func onChangeSettingsComplete(connection: UnsafeMutablePointer?, + errorCode: Int32, + userData: UnsafeMutableRawPointer!) { + let continuation = Unmanaged>.fromOpaque(userData).takeRetainedValue().continuation + + guard errorCode == AWS_OP_SUCCESS else { + continuation.resume(throwing: CommonRunTimeError.crtError(CRTError(code: errorCode))) + return + } + + // SUCCESS + continuation.resume() +} + +private func onPingComplete(connection: UnsafeMutablePointer?, + roundTripTimeNs: UInt64, + errorCode: Int32, + userData: UnsafeMutableRawPointer!) { + let continuation = Unmanaged>.fromOpaque(userData).takeRetainedValue().continuation + + guard errorCode == AWS_OP_SUCCESS else { + continuation.resume(throwing: CommonRunTimeError.crtError(CRTError(code: errorCode))) + return + } + + // SUCCESS + continuation.resume(returning: roundTripTimeNs) +} diff --git a/Source/AwsCommonRuntimeKit/http/HTTP2Error.swift b/Source/AwsCommonRuntimeKit/http/HTTP2Error.swift new file mode 100644 index 000000000..17d06d3b5 --- /dev/null +++ b/Source/AwsCommonRuntimeKit/http/HTTP2Error.swift @@ -0,0 +1,19 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0. + +/// Error codes that may be present in HTTP/2 RST_STREAM and GOAWAY frames (RFC-7540 7). +public enum HTTP2Error: UInt32 { + case protocolError = 1 + case internalError = 2 + case flowControlError = 3 + case settingsTimeout = 4 + case streamClosed = 5 + case frameSizeError = 6 + case refusedStream = 7 + case cancel = 8 + case compressionError = 9 + case connectError = 10 + case enhanceYourCalm = 11 + case inadequateSecurity = 12 + case HTTP_1_1_Required = 13 +} diff --git a/Source/AwsCommonRuntimeKit/http/HTTP2Settings.swift b/Source/AwsCommonRuntimeKit/http/HTTP2Settings.swift new file mode 100644 index 000000000..394058d74 --- /dev/null +++ b/Source/AwsCommonRuntimeKit/http/HTTP2Settings.swift @@ -0,0 +1,72 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0. + +import AwsCHttp + +/// Predefined settings identifiers (RFC-7540 6.5.2) +/// Nil means use default values +public struct HTTP2Settings: CStruct { + public var headerTableSize: UInt32? + public var enablePush: Bool? + public var maxConcurrentStreams: UInt32? + public var initialWindowSize: UInt32? + public var maxFrameSize: UInt32? + public var maxHeaderListSize: UInt32? + + public init(headerTableSize: UInt32? = nil, + enablePush: Bool? = nil, + maxConcurrentStreams: UInt32? = nil, + initialWindowSize: UInt32? = nil, + maxFrameSize: UInt32? = nil, + maxHeaderListSize: UInt32? = nil) { + self.headerTableSize = headerTableSize + self.enablePush = enablePush + self.maxConcurrentStreams = maxConcurrentStreams + self.initialWindowSize = initialWindowSize + self.maxFrameSize = maxFrameSize + self.maxHeaderListSize = maxHeaderListSize + } + + typealias RawType = [aws_http2_setting] + func withCStruct(_ body: ([aws_http2_setting]) -> Result + ) -> Result { + var http2SettingList = [aws_http2_setting]() + if let value = headerTableSize { + http2SettingList.append( + aws_http2_setting( + id: AWS_HTTP2_SETTINGS_HEADER_TABLE_SIZE, + value: value)) + } + if let value = enablePush { + http2SettingList.append( + aws_http2_setting( + id: AWS_HTTP2_SETTINGS_ENABLE_PUSH, + value: value.uintValue)) + } + if let value = maxConcurrentStreams { + http2SettingList.append( + aws_http2_setting( + id: AWS_HTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, + value: value)) + } + if let value = initialWindowSize { + http2SettingList.append( + aws_http2_setting( + id: AWS_HTTP2_SETTINGS_INITIAL_WINDOW_SIZE, + value: value)) + } + if let value = maxFrameSize { + http2SettingList.append( + aws_http2_setting( + id: AWS_HTTP2_SETTINGS_MAX_FRAME_SIZE, + value: value)) + } + if let value = maxHeaderListSize { + http2SettingList.append( + aws_http2_setting( + id: AWS_HTTP2_SETTINGS_MAX_HEADER_LIST_SIZE, + value: value)) + } + return body(http2SettingList) + } +} diff --git a/Source/AwsCommonRuntimeKit/http/HTTP2Stream.swift b/Source/AwsCommonRuntimeKit/http/HTTP2Stream.swift new file mode 100644 index 000000000..2343ce92f --- /dev/null +++ b/Source/AwsCommonRuntimeKit/http/HTTP2Stream.swift @@ -0,0 +1,83 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0. + +import AwsCHttp +import Foundation + +/// An HTTP2Stream represents a single HTTP/2 specific HTTP Request/Response. +public class HTTP2Stream: HTTPStream { + private let httpConnection: HTTPClientConnection? + + // Called by Connection Manager + init( + httpConnection: HTTPClientConnection, + options: aws_http_make_request_options, + callbackData: HTTPStreamCallbackCore) throws { + guard let rawValue = withUnsafePointer( + to: options, { aws_http_connection_make_request(httpConnection.rawValue, $0) }) else { + throw CommonRunTimeError.crtError(.makeFromLastError()) + } + self.httpConnection = httpConnection + super.init(rawValue: rawValue, callbackData: callbackData) + } + + // Called by Stream manager + override init(rawValue: UnsafeMutablePointer, + callbackData: HTTPStreamCallbackCore) { + httpConnection = nil + super.init(rawValue: rawValue, callbackData: callbackData) + } + + /// Reset the HTTP/2 stream (HTTP/2 only). + /// Note that if the stream closes before this async call is fully processed, the RST_STREAM frame will not be sent. + /// - Parameter error: Reason to reset the stream. + public func resetStream(error: HTTP2Error) throws { + guard aws_http2_stream_reset(super.rawValue, error.rawValue) == AWS_OP_SUCCESS else { + throw CommonRunTimeError.crtError(.makeFromLastError()) + } + } + + /// manualDataWrites must have been enabled during HTTP2Request creation. + /// A write with that has endOfStream set to be true will end the stream and prevent any further write. + /// + /// - Parameters: + /// - data: Data to write. It can be empty + /// - endOfStream: Set it true to end the stream and prevent any further write. + /// The last frame must be send with the value true. + /// - allocator: (Optional) allocator to override + /// - Throws: + public func writeData(data: Data, endOfStream: Bool) async throws { + var options = aws_http2_stream_write_data_options() + options.end_stream = endOfStream + options.on_complete = onWriteComplete + try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation<(), Error>) in + let continuationCore = ContinuationCore(continuation: continuation) + let stream = IStreamCore( + iStreamable: ByteBuffer(data: data), + allocator: callbackData.requestOptions.request.allocator) + options.data = stream.rawValue + options.user_data = continuationCore.passRetained() + guard aws_http2_stream_write_data( + rawValue, + &options) == AWS_OP_SUCCESS else { + continuationCore.release() + continuation.resume(throwing: CommonRunTimeError.crtError(.makeFromLastError())) + return + } + + }) + } +} + +private func onWriteComplete(stream: UnsafeMutablePointer?, + errorCode: Int32, + userData: UnsafeMutableRawPointer!) { + let continuation = Unmanaged>.fromOpaque(userData).takeRetainedValue().continuation + guard errorCode == AWS_OP_SUCCESS else { + continuation.resume(throwing: CommonRunTimeError.crtError(CRTError(code: errorCode))) + return + } + + // SUCCESS + continuation.resume() +} diff --git a/Source/AwsCommonRuntimeKit/http/HTTP2StreamManager.swift b/Source/AwsCommonRuntimeKit/http/HTTP2StreamManager.swift new file mode 100644 index 000000000..afaca3403 --- /dev/null +++ b/Source/AwsCommonRuntimeKit/http/HTTP2StreamManager.swift @@ -0,0 +1,78 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0. +import AwsCHttp +import Collections + +/// Manages a Pool of HTTP/2 Streams. Creates and manages HTTP/2 connections under the hood. +public class HTTP2StreamManager { + let rawValue: UnsafeMutablePointer + + public init(options: HTTP2StreamManagerOptions, allocator: Allocator = defaultAllocator) throws { + let shutdownCallbackCore = ShutdownCallbackCore(options.shutdownCallback) + let shutdownOptions = shutdownCallbackCore.getRetainedShutdownOptions() + guard let rawValue: UnsafeMutablePointer = ( + options.withCPointer(shutdownOptions: shutdownOptions) { managerOptions in + aws_http2_stream_manager_new(allocator.rawValue, managerOptions) + }) else { + shutdownCallbackCore.release() + throw CommonRunTimeError.crtError(.makeFromLastError()) + } + self.rawValue = rawValue + } + + /// Acquires an `HTTP2Stream` asynchronously. + /// There is no need to call activate. + /// - Parameter requestOptions: The Request to make to the Server. + /// - Returns: HTTP2Stream when the stream is acquired + /// - Throws: CommonRunTimeError.crtError + public func acquireStream(requestOptions: HTTPRequestOptions) async throws -> HTTP2Stream { + try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) in + let httpStreamCallbackCore = HTTPStreamCallbackCore(requestOptions: requestOptions) + let acquireStreamCore = HTTP2AcquireStreamCore( + continuation: continuation, + callbackCore: httpStreamCallbackCore) + let requestOptions = httpStreamCallbackCore.getRetainedHttpMakeRequestOptions() + + var options = aws_http2_stream_manager_acquire_stream_options() + options.callback = onStreamAcquired + options.user_data = acquireStreamCore.passRetained() + withUnsafePointer(to: requestOptions, { requestOptionsPointer in + options.options = requestOptionsPointer + aws_http2_stream_manager_acquire_stream(rawValue, &options) + }) + }) + } + + deinit { + aws_http2_stream_manager_release(rawValue) + } +} + +private class HTTP2AcquireStreamCore { + let continuation: CheckedContinuation + let callbackCore: HTTPStreamCallbackCore + + init(continuation: CheckedContinuation, callbackCore: HTTPStreamCallbackCore) { + self.callbackCore = callbackCore + self.continuation = continuation + } + + func passRetained() -> UnsafeMutableRawPointer { + Unmanaged.passRetained(self).toOpaque() + } +} + +private func onStreamAcquired(stream: UnsafeMutablePointer?, + errorCode: Int32, + userData: UnsafeMutableRawPointer!) { + let acquireStreamCore = Unmanaged.fromOpaque(userData).takeRetainedValue() + guard errorCode == AWS_OP_SUCCESS else { + acquireStreamCore.callbackCore.release() + acquireStreamCore.continuation.resume(throwing: CommonRunTimeError.crtError(CRTError(code: errorCode))) + return + } + + // SUCCESS + let http2Stream = HTTP2Stream(rawValue: stream!, callbackData: acquireStreamCore.callbackCore) + acquireStreamCore.continuation.resume(returning: http2Stream) +} diff --git a/Source/AwsCommonRuntimeKit/http/HTTP2StreamManagerOptions.swift b/Source/AwsCommonRuntimeKit/http/HTTP2StreamManagerOptions.swift new file mode 100644 index 000000000..495227313 --- /dev/null +++ b/Source/AwsCommonRuntimeKit/http/HTTP2StreamManagerOptions.swift @@ -0,0 +1,197 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0. + +import AwsCHttp + +/// Stream manager configuration struct. +/// Contains all of the configuration needed to create an http connection as well as +/// the maximum number of connections to ever have in existence. +public struct HTTP2StreamManagerOptions: CStructWithShutdownOptions { + + /// The client bootstrap instance to use to create the pool's connections + public var clientBootstrap: ClientBootstrap + /// The host name to use for connections in the connection pool + public var hostName: String + + /// The port to connect to for connections in the connection pool + public var port: UInt16 + /// The proxy options for connections in the connection pool + public var proxyOptions: HTTPProxyOptions? + /// Configuration for using proxy from environment variable. Only works when proxyOptions is not set. + public var proxyEnvSettings: HTTPProxyEnvSettings? + /// The socket options to use for connections in the connection pool + public var socketOptions: SocketOptions + /// The tls options to use for connections in the connection pool + public var tlsOptions: TLSConnectionOptions? + + /// HTTP/2 Stream window control. + /// If set to true, the read back pressure mechanism will be enabled for streams created. + /// The initial window size can be set by `initialWindowSize` via `http2InitialSettings` + public var enableStreamManualWindowManagement: Bool + + /// Max connections the manager can contain + public var maxConnections: Int + + /// Add a shut down callback using these options + public var shutdownCallback: ShutdownCallback? + + public var monitoringOptions: HTTPMonitoringOptions? + + /// Specify whether you have prior knowledge that cleartext (HTTP) connections are HTTP/2 (RFC-7540 3.4). + /// If false, then cleartext connections are treated as HTTP/1.1. + /// It is illegal to set this true when secure connections are being used. + /// Note that upgrading from HTTP/1.1 to HTTP/ 2 is not supported (RFC-7540 3.2). + public var priorKnowledge: Bool + + /// The data of settings to change for initial settings. + /// Note: each setting has its boundary. + public var initialSettings: HTTP2Settings? + + /// The max number of recently-closed streams to remember. + /// Set it to nil to use the default setting + /// + /// If the connection receives a frame for a closed stream, + /// the frame will be ignored or cause a connection error, + /// depending on the frame type and how the stream was closed. + /// Remembering more streams reduces the chances that a late frame causes + /// a connection error, but costs some memory. + public var maxClosedStreams: Int? + + /// Connection level window control + /// Set to true to manually manage the flow-control window of whole HTTP/2 connection. + /// + /// If false, the connection will maintain its flow-control windows such that + /// no back-pressure is applied and data arrives as fast as possible. + /// + /// If true, the flow-control window of the whole connection will shrink as body data + /// is received (headers, padding, and other metadata do not affect the window) for every streams + /// created on this connection. + /// The initial connection flow-control window is 65,535. It is not controllable. + /// Once the connection's flow-control window reaches to 0, all the streams on the connection stop receiving any + /// further data. + /// The user must call aws_http2_connection_update_window() to increment the connection's // TODO: update + /// window and keep data flowing. + /// Note: the padding of data frame counts to the flow-control window. + /// But, the client will always automatically update the window for padding even for manual window update. + public var enableConnectionManualWindowManagement: Bool + + /// (Optional) + /// When set, connection will be closed if 5xx response received from server. + public var closeConnectionOnServerError: Bool + + /// (Optional) + /// The period for all the connections held by stream manager to send a PING in milliseconds. + /// If you specify Nil, manager will NOT send any PING. + /// Note: if set, it must be large than the time of ping timeout setting. + public var connectionPingPeriodMs: Int? + + /// (Optional) + /// Network connection will be closed if a ping response is not received + /// within this amount of time (milliseconds). + public var connectionPingTimeoutMs: Int? + + /// (Optional) + /// The ideal number of concurrent streams for a connection. Stream manager will try to create a new connection if + /// one connection reaches this number. But, if the max connections reaches, manager will reuse connections to create + /// the acquired steams as much as possible. + public var idealConcurrentStreamsPerConnection: Int? + + /// (Optional) + /// Default is no limit, which will use the limit from the server. + /// The real number of concurrent streams per connection will be controlled by the minimal value of the setting from + /// other end and the value here. + public var maxConcurrentStreamsPerConnection: Int? + + public init(clientBootstrap: ClientBootstrap, + hostName: String, + port: UInt16, + maxConnections: Int, + proxyOptions: HTTPProxyOptions? = nil, + proxyEnvSettings: HTTPProxyEnvSettings? = nil, + socketOptions: SocketOptions = SocketOptions(), + tlsOptions: TLSConnectionOptions? = nil, + monitoringOptions: HTTPMonitoringOptions? = nil, + enableStreamManualWindowManagement: Bool = false, + shutdownCallback: ShutdownCallback? = nil, + priorKnowledge: Bool = false, + initialSettings: HTTP2Settings? = nil, + maxClosedStreams: Int? = nil, + enableConnectionManualWindowManagement: Bool = false, + closeConnectionOnServerError: Bool = false, + connectionPingPeriodMs: Int? = nil, + connectionPingTimeoutMs: Int? = nil, + idealConcurrentStreamsPerConnection: Int? = nil, + maxConcurrentStreamsPerConnection: Int? = nil) { + + self.clientBootstrap = clientBootstrap + self.hostName = hostName + self.port = port + self.proxyOptions = proxyOptions + self.proxyEnvSettings = proxyEnvSettings + self.socketOptions = socketOptions + self.tlsOptions = tlsOptions + self.monitoringOptions = monitoringOptions + self.maxConnections = maxConnections + self.enableStreamManualWindowManagement = enableStreamManualWindowManagement + self.shutdownCallback = shutdownCallback + self.priorKnowledge = priorKnowledge + self.initialSettings = initialSettings + self.maxClosedStreams = maxClosedStreams + self.enableConnectionManualWindowManagement = enableConnectionManualWindowManagement + self.closeConnectionOnServerError = closeConnectionOnServerError + self.connectionPingPeriodMs = connectionPingPeriodMs + self.connectionPingTimeoutMs = connectionPingTimeoutMs + self.idealConcurrentStreamsPerConnection = idealConcurrentStreamsPerConnection + self.maxConcurrentStreamsPerConnection = maxConcurrentStreamsPerConnection + } + + typealias RawType = aws_http2_stream_manager_options + // swiftlint:disable closure_parameter_position + func withCStruct( + shutdownOptions: aws_shutdown_callback_options, + _ body: (aws_http2_stream_manager_options) -> Result + ) -> Result { + return hostName.withByteCursor { hostNameCursor in + return withOptionalCStructPointer( + proxyOptions, + proxyEnvSettings, + socketOptions, + monitoringOptions, + tlsOptions, + initialSettings) { proxyPointer, proxyEnvSettingsPointer, socketPointer, + monitoringPointer, tlsPointer, http2SettingPointer in + + var cStreamManagerOptions = aws_http2_stream_manager_options() + cStreamManagerOptions.bootstrap = clientBootstrap.rawValue + cStreamManagerOptions.host = hostNameCursor + cStreamManagerOptions.port = port + cStreamManagerOptions.proxy_options = proxyPointer + cStreamManagerOptions.proxy_ev_settings = proxyEnvSettingsPointer + cStreamManagerOptions.socket_options = socketPointer + cStreamManagerOptions.tls_connection_options = tlsPointer + cStreamManagerOptions.monitoring_options = monitoringPointer + cStreamManagerOptions.max_connections = maxConnections + cStreamManagerOptions.enable_read_back_pressure = enableStreamManualWindowManagement + cStreamManagerOptions.shutdown_complete_user_data = shutdownOptions.shutdown_callback_user_data + cStreamManagerOptions.shutdown_complete_callback = shutdownOptions.shutdown_callback_fn + + cStreamManagerOptions.http2_prior_knowledge = priorKnowledge + cStreamManagerOptions.max_closed_streams = maxClosedStreams ?? 0 + cStreamManagerOptions.conn_manual_window_management = enableConnectionManualWindowManagement + cStreamManagerOptions.close_connection_on_server_error = closeConnectionOnServerError + cStreamManagerOptions.connection_ping_period_ms = connectionPingPeriodMs ?? 0 + cStreamManagerOptions.connection_ping_timeout_ms = connectionPingTimeoutMs ?? 0 + cStreamManagerOptions.ideal_concurrent_streams_per_connection = idealConcurrentStreamsPerConnection ?? 0 + cStreamManagerOptions.max_concurrent_streams_per_connection = maxConcurrentStreamsPerConnection ?? 0 + if let http2SettingPointer = http2SettingPointer { + return http2SettingPointer.pointee.withUnsafeBufferPointer { pointer in + cStreamManagerOptions.initial_settings_array = pointer.baseAddress! + cStreamManagerOptions.num_initial_settings = http2SettingPointer.pointee.count + return body(cStreamManagerOptions) + } + } + return body(cStreamManagerOptions) + } + } + } +} diff --git a/Source/AwsCommonRuntimeKit/http/HTTPClientConnection.swift b/Source/AwsCommonRuntimeKit/http/HTTPClientConnection.swift index 15f4aeb3f..c89354230 100644 --- a/Source/AwsCommonRuntimeKit/http/HTTPClientConnection.swift +++ b/Source/AwsCommonRuntimeKit/http/HTTPClientConnection.swift @@ -25,21 +25,27 @@ public class HTTPClientConnection { return aws_http_connection_is_open(rawValue) } + public var httpVersion: HTTPVersion { + HTTPVersion(rawValue: aws_http_connection_get_version(rawValue)) + } + /// Close the http connection public func close() { aws_http_connection_close(rawValue) } /// Creates a new http stream from the `HTTPRequestOptions` given. + /// The stream will send no data until HTTPStream.activate() + /// is called. Call activate() when you're ready for callbacks and events to fire. /// - Parameter requestOptions: An `HTTPRequestOptions` struct containing callbacks on /// the different events from the stream /// - Returns: An `HTTPStream` containing the `HTTPClientConnection` public func makeRequest(requestOptions: HTTPRequestOptions) throws -> HTTPStream { let httpStreamCallbackCore = HTTPStreamCallbackCore(requestOptions: requestOptions) do { - return try HTTPStream(httpConnection: self, - options: httpStreamCallbackCore.getRetainedHttpMakeRequestOptions(), - callbackData: httpStreamCallbackCore) + return try HTTP1Stream(httpConnection: self, + options: httpStreamCallbackCore.getRetainedHttpMakeRequestOptions(), + callbackData: httpStreamCallbackCore) } catch { httpStreamCallbackCore.release() throw error diff --git a/Source/AwsCommonRuntimeKit/http/HTTPClientConnectionManagerCallbackCore.swift b/Source/AwsCommonRuntimeKit/http/HTTPClientConnectionManagerCallbackCore.swift index 6ec5df4fc..82b11a499 100644 --- a/Source/AwsCommonRuntimeKit/http/HTTPClientConnectionManagerCallbackCore.swift +++ b/Source/AwsCommonRuntimeKit/http/HTTPClientConnectionManagerCallbackCore.swift @@ -39,13 +39,28 @@ private func onConnectionSetup(connection: UnsafeMutablePointer.fromOpaque(userData!).takeRetainedValue() + let continuation = callbackDataCore.continuation + if errorCode != AWS_OP_SUCCESS { - callbackDataCore.continuation.resume(throwing: CommonRunTimeError.crtError(CRTError(code: errorCode))) + continuation.resume(throwing: CommonRunTimeError.crtError(CRTError(code: errorCode))) return } // Success - let httpConnection = HTTPClientConnection(manager: callbackDataCore.connectionManager, - connection: connection!) - callbackDataCore.continuation.resume(returning: httpConnection) + switch aws_http_connection_get_version(connection) { + case AWS_HTTP_VERSION_2: continuation.resume( + returning: HTTP2ClientConnection( + manager: callbackDataCore.connectionManager, + connection: connection!)) + case AWS_HTTP_VERSION_1_1: + continuation.resume( + returning: HTTPClientConnection( + manager: callbackDataCore.connectionManager, + connection: connection!)) + default: + continuation.resume( + throwing: CommonRunTimeError.crtError( + CRTError( + code: AWS_ERROR_HTTP_UNSUPPORTED_PROTOCOL.rawValue))) + } } diff --git a/Source/AwsCommonRuntimeKit/http/HTTPHeaderBlock.swift b/Source/AwsCommonRuntimeKit/http/HTTPHeaderBlock.swift index f21f0f09c..9de1cead0 100644 --- a/Source/AwsCommonRuntimeKit/http/HTTPHeaderBlock.swift +++ b/Source/AwsCommonRuntimeKit/http/HTTPHeaderBlock.swift @@ -2,8 +2,7 @@ // SPDX-License-Identifier: Apache-2.0. import AwsCHttp -public enum HTTPHeaderBlock { - +enum HTTPHeaderBlock { /// Main header block sent with request or response. case main /// Header block for 1xx informational (interim) responses. @@ -14,12 +13,12 @@ public enum HTTPHeaderBlock { extension HTTPHeaderBlock: RawRepresentable, CaseIterable { - public init(rawValue: aws_http_header_block) { + init(rawValue: aws_http_header_block) { let value = Self.allCases.first(where: {$0.rawValue == rawValue}) self = value ?? .main } - public var rawValue: aws_http_header_block { + var rawValue: aws_http_header_block { switch self { case .main: return AWS_HTTP_HEADER_BLOCK_MAIN case .informational: return AWS_HTTP_HEADER_BLOCK_INFORMATIONAL diff --git a/Source/AwsCommonRuntimeKit/http/HTTPRequest.swift b/Source/AwsCommonRuntimeKit/http/HTTPRequest.swift index 23e827932..8e8fe6059 100644 --- a/Source/AwsCommonRuntimeKit/http/HTTPRequest.swift +++ b/Source/AwsCommonRuntimeKit/http/HTTPRequest.swift @@ -4,7 +4,8 @@ import AwsCHttp import AwsCIo import AwsCCommon -public class HTTPRequest: HTTPMessage { +/// Represents a single client request to be sent on a HTTP 1.1 connection +public class HTTPRequest: HTTPRequestBase { public var method: String { get { @@ -45,15 +46,36 @@ public class HTTPRequest: HTTPMessage { headers: [HTTPHeader] = [HTTPHeader](), body: IStreamable? = nil, allocator: Allocator = defaultAllocator) throws { - try super.init(allocator: allocator) + guard let rawValue = aws_http_message_new_request(allocator.rawValue) else { + throw CommonRunTimeError.crtError(.makeFromLastError()) + } + super.init(rawValue: rawValue, allocator: allocator) self.method = method self.path = path + self.body = body + addHeaders(headers: headers) + } +} - if let body = body { - let iStreamCore = IStreamCore(iStreamable: body, allocator: allocator) - aws_http_message_set_body_stream(self.rawValue, &iStreamCore.rawValue) +/// Represents a single client request to be sent on a HTTP2 connection +public class HTTP2Request: HTTPRequestBase { + /// Creates an http2 request which can be passed to a connection. + /// - Parameters: + /// - headers: (Optional) headers to send + /// - body: (Optional) body stream to send as part of request + /// - allocator: (Optional) allocator to override + /// - Throws: CommonRuntimeError + public init(headers: [HTTPHeader] = [HTTPHeader](), + body: IStreamable? = nil, + allocator: Allocator = defaultAllocator) throws { + + guard let rawValue = aws_http2_message_new_request(allocator.rawValue) else { + throw CommonRunTimeError.crtError(.makeFromLastError()) } + super.init(rawValue: rawValue, allocator: allocator) + + self.body = body addHeaders(headers: headers) } } diff --git a/Source/AwsCommonRuntimeKit/http/HTTPMessage.swift b/Source/AwsCommonRuntimeKit/http/HTTPRequestBase.swift similarity index 92% rename from Source/AwsCommonRuntimeKit/http/HTTPMessage.swift rename to Source/AwsCommonRuntimeKit/http/HTTPRequestBase.swift index 1f31826c5..606972755 100644 --- a/Source/AwsCommonRuntimeKit/http/HTTPMessage.swift +++ b/Source/AwsCommonRuntimeKit/http/HTTPRequestBase.swift @@ -3,7 +3,8 @@ import AwsCHttp import AwsCIo -public class HTTPMessage { +/// Represents a single client request to be sent +public class HTTPRequestBase { let rawValue: OpaquePointer let allocator: Allocator @@ -11,7 +12,7 @@ public class HTTPMessage { willSet(value) { if let newBody = value { let iStreamCore = IStreamCore(iStreamable: newBody, allocator: allocator) - aws_http_message_set_body_stream(self.rawValue, &iStreamCore.rawValue) + aws_http_message_set_body_stream(self.rawValue, iStreamCore.rawValue) } else { aws_http_message_set_body_stream(self.rawValue, nil) } @@ -20,11 +21,9 @@ public class HTTPMessage { // internal initializer. Consumers will initialize HttpRequest subclass and // not interact with this class directly. - init(allocator: Allocator = defaultAllocator) throws { + init(rawValue: OpaquePointer, + allocator: Allocator = defaultAllocator) { self.allocator = allocator - guard let rawValue = aws_http_message_new_request(allocator.rawValue) else { - throw CommonRunTimeError.crtError(.makeFromLastError()) - } self.rawValue = rawValue } @@ -33,7 +32,7 @@ public class HTTPMessage { } } -public extension HTTPMessage { +public extension HTTPRequestBase { var headerCount: Int { return aws_http_message_get_header_count(rawValue) diff --git a/Source/AwsCommonRuntimeKit/http/HTTPRequestOptions.swift b/Source/AwsCommonRuntimeKit/http/HTTPRequestOptions.swift index 6ee9f15da..0057368cd 100644 --- a/Source/AwsCommonRuntimeKit/http/HTTPRequestOptions.swift +++ b/Source/AwsCommonRuntimeKit/http/HTTPRequestOptions.swift @@ -2,30 +2,57 @@ // SPDX-License-Identifier: Apache-2.0. import Foundation +/// Definition for outgoing request and callbacks to receive response. public struct HTTPRequestOptions { - public typealias OnIncomingHeaders = (_ stream: HTTPStream, - _ headerBlock: HTTPHeaderBlock, + + /// Callback to receive interim response + public typealias OnInterimResponse = (_ statusCode: UInt32, _ headers: [HTTPHeader]) -> Void - public typealias OnIncomingHeadersBlockDone = (_ stream: HTTPStream, - _ headerBlock: HTTPHeaderBlock) -> Void - public typealias OnIncomingBody = (_ stream: HTTPStream, _ bodyChunk: Data) -> Void - public typealias OnStreamComplete = (_ stream: HTTPStream, _ error: CRTError?) -> Void - - let request: HTTPRequest - public let onIncomingHeaders: OnIncomingHeaders - public let onIncomingHeadersBlockDone: OnIncomingHeadersBlockDone + /// Callback to receive main headers + public typealias OnResponse = (_ statusCode: UInt32, + _ headers: [HTTPHeader]) -> Void + /// Callback to receive the incoming body + public typealias OnIncomingBody = (_ bodyChunk: Data) -> Void + /// Callback to receive trailer headers + public typealias OnTrailer = (_ headers: [HTTPHeader]) -> Void + /// Callback to know when request is completed, whether successful or unsuccessful + public typealias OnStreamComplete = (_ result: Result) -> Void + + /// Outgoing request. + let request: HTTPRequestBase + + /// Invoked 0+ times if informational 1xx interim responses are received. + public let onInterimResponse: OnInterimResponse? + + /// Invoked when main response headers are received. + public let onResponse: OnResponse + + /// Invoked repeatedly as body data is received. public let onIncomingBody: OnIncomingBody + + /// Invoked when trailer headers are received. + public let onTrailer: OnTrailer? + + /// Invoked when request/response stream is complete, whether successful or unsuccessful public let onStreamComplete: OnStreamComplete - public init(request: HTTPRequest, - onIncomingHeaders: @escaping OnIncomingHeaders, - onIncomingHeadersBlockDone: @escaping OnIncomingHeadersBlockDone, + /// When using HTTP/2, set http2ManualDataWrites to true to specify that request body data will be provided over time. + /// The stream will only be polled for writing when data has been supplied via `HTTP2Stream.writeData` + public var http2ManualDataWrites: Bool = false + + public init(request: HTTPRequestBase, + onInterimResponse: OnInterimResponse? = nil, + onResponse: @escaping OnResponse, onIncomingBody: @escaping OnIncomingBody, - onStreamComplete: @escaping OnStreamComplete) { + onTrailer: OnTrailer? = nil, + onStreamComplete: @escaping OnStreamComplete, + http2ManualDataWrites: Bool = false) { self.request = request - self.onIncomingHeaders = onIncomingHeaders - self.onIncomingHeadersBlockDone = onIncomingHeadersBlockDone + self.onInterimResponse = onInterimResponse + self.onResponse = onResponse + self.onTrailer = onTrailer self.onIncomingBody = onIncomingBody self.onStreamComplete = onStreamComplete + self.http2ManualDataWrites = http2ManualDataWrites } } diff --git a/Source/AwsCommonRuntimeKit/http/HTTPStream.swift b/Source/AwsCommonRuntimeKit/http/HTTPStream.swift index 51d3acd0f..79405f22c 100644 --- a/Source/AwsCommonRuntimeKit/http/HTTPStream.swift +++ b/Source/AwsCommonRuntimeKit/http/HTTPStream.swift @@ -1,26 +1,18 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0. import AwsCHttp +import Foundation +/// An base class that represents a single Http Request/Response for both HTTP/1.1 and HTTP/2. +/// Can be used to update the Window size, and get status code. public class HTTPStream { let rawValue: UnsafeMutablePointer var callbackData: HTTPStreamCallbackCore - /// Stream keeps a reference to HttpConnection to keep it alive - private let httpConnection: HTTPClientConnection - - // Called by HTTPClientConnection - init( - httpConnection: HTTPClientConnection, - options: aws_http_make_request_options, - callbackData: HTTPStreamCallbackCore) throws { + init(rawValue: UnsafeMutablePointer, + callbackData: HTTPStreamCallbackCore) { self.callbackData = callbackData - guard let rawValue = withUnsafePointer( - to: options, { aws_http_connection_make_request(httpConnection.rawValue, $0) }) else { - throw CommonRunTimeError.crtError(.makeFromLastError()) - } self.rawValue = rawValue - self.httpConnection = httpConnection } /// Opens the Sliding Read/Write Window by the number of bytes passed as an argument for this HTTPStream. @@ -43,12 +35,9 @@ public class HTTPStream { return Int(status) } - // TODO: make it thread safe /// Activates the client stream. public func activate() throws { - callbackData.stream = self if aws_http_stream_activate(rawValue) != AWS_OP_SUCCESS { - callbackData.stream = nil throw CommonRunTimeError.crtError(.makeFromLastError()) } } diff --git a/Source/AwsCommonRuntimeKit/http/HTTPStreamCallbackCore.swift b/Source/AwsCommonRuntimeKit/http/HTTPStreamCallbackCore.swift index 70d24f782..2b9257003 100644 --- a/Source/AwsCommonRuntimeKit/http/HTTPStreamCallbackCore.swift +++ b/Source/AwsCommonRuntimeKit/http/HTTPStreamCallbackCore.swift @@ -9,8 +9,8 @@ import Foundation /// You have to balance the retain & release calls in all cases to avoid leaking memory. class HTTPStreamCallbackCore { let requestOptions: HTTPRequestOptions - var stream: HTTPStream? - + // buffered list of headers + var headers: [HTTPHeader] = [HTTPHeader]() init(requestOptions: HTTPRequestOptions) { self.requestOptions = requestOptions } @@ -33,6 +33,7 @@ class HTTPStreamCallbackCore { options.on_complete = onComplete options.on_destroy = onDestroy options.user_data = getRetainedSelf() + options.http2_use_manual_data_writes = requestOptions.http2ManualDataWrites return options } @@ -50,14 +51,9 @@ private func onResponseHeaders(stream: UnsafeMutablePointer?, let httpStreamCbData = Unmanaged .fromOpaque(userData) .takeUnretainedValue() - let headers = UnsafeBufferPointer( + UnsafeBufferPointer( start: headerArray, - count: headersCount).map { HTTPHeader(rawValue: $0) } - - let stream = httpStreamCbData.stream! - httpStreamCbData.requestOptions.onIncomingHeaders(stream, - HTTPHeaderBlock(rawValue: headerBlock), - headers) + count: headersCount).forEach { httpStreamCbData.headers.append(HTTPHeader(rawValue: $0)) } return AWS_OP_SUCCESS } @@ -65,9 +61,25 @@ private func onResponseHeaderBlockDone(stream: UnsafeMutablePointer Int32 { let httpStreamCbData = Unmanaged.fromOpaque(userData).takeUnretainedValue() - let stream = httpStreamCbData.stream! + var status: Int32 = 0 + guard aws_http_stream_get_incoming_response_status(stream!, &status) == AWS_OP_SUCCESS else { + fatalError( + """ + Failed to get HTTP status code in onResponseHeaderBlockDone callback with error + \(CommonRunTimeError.crtError(.makeFromLastError())) + """ + ) + } + switch HTTPHeaderBlock(rawValue: headerBlock) { + case .informational: + httpStreamCbData.requestOptions.onInterimResponse?(UInt32(status), httpStreamCbData.headers) + case .main: + httpStreamCbData.requestOptions.onResponse(UInt32(status), httpStreamCbData.headers) + case .trailing: + httpStreamCbData.requestOptions.onTrailer?(httpStreamCbData.headers) + } - httpStreamCbData.requestOptions.onIncomingHeadersBlockDone(stream, HTTPHeaderBlock(rawValue: headerBlock)) + httpStreamCbData.headers.removeAll() return AWS_OP_SUCCESS } @@ -82,7 +94,7 @@ private func onResponseBody(stream: UnsafeMutablePointer?, let incomingBodyFn = httpStreamCbData.requestOptions.onIncomingBody let callbackBytes = Data(bytesNoCopy: bufPtr, count: bufLen, deallocator: .none) - incomingBodyFn((httpStreamCbData.stream)!, callbackBytes) + incomingBodyFn(callbackBytes) return AWS_OP_SUCCESS } @@ -91,11 +103,22 @@ private func onComplete(stream: UnsafeMutablePointer?, userData: UnsafeMutableRawPointer!) { let httpStreamCbData = Unmanaged.fromOpaque(userData).takeUnretainedValue() - let stream = httpStreamCbData.stream! let onStreamCompleteFn = httpStreamCbData.requestOptions.onStreamComplete - let crtError = errorCode == AWS_OP_SUCCESS ? nil : CRTError(code: errorCode) - onStreamCompleteFn(stream, crtError) - httpStreamCbData.stream = nil + guard errorCode == AWS_OP_SUCCESS else { + onStreamCompleteFn(.failure(CommonRunTimeError.crtError(CRTError(code: errorCode)))) + return + } + + var status: Int32 = 0 + guard aws_http_stream_get_incoming_response_status(stream!, &status) == AWS_OP_SUCCESS else { + fatalError( + """ + Failed to get HTTP status code in onComplete callback with error + \(CommonRunTimeError.crtError(.makeFromLastError())) + """ + ) + } + onStreamCompleteFn(.success(UInt32(status))) } private func onDestroy(userData: UnsafeMutableRawPointer!) { diff --git a/Source/AwsCommonRuntimeKit/http/HTTPVersion.swift b/Source/AwsCommonRuntimeKit/http/HTTPVersion.swift new file mode 100644 index 000000000..880f190b5 --- /dev/null +++ b/Source/AwsCommonRuntimeKit/http/HTTPVersion.swift @@ -0,0 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0. +import AwsCHttp + +/// HTTPVersion of a Connection. +public enum HTTPVersion { + case unknown // Invalid version + case version_1_1 + case version_2 +} + +extension HTTPVersion: RawRepresentable, CaseIterable { + + public init(rawValue: aws_http_version) { + let value = Self.allCases.first(where: {$0.rawValue == rawValue}) + self = value ?? .unknown + } + public var rawValue: aws_http_version { + switch self { + case .unknown: return AWS_HTTP_VERSION_UNKNOWN + case .version_1_1: return AWS_HTTP_VERSION_1_1 + case .version_2: return AWS_HTTP_VERSION_2 + } + } +} diff --git a/Source/AwsCommonRuntimeKit/io/StreamCore.swift b/Source/AwsCommonRuntimeKit/io/StreamCore.swift index e770a6ae0..1b403157f 100644 --- a/Source/AwsCommonRuntimeKit/io/StreamCore.swift +++ b/Source/AwsCommonRuntimeKit/io/StreamCore.swift @@ -6,7 +6,7 @@ import AwsCCommon /// aws_input_stream has acquire and release functions which manage the lifetime of this object. class IStreamCore { - var rawValue: aws_input_stream + var rawValue: UnsafeMutablePointer let iStreamable: IStreamable var isEndOfStream: Bool = false private let allocator: Allocator @@ -23,16 +23,17 @@ class IStreamCore { init(iStreamable: IStreamable, allocator: Allocator) { self.allocator = allocator self.iStreamable = iStreamable - rawValue = aws_input_stream() + rawValue = allocator.allocate(capacity: 1) // Use a manually managed vtable pointer to avoid undefined behavior self.vtablePointer = allocator.allocate(capacity: 1) vtablePointer.initialize(to: vtable) - rawValue.vtable = UnsafePointer(vtablePointer) + rawValue.pointee.vtable = UnsafePointer(vtablePointer) - rawValue.impl = Unmanaged.passUnretained(self).toOpaque() + rawValue.pointee.impl = Unmanaged.passUnretained(self).toOpaque() } deinit { + allocator.release(rawValue) allocator.release(vtablePointer) } } diff --git a/Source/AwsCommonRuntimeKit/io/TLSContextOptions.swift b/Source/AwsCommonRuntimeKit/io/TLSContextOptions.swift index 13dbf8377..faf2c90ad 100644 --- a/Source/AwsCommonRuntimeKit/io/TLSContextOptions.swift +++ b/Source/AwsCommonRuntimeKit/io/TLSContextOptions.swift @@ -58,6 +58,10 @@ public class TLSContextOptions: CStruct { aws_tls_ctx_options_set_verify_peer(rawValue, verifyPeer) } + public func setMinimumTLSVersion(_ tlsVersion: TLSVersion) { + aws_tls_ctx_options_set_minimum_tls_version(rawValue, aws_tls_versions(rawValue: tlsVersion.rawValue)) + } + typealias RawType = aws_tls_ctx_options func withCStruct(_ body: (aws_tls_ctx_options) -> Result) -> Result { return body(rawValue.pointee) @@ -68,3 +72,12 @@ public class TLSContextOptions: CStruct { allocator.release(rawValue) } } + +public enum TLSVersion: UInt32 { + case SSLv3 = 0 + case TLSv1 = 1 + case TLSv1_1 = 2 + case TLSv1_2 = 3 + case TLSv1_3 = 4 + case systemDefault = 128 +} diff --git a/Source/Elasticurl/Elasticurl.swift b/Source/Elasticurl/Elasticurl.swift index 9313e20e4..7243761ce 100644 --- a/Source/Elasticurl/Elasticurl.swift +++ b/Source/Elasticurl/Elasticurl.swift @@ -280,22 +280,18 @@ struct Elasticurl { } httpRequest.addHeaders(headers: headers) - let onIncomingHeaders: HTTPRequestOptions.OnIncomingHeaders = { _, _, headers in + let onResponse: HTTPRequestOptions.OnResponse = { _, headers in for header in headers { print(header.name + " : " + header.value) } } - let onBody: HTTPRequestOptions.OnIncomingBody = { _, bodyChunk in + let onBody: HTTPRequestOptions.OnIncomingBody = { bodyChunk in writeData(data: bodyChunk) } - let onBlockDone: HTTPRequestOptions.OnIncomingHeadersBlockDone = { _, _ in - - } - - let onComplete: HTTPRequestOptions.OnStreamComplete = { _, error in - print(error?.message ?? "Success") + let onComplete: HTTPRequestOptions.OnStreamComplete = { result in + print(result) semaphore.signal() } @@ -313,8 +309,7 @@ struct Elasticurl { do { let connection = try await connectionManager.acquireConnection() let requestOptions = HTTPRequestOptions(request: httpRequest, - onIncomingHeaders: onIncomingHeaders, - onIncomingHeadersBlockDone: onBlockDone, + onResponse: onResponse, onIncomingBody: onBody, onStreamComplete: onComplete) stream = try connection.makeRequest(requestOptions: requestOptions) diff --git a/Test/AwsCommonRuntimeKitTests/auth/CredentialsProviderTests.swift b/Test/AwsCommonRuntimeKitTests/auth/CredentialsProviderTests.swift index e9ea9534b..460d472e2 100644 --- a/Test/AwsCommonRuntimeKitTests/auth/CredentialsProviderTests.swift +++ b/Test/AwsCommonRuntimeKitTests/auth/CredentialsProviderTests.swift @@ -92,7 +92,7 @@ class CredentialsProviderTests: XCBaseTestCase { wait(for: [exceptionWasThrown], timeout: 15) } - func testCreateCredentialsProviderEnv() async throws { + func withEnvironmentCredentialsClosure(closure: () async throws -> T) async rethrows -> T { setenv("AWS_ACCESS_KEY_ID", accessKey, 1) setenv("AWS_SECRET_ACCESS_KEY", secret, 1) setenv("AWS_SESSION_TOKEN", sessionToken, 1) @@ -101,16 +101,23 @@ class CredentialsProviderTests: XCBaseTestCase { unsetenv("AWS_SECRET_ACCESS_KEY") unsetenv("AWS_SESSION_TOKEN") } - let provider = try CredentialsProvider(source: .environment()) - let credentials = try await provider.getCredentials() - assertCredentials(credentials: credentials) + return try await closure() + } + func testCreateCredentialsProviderEnv() async throws { + try await withEnvironmentCredentialsClosure { + let provider = try CredentialsProvider(source: .environment()) + let credentials = try await provider.getCredentials() + XCTAssertNotNil(credentials) + assertCredentials(credentials: credentials) + } } func testCreateCredentialsProviderProfile() async throws { do { - - let provider = try CredentialsProvider(source: .profile(configFileNameOverride: Bundle.module.path(forResource: "example_config", ofType: "txt")!, + let provider = try CredentialsProvider(source: .profile( + bootstrap: getClientBootstrap(), + configFileNameOverride: Bundle.module.path(forResource: "example_config", ofType: "txt")!, credentialsFileNameOverride: Bundle.module.path(forResource: "example_profile", ofType: "txt")!, shutdownCallback: getShutdownCallback()), allocator: allocator) @@ -147,15 +154,18 @@ class CredentialsProviderTests: XCBaseTestCase { wait(for: [shutdownWasCalled], timeout: 15) } - func testCreateAWSCredentialsProviderChain() async throws { + func testCreateAWSCredentialsProviderDefaultChain() async throws { try skipIfLinux() do { - let provider = try CredentialsProvider(source: .defaultChain(bootstrap: getClientBootstrap(), - shutdownCallback: getShutdownCallback()), - allocator: allocator) - - let credentials = try await provider.getCredentials() - XCTAssertNotNil(credentials) + try await withEnvironmentCredentialsClosure { + let provider = try CredentialsProvider(source: .defaultChain(bootstrap: getClientBootstrap(), + shutdownCallback: getShutdownCallback()), + allocator: allocator) + + let credentials = try await provider.getCredentials() + XCTAssertNotNil(credentials) + assertCredentials(credentials: credentials) + } } wait(for: [shutdownWasCalled], timeout: 15) } diff --git a/Test/AwsCommonRuntimeKitTests/auth/SignerTests.swift b/Test/AwsCommonRuntimeKitTests/auth/SignerTests.swift index 3951e4671..7397d4a98 100644 --- a/Test/AwsCommonRuntimeKitTests/auth/SignerTests.swift +++ b/Test/AwsCommonRuntimeKitTests/auth/SignerTests.swift @@ -42,6 +42,34 @@ class SignerTests: XCBaseTestCase { XCTAssert(headers.contains(where: { $0.name == "Host" && $0.value == SIGV4TEST_HOST })) } + func testHTTP2SigningSigv4Headers() async throws { + let request = try makeMockHTTP2RequestWithDoNotSignHeader() + let provider = try makeMockCredentialsProvider() + let shouldSignHeader: (String) -> Bool = { name in + return !name.starts(with: "doNotSign") + } + let config = SigningConfig( + algorithm: SigningAlgorithmType.signingV4, + signatureType: SignatureType.requestHeaders, + service: SIGV4TEST_SERVICE, + region: SIGV4TEST_REGION, + date: getDate(), + credentialsProvider: provider, + shouldSignHeader: shouldSignHeader) + + let signedRequest = try await Signer.signRequest(request: request, + config: config, + allocator: allocator) + XCTAssertNotNil(signedRequest) + let headers = signedRequest.getHeaders() + XCTAssert(headers.contains(where: { + $0.name == "Authorization" + && $0.value.starts(with: "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=:authority;:method;:path;:scheme;x-amz-date, Signature=") + })) + XCTAssert(headers.contains(where: { $0.name == "X-Amz-Date" })) + XCTAssert(headers.contains(where: { $0.name == ":authority" && $0.value == SIGV4TEST_HOST })) + } + func testSigningSigv4HeadersWithCredentials() async throws { let request = try makeMockRequest() let credentials = try makeMockCredentials() @@ -144,8 +172,18 @@ class SignerTests: XCBaseTestCase { return request } + func makeMockHTTP2RequestWithDoNotSignHeader() throws -> HTTP2Request { + let request = try HTTP2Request(allocator: allocator) + request.addHeader(header: HTTPHeader(name: ":method", value: "GET")) + request.addHeader(header: HTTPHeader(name: ":path", value: "/")) + request.addHeader(header: HTTPHeader(name: ":scheme", value: "https")) + request.addHeader(header: HTTPHeader(name: ":authority", value: SIGV4TEST_HOST)) + request.addHeader(header: HTTPHeader(name: "doNotSign", value: "test-header")) + return request + } + func makeMockRequestWithDoNotSignHeader() throws -> HTTPRequest { - let request = try HTTPRequest() + let request = try HTTPRequest(allocator: allocator) request.addHeader(header: HTTPHeader(name: "Host", value: SIGV4TEST_HOST)) request.addHeader(header: HTTPHeader(name: "doNotSign", value: "test-header")) return request diff --git a/Test/AwsCommonRuntimeKitTests/crt/UtilityTests.swift b/Test/AwsCommonRuntimeKitTests/crt/UtilityTests.swift index 69e50fbcc..cd8e15857 100644 --- a/Test/AwsCommonRuntimeKitTests/crt/UtilityTests.swift +++ b/Test/AwsCommonRuntimeKitTests/crt/UtilityTests.swift @@ -47,7 +47,7 @@ class UtilityTests: XCBaseTestCase { aws_array_list_clean_up(list) allocator.release(list) } - let init_size: size_t = 4; + let init_size: size_t = 4 "first".withByteCursorPointer { firstCursorPointer in aws_array_list_init_dynamic(list, allocator.rawValue, init_size, MemoryLayout.size(ofValue: firstCursorPointer.pointee)) diff --git a/Test/AwsCommonRuntimeKitTests/http/HTTP2ClientConnectionTests.swift b/Test/AwsCommonRuntimeKitTests/http/HTTP2ClientConnectionTests.swift new file mode 100644 index 000000000..dcb7a1a43 --- /dev/null +++ b/Test/AwsCommonRuntimeKitTests/http/HTTP2ClientConnectionTests.swift @@ -0,0 +1,184 @@ +//// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +//// SPDX-License-Identifier: Apache-2.0. +import XCTest +@testable import AwsCommonRuntimeKit + +class HTTP2ClientConnectionTests: HTTPClientTestFixture { + + let expectedVersion = HTTPVersion.version_2 + + func testGetHTTP2RequestVersion() async throws { + let connectionManager = try await getHttpConnectionManager(endpoint: "httpbin.org", alpnList: ["h2","http/1.1"]) + let connection = try await connectionManager.acquireConnection() + XCTAssertEqual(connection.httpVersion, HTTPVersion.version_2) + } + + // Test that the binding works not the actual functionality. C part has tests for functionality + func testHTTP2UpdateSetting() async throws { + let connectionManager = try await getHttpConnectionManager(endpoint: "httpbin.org", alpnList: ["h2","http/1.1"]) + let connection = try await connectionManager.acquireConnection() + if let connection = connection as? HTTP2ClientConnection { + try await connection.updateSetting(setting: HTTP2Settings(enablePush: false)) + } else { + XCTFail("Connection is not HTTP2") + } + } + + // Test that the binding works not the actual functionality. C part has tests for functionality + func testHTTP2UpdateSettingEmpty() async throws { + let connectionManager = try await getHttpConnectionManager(endpoint: "httpbin.org", alpnList: ["h2","http/1.1"]) + let connection = try await connectionManager.acquireConnection() + if let connection = connection as? HTTP2ClientConnection { + try await connection.updateSetting(setting: HTTP2Settings()) + } else { + XCTFail("Connection is not HTTP2") + } + } + + // Test that the binding works not the actual functionality. C part has tests for functionality + func testHTTP2SendPing() async throws { + let connectionManager = try await getHttpConnectionManager(endpoint: "httpbin.org", alpnList: ["h2","http/1.1"]) + let connection = try await connectionManager.acquireConnection() + if let connection = connection as? HTTP2ClientConnection { + var time = try await connection.sendPing() + XCTAssertTrue(time > 0) + time = try await connection.sendPing(data: "12345678".data(using: .utf8)!) + XCTAssertTrue(time > 0) + } else { + XCTFail("Connection is not HTTP2") + } + } + + // Test that the binding works not the actual functionality. C part has tests for functionality + func testHTTP2SendGoAway() async throws { + let connectionManager = try await getHttpConnectionManager(endpoint: "httpbin.org", alpnList: ["h2","http/1.1"]) + let connection = try await connectionManager.acquireConnection() + if let connection = connection as? HTTP2ClientConnection { + connection.sendGoAway(error: .internalError, allowMoreStreams: false) + } else { + XCTFail("Connection is not HTTP2") + } + } + + func testGetHttpsRequest() async throws { + let connectionManager = try await getHttpConnectionManager(endpoint: "httpbin.org", alpnList: ["h2","http/1.1"]) + let response = try await sendHTTPRequest( + method: "GET", + endpoint: "httpbin.org", + path: "/get", + connectionManager: connectionManager, + expectedVersion: expectedVersion, + requestVersion: .version_2) + // The first header of response has to be ":status" for HTTP/2 response + XCTAssertEqual(response.headers[0].name, ":status") + let response2 = try await sendHTTPRequest( + method: "GET", + endpoint: "httpbin.org", + path: "/delete", + expectedStatus: 405, + connectionManager: connectionManager, + expectedVersion: expectedVersion, + requestVersion: .version_2) + XCTAssertEqual(response2.headers[0].name, ":status") + } + + + func testGetHttpsRequestWithHTTP1_1Request() async throws { + let connectionManager = try await getHttpConnectionManager(endpoint: "httpbin.org", alpnList: ["h2","http/1.1"]) + let response = try await sendHTTPRequest( + method: "GET", + endpoint: "httpbin.org", + path: "/get", + connectionManager: connectionManager, + expectedVersion: expectedVersion, + requestVersion: .version_1_1) + // The first header of response has to be ":status" for HTTP/2 response + XCTAssertEqual(response.headers[0].name, ":status") + let response2 = try await sendHTTPRequest( + method: "GET", + endpoint: "httpbin.org", + path: "/delete", + expectedStatus: 405, + connectionManager: connectionManager, + expectedVersion: expectedVersion, + requestVersion: .version_1_1) + XCTAssertEqual(response2.headers[0].name, ":status") + } + + func testHTTP2Download() async throws { + let connectionManager = try await getHttpConnectionManager(endpoint: "d1cz66xoahf9cl.cloudfront.net", alpnList: ["h2","http/1.1"]) + let response = try await sendHTTPRequest( + method: "GET", + endpoint: "d1cz66xoahf9cl.cloudfront.net", + path: "/http_test_doc.txt", + connectionManager: connectionManager, + expectedVersion: expectedVersion, + requestVersion: .version_2) + let actualSha = try response.body.sha256() + XCTAssertEqual( + actualSha.encodeToHexString().uppercased(), + "C7FDB5314B9742467B16BD5EA2F8012190B5E2C44A005F7984F89AAB58219534") + } + + func testHTTP2DownloadWithHTTP1_1Request() async throws { + let connectionManager = try await getHttpConnectionManager(endpoint: "d1cz66xoahf9cl.cloudfront.net", alpnList: ["h2","http/1.1"]) + let response = try await sendHTTPRequest( + method: "GET", + endpoint: "d1cz66xoahf9cl.cloudfront.net", + path: "/http_test_doc.txt", + connectionManager: connectionManager, + expectedVersion: expectedVersion, + requestVersion: .version_1_1) + let actualSha = try response.body.sha256() + XCTAssertEqual( + actualSha.encodeToHexString().uppercased(), + "C7FDB5314B9742467B16BD5EA2F8012190B5E2C44A005F7984F89AAB58219534") + } + + func testHTTP2StreamUpload() async throws { + let connectionManager = try await getHttpConnectionManager(endpoint: "nghttp2.org", alpnList: ["h2"]) + let semaphore = DispatchSemaphore(value: 0) + var httpResponse = HTTPResponse() + var onCompleteCalled = false + let testBody = "testBody" + let http2RequestOptions = try getHTTP2RequestOptions( + method: "PUT", + path: "/httpbin/put", + authority: "nghttp2.org", + body: testBody, + response: &httpResponse, + semaphore: semaphore, + onComplete: { _ in + onCompleteCalled = true + }, + http2ManualDataWrites: true) + let connection = try await connectionManager.acquireConnection() + let streamBase = try connection.makeRequest(requestOptions: http2RequestOptions) + let stream = streamBase as! HTTP2Stream + try stream.activate() + XCTAssertFalse(onCompleteCalled) + let data = TEST_DOC_LINE.data(using: .utf8)! + for chunk in data.chunked(into: 5) { + try await stream.writeData(data: chunk, endOfStream: false) + XCTAssertFalse(onCompleteCalled) + } + + XCTAssertFalse(onCompleteCalled) + // Sleep for 5 seconds to make sure onComplete is not triggerred until endOfStream is true + try await Task.sleep(nanoseconds: 5_000_000_000) + XCTAssertFalse(onCompleteCalled) + try await stream.writeData(data: Data(), endOfStream: true) + semaphore.wait() + XCTAssertTrue(onCompleteCalled) + XCTAssertNil(httpResponse.error) + XCTAssertEqual(httpResponse.statusCode, 200) + + // Parse json body + struct Response: Codable { + let data: String + } + + let body: Response = try! JSONDecoder().decode(Response.self, from: httpResponse.body) + XCTAssertEqual(body.data, testBody + TEST_DOC_LINE) + } +} diff --git a/Test/AwsCommonRuntimeKitTests/http/HTTP2SettingsTests.swift b/Test/AwsCommonRuntimeKitTests/http/HTTP2SettingsTests.swift new file mode 100644 index 000000000..ff9251fbf --- /dev/null +++ b/Test/AwsCommonRuntimeKitTests/http/HTTP2SettingsTests.swift @@ -0,0 +1,40 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0. + +import XCTest +@testable import AwsCommonRuntimeKit +import AwsCHttp + +class HTTP2SettingsTests: XCBaseTestCase { + + func testCreateHTTP2Settings() throws { + let settings = HTTP2Settings( + headerTableSize: 10, + enablePush: false, + maxConcurrentStreams: 20, + initialWindowSize: 30, + maxFrameSize: 40, + maxHeaderListSize: 50) + settings.withCStruct { cSettingList in + XCTAssertEqual(cSettingList.count, 6) + for cSetting in cSettingList { + switch cSetting.id { + case AWS_HTTP2_SETTINGS_HEADER_TABLE_SIZE: + XCTAssertEqual(cSetting.value, 10) + case AWS_HTTP2_SETTINGS_ENABLE_PUSH: + XCTAssertEqual(cSetting.value, 0) + case AWS_HTTP2_SETTINGS_MAX_CONCURRENT_STREAMS: + XCTAssertEqual(cSetting.value, 20) + case AWS_HTTP2_SETTINGS_INITIAL_WINDOW_SIZE: + XCTAssertEqual(cSetting.value, 30) + case AWS_HTTP2_SETTINGS_MAX_FRAME_SIZE: + XCTAssertEqual(cSetting.value, 40) + case AWS_HTTP2_SETTINGS_MAX_HEADER_LIST_SIZE: + XCTAssertEqual(cSetting.value, 50) + default: + XCTFail("Unexpected case found") + } + } + } + } +} diff --git a/Test/AwsCommonRuntimeKitTests/http/HTTP2StreamManagerTests.swift b/Test/AwsCommonRuntimeKitTests/http/HTTP2StreamManagerTests.swift new file mode 100644 index 000000000..365e5e0c5 --- /dev/null +++ b/Test/AwsCommonRuntimeKitTests/http/HTTP2StreamManagerTests.swift @@ -0,0 +1,187 @@ +//// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +//// SPDX-License-Identifier: Apache-2.0. + +import XCTest +@testable import AwsCommonRuntimeKit + +class HTT2StreamManagerTests: HTTPClientTestFixture { + let endpoint = "d1cz66xoahf9cl.cloudfront.net"; // Use cloudfront for HTTP/2 + let path = "/random_32_byte.data"; + + func testStreamManagerCreate() throws { + let tlsContextOptions = TLSContextOptions(allocator: allocator) + let tlsContext = try TLSContext(options: tlsContextOptions, mode: .client, allocator: allocator) + let tlsConnectionOptions = TLSConnectionOptions(context: tlsContext, allocator: allocator) + let elg = try EventLoopGroup(threadCount: 1, allocator: allocator) + let hostResolver = try HostResolver(eventLoopGroup: elg, maxHosts: 8, maxTTL: 30, allocator: allocator) + let bootstrap = try ClientBootstrap(eventLoopGroup: elg, + hostResolver: hostResolver, + allocator: allocator) + let port = UInt16(443) + + let options = HTTP2StreamManagerOptions( + clientBootstrap: bootstrap, + hostName: endpoint, + port: port, + maxConnections: 30, + proxyOptions: HTTPProxyOptions(hostName: "localhost", port: 80), + proxyEnvSettings: HTTPProxyEnvSettings(proxyConnectionType: HTTPProxyConnectionType.forward), + socketOptions: SocketOptions(socketType: .stream), + tlsOptions: tlsConnectionOptions, + monitoringOptions: HTTPMonitoringOptions(minThroughputBytesPerSecond: 10, allowableThroughputFailureInterval: 20), + enableStreamManualWindowManagement: true, + shutdownCallback: {}, + priorKnowledge: true, + initialSettings: HTTP2Settings(enablePush: true), + maxClosedStreams: 40, + enableConnectionManualWindowManagement: true, + closeConnectionOnServerError: true, + connectionPingPeriodMs: 50, + connectionPingTimeoutMs: 60, + idealConcurrentStreamsPerConnection: 70, + maxConcurrentStreamsPerConnection: 80) + let shutdownCallbackCore = ShutdownCallbackCore(options.shutdownCallback) + let shutdownOptions = shutdownCallbackCore.getRetainedShutdownOptions() + options.withCStruct(shutdownOptions: shutdownOptions) {cOptions in + XCTAssertNotNil(cOptions.bootstrap) + XCTAssertNotNil(cOptions.socket_options) + XCTAssertNotNil(cOptions.tls_connection_options) + XCTAssertTrue(cOptions.http2_prior_knowledge) + XCTAssertEqual(cOptions.host.toString(), endpoint) + XCTAssertEqual(cOptions.port, port) + XCTAssertNotNil(cOptions.initial_settings_array) + XCTAssertEqual(cOptions.num_initial_settings, 1) + XCTAssertEqual(cOptions.max_closed_streams, 40) + XCTAssertTrue(cOptions.conn_manual_window_management) + XCTAssertTrue(cOptions.enable_read_back_pressure) + XCTAssertNotNil(cOptions.monitoring_options) + XCTAssertNotNil(cOptions.proxy_options) + XCTAssertNotNil(cOptions.proxy_ev_settings) + XCTAssertNotNil(cOptions.shutdown_complete_user_data) + XCTAssertNotNil(cOptions.shutdown_complete_callback) + XCTAssertTrue(cOptions.close_connection_on_server_error) + XCTAssertEqual(cOptions.connection_ping_period_ms, 50) + XCTAssertEqual(cOptions.connection_ping_timeout_ms, 60) + XCTAssertEqual(cOptions.ideal_concurrent_streams_per_connection, 70) + XCTAssertEqual(cOptions.max_concurrent_streams_per_connection, 80) + XCTAssertEqual(cOptions.max_connections, 30) + } + shutdownCallbackCore.release() + } + + func makeStreamManger(host: String, port: Int = 443) throws -> HTTP2StreamManager { + let tlsContextOptions = TLSContextOptions(allocator: allocator) + tlsContextOptions.setAlpnList(["h2"]) + let tlsContext = try TLSContext(options: tlsContextOptions, mode: .client, allocator: allocator) + + var tlsConnectionOptions = TLSConnectionOptions(context: tlsContext, allocator: allocator) + + tlsConnectionOptions.serverName = host + + let elg = try EventLoopGroup(threadCount: 1, allocator: allocator) + let hostResolver = try HostResolver(eventLoopGroup: elg, maxHosts: 8, maxTTL: 30, allocator: allocator) + + let bootstrap = try ClientBootstrap(eventLoopGroup: elg, + hostResolver: hostResolver, + allocator: allocator) + + let socketOptions = SocketOptions(socketType: .stream) + let port = UInt16(443) + let streamManager = try HTTP2StreamManager( + options: HTTP2StreamManagerOptions( + clientBootstrap: bootstrap, + hostName: host, + port: UInt16(port), + maxConnections: 5, + socketOptions: socketOptions, + tlsOptions: tlsConnectionOptions)) + return streamManager + } + + func testCanCreateConnectionManager() throws { + _ = try makeStreamManger(host: endpoint) + } + + func testHTTP2Stream() async throws { + let streamManager = try makeStreamManger(host: endpoint) + _ = try await sendHTTP2Request(method: "GET", path: path, authority: endpoint, streamManager: streamManager) + } + + func testHTTP2StreamUpload() async throws { + let streamManager = try makeStreamManger(host: "nghttp2.org") + let semaphore = DispatchSemaphore(value: 0) + var httpResponse = HTTPResponse() + var onCompleteCalled = false + let testBody = "testBody" + let http2RequestOptions = try getHTTP2RequestOptions( + method: "PUT", + path: "/httpbin/put", + authority: "nghttp2.org", + body: testBody, + response: &httpResponse, + semaphore: semaphore, + onComplete: { _ in + onCompleteCalled = true + }, + http2ManualDataWrites: true) + + let stream = try await streamManager.acquireStream(requestOptions: http2RequestOptions) + XCTAssertFalse(onCompleteCalled) + let data = TEST_DOC_LINE.data(using: .utf8)! + for chunk in data.chunked(into: 5) { + try await stream.writeData(data: chunk, endOfStream: false) + XCTAssertFalse(onCompleteCalled) + } + + XCTAssertFalse(onCompleteCalled) + // Sleep for 5 seconds to make sure onComplete is not triggerred until endOfStream is true + try await Task.sleep(nanoseconds: 5_000_000_000) + XCTAssertFalse(onCompleteCalled) + try await stream.writeData(data: Data(), endOfStream: true) + semaphore.wait() + XCTAssertTrue(onCompleteCalled) + XCTAssertNil(httpResponse.error) + XCTAssertEqual(httpResponse.statusCode, 200) + + // Parse json body + struct Response: Codable { + let data: String + } + + let body: Response = try! JSONDecoder().decode(Response.self, from: httpResponse.body) + XCTAssertEqual(body.data, testBody + TEST_DOC_LINE) + } + + // Test that the binding works not the actual functionality. C part has tests for functionality + func testHTTP2StreamReset() async throws { + let streamManager = try makeStreamManger(host: endpoint) + let http2RequestOptions = try getHTTP2RequestOptions( + method: "PUT", + path: "/httpbin/put", + authority: "nghttp2.org") + + let stream = try await streamManager.acquireStream(requestOptions: http2RequestOptions) + try stream.resetStream(error: HTTP2Error.internalError) + } + + func testHTTP2ParallelStreams() async throws { + try await testHTTP2ParallelStreams(count: 10) + } + + func testHTTP2ParallelStreams(count: Int) async throws { + let streamManager = try makeStreamManger(host: "nghttp2.org") + let requestCompleteExpectation = XCTestExpectation(description: "Request was completed successfully") + requestCompleteExpectation.expectedFulfillmentCount = count + await withTaskGroup(of: Void.self) { taskGroup in + for _ in 1...count { + taskGroup.addTask { + _ = try! await self.sendHTTP2Request(method: "GET", path: "/httpbin/get", authority: "nghttp2.org", streamManager: streamManager, onComplete: { _ in + requestCompleteExpectation.fulfill() + }) + } + } + } + wait(for: [requestCompleteExpectation], timeout: 15) + print("Request were successfully completed.") + } +} diff --git a/Test/AwsCommonRuntimeKitTests/http/HTTPClientConnectionOptionsTests.swift b/Test/AwsCommonRuntimeKitTests/http/HTTPClientConnectionOptionsTests.swift index 6a9a3c2bf..fcd69ce9b 100644 --- a/Test/AwsCommonRuntimeKitTests/http/HTTPClientConnectionOptionsTests.swift +++ b/Test/AwsCommonRuntimeKitTests/http/HTTPClientConnectionOptionsTests.swift @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0. import XCTest -import AwsCHttp @testable import AwsCommonRuntimeKit class HTTPClientConnectionOptionsTests: XCBaseTestCase { diff --git a/Test/AwsCommonRuntimeKitTests/http/HTTPClientTestFixture.swift b/Test/AwsCommonRuntimeKitTests/http/HTTPClientTestFixture.swift index d628a2587..b4457b030 100644 --- a/Test/AwsCommonRuntimeKitTests/http/HTTPClientTestFixture.swift +++ b/Test/AwsCommonRuntimeKitTests/http/HTTPClientTestFixture.swift @@ -3,41 +3,116 @@ import XCTest @testable import AwsCommonRuntimeKit -import AwsCHttp struct HTTPResponse { var statusCode: Int = -1 var headers: [HTTPHeader] = [HTTPHeader]() var body: Data = Data() + var error: CommonRunTimeError? + var version: HTTPVersion? } class HTTPClientTestFixture: XCBaseTestCase { - let semaphore = DispatchSemaphore(value: 0) + let TEST_DOC_LINE: String = """ + This is a sample to prove that http downloads and uploads work. + It doesn't really matter what's in here, + we mainly just need to verify the downloads and uploads work. + """ - func sendHttpRequest(method: String, + func sendHTTPRequest(method: String, endpoint: String, path: String = "/", - requestBody: String = "", + body: String = "", expectedStatus: Int = 200, connectionManager: HTTPClientConnectionManager, - numRetries: UInt = 2) async throws -> HTTPResponse { + expectedVersion: HTTPVersion = HTTPVersion.version_1_1, + requestVersion: HTTPVersion = HTTPVersion.version_1_1, + numRetries: UInt = 2, + onResponse: HTTPRequestOptions.OnResponse? = nil, + onBody: HTTPRequestOptions.OnIncomingBody? = nil, + onComplete: HTTPRequestOptions.OnStreamComplete? = nil) async throws -> HTTPResponse { + var httpResponse = HTTPResponse() - let httpRequestOptions = try getHTTPRequestOptions( - method: method, - endpoint: endpoint, - path: path, - body: requestBody, - response: &httpResponse) + let semaphore = DispatchSemaphore(value: 0) + + let httpRequestOptions: HTTPRequestOptions + if requestVersion == HTTPVersion.version_2 { + httpRequestOptions = try getHTTP2RequestOptions( + method: method, + path: path, + authority: endpoint, + body: body, + response: &httpResponse, + semaphore: semaphore, + onResponse: onResponse, + onBody: onBody, + onComplete: onComplete) + } else { + httpRequestOptions = try getHTTPRequestOptions( + method: method, + endpoint: endpoint, + path: path, + body: body, + response: &httpResponse, + semaphore: semaphore, + onResponse: onResponse, + onBody: onBody, + onComplete: onComplete) + } for i in 1...numRetries+1 where httpResponse.statusCode != expectedStatus { print("Attempt#\(i) to send an HTTP request") let connection = try await connectionManager.acquireConnection() XCTAssertTrue(connection.isOpen) + httpResponse.version = connection.httpVersion + XCTAssertEqual(connection.httpVersion, expectedVersion) let stream = try connection.makeRequest(requestOptions: httpRequestOptions) try stream.activate() semaphore.wait() } + XCTAssertNil(httpResponse.error) + XCTAssertEqual(httpResponse.statusCode, expectedStatus) + return httpResponse + } + + func sendHTTP2Request(method: String, + path: String, + scheme: String = "https", + authority: String, + body: String = "", + expectedStatus: Int = 200, + streamManager: HTTP2StreamManager, + numRetries: UInt = 2, + http2ManualDataWrites: Bool = false, + onResponse: HTTPRequestOptions.OnResponse? = nil, + onBody: HTTPRequestOptions.OnIncomingBody? = nil, + onComplete: HTTPRequestOptions.OnStreamComplete? = nil) async throws -> HTTPResponse { + + var httpResponse = HTTPResponse() + let semaphore = DispatchSemaphore(value: 0) + + let httpRequestOptions = try getHTTP2RequestOptions( + method: method, + path: path, + scheme: scheme, + authority: authority, + body: body, + response: &httpResponse, + semaphore: semaphore, + onResponse: onResponse, + onBody: onBody, + onComplete: onComplete, + http2ManualDataWrites: http2ManualDataWrites) + + for i in 1...numRetries+1 where httpResponse.statusCode != expectedStatus { + print("Attempt#\(i) to send an HTTP request") + let stream = try await streamManager.acquireStream(requestOptions: httpRequestOptions) + try stream.activate() + semaphore.wait() + } + + XCTAssertNil(httpResponse.error) XCTAssertEqual(httpResponse.statusCode, expectedStatus) return httpResponse } @@ -45,7 +120,7 @@ class HTTPClientTestFixture: XCBaseTestCase { func getHttpConnectionManager(endpoint: String, ssh: Bool = true, port: Int = 443, - alpnList: [String] = ["h2","http/1.1"], + alpnList: [String] = ["http/1.1"], proxyOptions: HTTPProxyOptions? = nil, monitoringOptions: HTTPMonitoringOptions? = nil, socketOptions: SocketOptions = SocketOptions(socketType: .stream)) async throws -> HTTPClientConnectionManager { @@ -71,6 +146,36 @@ class HTTPClientTestFixture: XCBaseTestCase { return try HTTPClientConnectionManager(options: httpClientOptions) } + func getRequestOptions(request: HTTPRequestBase, + response: UnsafeMutablePointer? = nil, + semaphore: DispatchSemaphore? = nil, + onResponse: HTTPRequestOptions.OnResponse? = nil, + onBody: HTTPRequestOptions.OnIncomingBody? = nil, + onComplete: HTTPRequestOptions.OnStreamComplete? = nil, + http2ManualDataWrites: Bool = false) -> HTTPRequestOptions { + HTTPRequestOptions(request: request, + onResponse: { status, headers in + response?.pointee.headers += headers + onResponse?(status, headers) + }, + + onIncomingBody: { bodyChunk in + response?.pointee.body += bodyChunk + onBody?(bodyChunk) + }, + onStreamComplete: { result in + switch result{ + case .success(let status): + response?.pointee.statusCode = Int(status) + case .failure(let error): + print("AWS_TEST_ERROR:\(String(describing: error))") + response?.pointee.error = error + } + onComplete?(result) + semaphore?.signal() + }, + http2ManualDataWrites: http2ManualDataWrites) + } func getHTTPRequestOptions(method: String, @@ -78,40 +183,52 @@ class HTTPClientTestFixture: XCBaseTestCase { path: String, body: String = "", response: UnsafeMutablePointer? = nil, - headers: [HTTPHeader] = [HTTPHeader]()) throws -> HTTPRequestOptions { + semaphore: DispatchSemaphore? = nil, + headers: [HTTPHeader] = [HTTPHeader](), + onResponse: HTTPRequestOptions.OnResponse? = nil, + onBody: HTTPRequestOptions.OnIncomingBody? = nil, + onComplete: HTTPRequestOptions.OnStreamComplete? = nil + ) throws -> HTTPRequestOptions { let httpRequest: HTTPRequest = try HTTPRequest(method: method, path: path, body: ByteBuffer(data: body.data(using: .utf8)!), allocator: allocator) httpRequest.addHeader(header: HTTPHeader(name: "Host", value: endpoint)) httpRequest.addHeader(header: HTTPHeader(name: "Content-Length", value: String(body.count))) httpRequest.addHeaders(headers: headers) - let onIncomingHeaders: HTTPRequestOptions.OnIncomingHeaders = { stream, headerBlock, headers in - for header in headers { - print(header.name + " : " + header.value) - response?.pointee.headers.append(header) - } - } - - let onBody: HTTPRequestOptions.OnIncomingBody = { stream, bodyChunk in - print("onBody: \(bodyChunk)") - response?.pointee.body += bodyChunk - } - - let onBlockDone: HTTPRequestOptions.OnIncomingHeadersBlockDone = { stream, block in - print("onBlockDone") - } - - let onComplete: HTTPRequestOptions.OnStreamComplete = { stream, error in - print("onComplete") - XCTAssertNil(error) - let statusCode = try! stream.statusCode() - response?.pointee.statusCode = statusCode - self.semaphore.signal() - } + return getRequestOptions( + request: httpRequest, + response: response, + semaphore: semaphore, + onResponse: onResponse, + onBody: onBody, + onComplete: onComplete) + } - let requestOptions = HTTPRequestOptions(request: httpRequest, - onIncomingHeaders: onIncomingHeaders, - onIncomingHeadersBlockDone: onBlockDone, - onIncomingBody: onBody, - onStreamComplete: onComplete) - return requestOptions + func getHTTP2RequestOptions(method: String, + path: String, + scheme: String = "https", + authority: String, + body: String = "", + manualDataWrites: Bool = false, + response: UnsafeMutablePointer? = nil, + semaphore: DispatchSemaphore? = nil, + onResponse: HTTPRequestOptions.OnResponse? = nil, + onBody: HTTPRequestOptions.OnIncomingBody? = nil, + onComplete: HTTPRequestOptions.OnStreamComplete? = nil, + http2ManualDataWrites: Bool = false) throws -> HTTPRequestOptions { + + let http2Request = try HTTP2Request(body: ByteBuffer(data: body.data(using: .utf8)!), allocator: allocator) + http2Request.addHeaders(headers: [ + HTTPHeader(name: ":method", value: method), + HTTPHeader(name: ":path", value: path), + HTTPHeader(name: ":scheme", value: scheme), + HTTPHeader(name: ":authority", value: authority) + ]) + return getRequestOptions( + request: http2Request, + response: response, + semaphore: semaphore, + onResponse: onResponse, + onBody: onBody, + onComplete: onComplete, + http2ManualDataWrites: http2ManualDataWrites) } } diff --git a/Test/AwsCommonRuntimeKitTests/http/HTTPMessageTests.swift b/Test/AwsCommonRuntimeKitTests/http/HTTPMessageTests.swift index 0102e6163..036227572 100644 --- a/Test/AwsCommonRuntimeKitTests/http/HTTPMessageTests.swift +++ b/Test/AwsCommonRuntimeKitTests/http/HTTPMessageTests.swift @@ -2,11 +2,13 @@ // SPDX-License-Identifier: Apache-2.0. import XCTest @testable import AwsCommonRuntimeKit +import AwsCHttp class HTTPMessageTests: XCBaseTestCase { - func testAddHeaders() throws { - let httpMessage = try HTTPMessage(allocator: allocator) + let httpMessage = HTTPRequestBase( + rawValue: aws_http_message_new_request(allocator.rawValue)!, + allocator: allocator) httpMessage.addHeaders(headers: [ HTTPHeader(name: "header1", value: "value1"), HTTPHeader(name: "header2", value: "value2")]) @@ -17,7 +19,9 @@ class HTTPMessageTests: XCBaseTestCase { let headers = [ HTTPHeader(name: "header1", value: "value1"), HTTPHeader(name: "header2", value: "value2")] - let httpMessage = try HTTPMessage(allocator: allocator) + let httpMessage = HTTPRequestBase( + rawValue: aws_http_message_new_request(allocator.rawValue)!, + allocator: allocator) httpMessage.addHeaders(headers: headers) let requestHeaders = httpMessage.getHeaders() XCTAssertTrue(headers.elementsEqual(requestHeaders, by: { $0.name == $1.name && $0.value == $1.value})) @@ -27,7 +31,9 @@ class HTTPMessageTests: XCBaseTestCase { let headers = [ HTTPHeader(name: "header1", value: "value1"), HTTPHeader(name: "header2", value: "value2")] - let httpMessage = try HTTPMessage(allocator: allocator) + let httpMessage = HTTPRequestBase( + rawValue: aws_http_message_new_request(allocator.rawValue)!, + allocator: allocator) httpMessage.addHeaders(headers: headers) XCTAssertEqual(httpMessage.getHeaderValue(name: "header1"), "value1") @@ -39,7 +45,9 @@ class HTTPMessageTests: XCBaseTestCase { let headers = [ HTTPHeader(name: "header1", value: "value1"), HTTPHeader(name: "header2", value: "value2")] - let httpMessage = try HTTPMessage(allocator: allocator) + let httpMessage = HTTPRequestBase( + rawValue: aws_http_message_new_request(allocator.rawValue)!, + allocator: allocator) httpMessage.addHeaders(headers: headers) XCTAssertEqual(httpMessage.headerCount, 2) httpMessage.addHeader(header: HTTPHeader(name: "HeaderToRemove", value: "xyz")) @@ -53,7 +61,9 @@ class HTTPMessageTests: XCBaseTestCase { let headers = [ HTTPHeader(name: "header1", value: "value1"), HTTPHeader(name: "header2", value: "value2")] - let httpMessage = try HTTPMessage(allocator: allocator) + let httpMessage = HTTPRequestBase( + rawValue: aws_http_message_new_request(allocator.rawValue)!, + allocator: allocator) httpMessage.addHeaders(headers: headers) XCTAssertEqual(httpMessage.headerCount, 2) httpMessage.addHeader(header: HTTPHeader(name: "", value: "xyz")) @@ -64,7 +74,9 @@ class HTTPMessageTests: XCBaseTestCase { let headers = [ HTTPHeader(name: "header1", value: "value1"), HTTPHeader(name: "header2", value: "value2")] - let httpMessage = try HTTPMessage(allocator: allocator) + let httpMessage = HTTPRequestBase( + rawValue: aws_http_message_new_request(allocator.rawValue)!, + allocator: allocator) httpMessage.addHeaders(headers: headers) XCTAssertEqual(httpMessage.headerCount, 2) httpMessage.clearHeaders() @@ -76,7 +88,9 @@ class HTTPMessageTests: XCBaseTestCase { let headers = [ HTTPHeader(name: "header1", value: "value1"), HTTPHeader(name: "header2", value: "value2")] - let httpMessage = try HTTPMessage(allocator: allocator) + let httpMessage = HTTPRequestBase( + rawValue: aws_http_message_new_request(allocator.rawValue)!, + allocator: allocator) httpMessage.addHeaders(headers: headers) XCTAssertEqual(httpMessage.headerCount, 2) diff --git a/Test/AwsCommonRuntimeKitTests/http/HTTPProxyEnvSettingsTests.swift b/Test/AwsCommonRuntimeKitTests/http/HTTPProxyEnvSettingsTests.swift index d8496f492..e95f027e9 100644 --- a/Test/AwsCommonRuntimeKitTests/http/HTTPProxyEnvSettingsTests.swift +++ b/Test/AwsCommonRuntimeKitTests/http/HTTPProxyEnvSettingsTests.swift @@ -12,7 +12,7 @@ class HTTPProxyEnvSettingsTests: XCBaseTestCase { } func testCreateProxyEnvSettingsNonDefault() throws { - let connectionType = HTTPProxyConnectionType.tunnel; + let connectionType = HTTPProxyConnectionType.tunnel let envVarType = HTTPProxyEnvType.enable let context = try TLSContext(options: TLSContextOptions(allocator: allocator), mode: TLSMode.client) let tlsOptions = TLSConnectionOptions(context: context, allocator: allocator) diff --git a/Test/AwsCommonRuntimeKitTests/http/HTTPProxyTests.swift b/Test/AwsCommonRuntimeKitTests/http/HTTPProxyTests.swift index bdbc7ef23..fd19f0749 100644 --- a/Test/AwsCommonRuntimeKitTests/http/HTTPProxyTests.swift +++ b/Test/AwsCommonRuntimeKitTests/http/HTTPProxyTests.swift @@ -191,7 +191,7 @@ class HTTPProxyTests: HTTPClientTestFixture { port: port, alpnList: ["http/1.1"], proxyOptions: proxyOptions) - _ = try await sendHttpRequest(method: "GET", endpoint: uri, connectionManager: manager) + _ = try await sendHTTPRequest(method: "GET", endpoint: uri, connectionManager: manager) } } diff --git a/Test/AwsCommonRuntimeKitTests/http/HTTPTests.swift b/Test/AwsCommonRuntimeKitTests/http/HTTPTests.swift index 1aa1fd381..5b930444b 100644 --- a/Test/AwsCommonRuntimeKitTests/http/HTTPTests.swift +++ b/Test/AwsCommonRuntimeKitTests/http/HTTPTests.swift @@ -9,30 +9,25 @@ import AwsCHttp class HTTPTests: HTTPClientTestFixture { let host = "httpbin.org" let getPath = "/get" - let TEST_DOC_LINE: String = """ - This is a sample to prove that http downloads and uploads work. - It doesn't really matter what's in here, - we mainly just need to verify the downloads and uploads work. - """ func testGetHTTPSRequest() async throws { let connectionManager = try await getHttpConnectionManager(endpoint: host, ssh: true, port: 443) - _ = try await sendHttpRequest(method: "GET", endpoint: host, path: getPath, connectionManager: connectionManager) - _ = try await sendHttpRequest(method: "GET", endpoint: host, path: "/delete", expectedStatus: 405, connectionManager: connectionManager) + _ = try await sendHTTPRequest(method: "GET", endpoint: host, path: getPath, connectionManager: connectionManager) + _ = try await sendHTTPRequest(method: "GET", endpoint: host, path: "/delete", expectedStatus: 405, connectionManager: connectionManager) } func testGetHTTPRequest() async throws { let connectionManager = try await getHttpConnectionManager(endpoint: host, ssh: false, port: 80) - _ = try await sendHttpRequest(method: "GET", endpoint: host, path: getPath, connectionManager: connectionManager) + _ = try await sendHTTPRequest(method: "GET", endpoint: host, path: getPath, connectionManager: connectionManager) } func testPutHttpRequest() async throws { let connectionManager = try await getHttpConnectionManager(endpoint: host, ssh: true, port: 443) - let response = try await sendHttpRequest( + let response = try await sendHTTPRequest( method: "PUT", endpoint: host, path: "/anything", - requestBody: TEST_DOC_LINE, + body: TEST_DOC_LINE, connectionManager: connectionManager) // Parse json body @@ -55,8 +50,10 @@ class HTTPTests: HTTPClientTestFixture { } func testStreamLivesUntilComplete() async throws { + let semaphore = DispatchSemaphore(value: 0) + do { - let httpRequestOptions = try getHTTPRequestOptions(method: "GET", endpoint: host, path: getPath) + let httpRequestOptions = try getHTTPRequestOptions(method: "GET", endpoint: host, path: getPath, semaphore: semaphore) let connectionManager = try await getHttpConnectionManager(endpoint: host, ssh: true, port: 443) let connection = try await connectionManager.acquireConnection() let stream = try connection.makeRequest(requestOptions: httpRequestOptions) @@ -67,11 +64,13 @@ class HTTPTests: HTTPClientTestFixture { func testManagerLivesUntilComplete() async throws { var connection: HTTPClientConnection! = nil + let semaphore = DispatchSemaphore(value: 0) + do { let connectionManager = try await getHttpConnectionManager(endpoint: host, ssh: true, port: 443) connection = try await connectionManager.acquireConnection() } - let httpRequestOptions = try getHTTPRequestOptions(method: "GET", endpoint: host, path: getPath) + let httpRequestOptions = try getHTTPRequestOptions(method: "GET", endpoint: host, path: getPath, semaphore: semaphore) let stream = try connection.makeRequest(requestOptions: httpRequestOptions) try stream.activate() semaphore.wait() @@ -79,10 +78,12 @@ class HTTPTests: HTTPClientTestFixture { func testConnectionLivesUntilComplete() async throws { var stream: HTTPStream! = nil + let semaphore = DispatchSemaphore(value: 0) + do { let connectionManager = try await getHttpConnectionManager(endpoint: host, ssh: true, port: 443) let connection = try await connectionManager.acquireConnection() - let httpRequestOptions = try getHTTPRequestOptions(method: "GET", endpoint: host, path: getPath) + let httpRequestOptions = try getHTTPRequestOptions(method: "GET", endpoint: host, path: getPath, semaphore: semaphore) stream = try connection.makeRequest(requestOptions: httpRequestOptions) } try stream.activate() diff --git a/aws-common-runtime/aws-c-auth b/aws-common-runtime/aws-c-auth index bad1066f0..97133a2b5 160000 --- a/aws-common-runtime/aws-c-auth +++ b/aws-common-runtime/aws-c-auth @@ -1 +1 @@ -Subproject commit bad1066f0a93f3a7df86c94cc03076fa6b901bd2 +Subproject commit 97133a2b5dbca1ccdf88cd6f44f39d0531d27d12