diff --git a/Sources/ClientRuntime/Networking/Endpoint.swift b/Sources/ClientRuntime/Networking/Endpoint.swift index b2206af3f..691d51400 100644 --- a/Sources/ClientRuntime/Networking/Endpoint.swift +++ b/Sources/ClientRuntime/Networking/Endpoint.swift @@ -5,7 +5,7 @@ import Foundation -public struct Endpoint { +public struct Endpoint: Hashable { public let path: String public let queryItems: [URLQueryItem]? public let protocolType: ProtocolType? @@ -58,55 +58,27 @@ public struct Endpoint { } } -public extension Endpoint { +extension Endpoint { // We still have to keep 'url' as an optional, since we're // dealing with dynamic components that could be invalid. - var url: URL? { + public var url: URL? { var components = URLComponents() components.scheme = protocolType?.rawValue components.host = host - components.path = path - components.percentEncodedQueryItems = queryItems + components.percentEncodedPath = path + components.percentEncodedQuery = queryItemString return components.url } - var queryItemString: String { - guard let queryItems = queryItems, !queryItems.isEmpty else { - return "" - } - let queryString = queryItems.map { "\($0.name)=\($0.value ?? "")" }.joined(separator: "&") - return "?\(queryString)" - } -} - -// It was discovered that in Swift 5.8 and earlier versions, the URLQueryItem type does not correctly implement -// Hashable: namely, multiple URLQueryItems with the same name & value and that are equal by the == operator will have -// different hash values. -// -// Github issue filed against open-source Foundation: -// https://github.com/apple/swift-corelibs-foundation/issues/4737 -// -// This extension is intended to correct this problem for the Endpoint type by substituting a -// different structure with the same properties as URLQueryItem when the Endpoint is hashed. -// -// This extension may be removed, and the compiler-generated Hashable compliance may be used instead, once the -// URLQueryItem's Hashable implementation is fixed in open-source Foundation. -extension Endpoint: Hashable { - - private struct QueryItem: Hashable { - let name: String - let value: String? - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(path) - let queryItemElements = queryItems?.map { QueryItem(name: $0.name, value: $0.value) } - hasher.combine(queryItemElements) - hasher.combine(protocolType) - hasher.combine(host) - hasher.combine(port) - hasher.combine(headers) - hasher.combine(properties) + var queryItemString: String? { + guard let queryItems = queryItems else { return nil } + return queryItems.map { queryItem in + if let value = queryItem.value { + return "\(queryItem.name)=\(value)" + } else { + return queryItem.name + } + }.joined(separator: "&") } } diff --git a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift index 2c77db57a..e0441fe69 100644 --- a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift +++ b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift @@ -45,7 +45,7 @@ public class CRTClientEngine: HttpClientEngine { return connectionPool } - + private func createConnectionPool(endpoint: Endpoint) throws -> HTTPClientConnectionManager { let tlsConnectionOptions = TLSConnectionOptions( context: sharedDefaultIO.tlsContext, @@ -69,7 +69,7 @@ public class CRTClientEngine: HttpClientEngine { enableManualWindowManagement: false ) // not using backpressure yet logger.debug(""" - Creating connection pool for \(String(describing: endpoint.url?.absoluteString)) \ + Creating connection pool for \(String(describing: endpoint.host)) \ with max connections: \(maxConnectionsPerEndpoint) """) return try HTTPClientConnectionManager(options: options) @@ -96,7 +96,7 @@ public class CRTClientEngine: HttpClientEngine { enableStreamManualWindowManagement: false ) logger.debug(""" - Creating connection pool for \(String(describing: endpoint.url?.absoluteString)) \ + Creating connection pool for \(String(describing: endpoint.host)) \ with max connections: \(maxConnectionsPerEndpoint) """) @@ -274,7 +274,7 @@ public class CRTClientEngine: HttpClientEngine { } requestOptions.http2ManualDataWrites = http2ManualDataWrites - + response.body = .stream(stream) return requestOptions } diff --git a/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift b/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift index cadb17c35..bad8e4952 100644 --- a/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift +++ b/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift @@ -10,42 +10,31 @@ import AwsCommonRuntimeKit // we need to maintain a reference to this same request while we add headers // in the CRT engine so that is why it's a class public class SdkHttpRequest { - public var body: HttpBody - public var headers: Headers - public let queryItems: [URLQueryItem]? + public let body: HttpBody public let endpoint: Endpoint public let method: HttpMethodType + public var headers: Headers { endpoint.headers ?? Headers() } + public var path: String { endpoint.path } + public var host: String { endpoint.host } + public var queryItems: [URLQueryItem]? { endpoint.queryItems } public init(method: HttpMethodType, endpoint: Endpoint, - headers: Headers, - queryItems: [URLQueryItem]? = nil, body: HttpBody = HttpBody.none) { self.method = method self.endpoint = endpoint - self.headers = headers self.body = body - self.queryItems = queryItems } } -// Create a `CharacterSet` of the characters that need not be percent encoded in the -// resulting URL. This set consists of alphanumerics plus underscore, dash, tilde, and -// period. Any other character should be percent-encoded when used in a path segment. -// Forward-slash is added as well because the segments have already been joined into a path. -// -// See, for URL-allowed characters: -// https://www.rfc-editor.org/rfc/rfc3986#section-2.3 -private let allowed = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "/_-.~")) - extension SdkHttpRequest { - public func toHttpRequest() throws -> HTTPRequest { - let httpHeaders = headers.toHttpHeaders() + + public func toHttpRequest(escaping: Bool = false) throws -> HTTPRequest { let httpRequest = try HTTPRequest() httpRequest.method = method.rawValue - let encodedPath = endpoint.path.addingPercentEncoding(withAllowedCharacters: allowed) ?? endpoint.path - httpRequest.path = "\(encodedPath)\(endpoint.queryItemString)" - httpRequest.addHeaders(headers: httpHeaders) + let encodedPath = escaping ? endpoint.path.urlPercentEncodedForPath : endpoint.path + httpRequest.path = [encodedPath, endpoint.queryItemString].compactMap { $0 }.joined(separator: "?") + httpRequest.addHeaders(headers: headers.toHttpHeaders()) httpRequest.body = StreamableHttpBody(body: body) return httpRequest } @@ -53,13 +42,12 @@ extension SdkHttpRequest { /// Convert the SDK request to a CRT HTTPRequestBase /// CRT converts the HTTPRequestBase to HTTP2Request internally if the protocol is HTTP/2 /// - Returns: the CRT request - public func toHttp2Request() throws -> HTTPRequestBase { - let httpHeaders = headers.toHttpHeaders() + public func toHttp2Request(escaping: Bool = false) throws -> HTTPRequestBase { let httpRequest = try HTTPRequest() httpRequest.method = method.rawValue - let encodedPath = endpoint.path.addingPercentEncoding(withAllowedCharacters: allowed) ?? endpoint.path - httpRequest.path = "\(encodedPath)\(endpoint.queryItemString)" - httpRequest.addHeaders(headers: httpHeaders) + let encodedPath = escaping ? endpoint.path.urlPercentEncodedForPath : endpoint.path + httpRequest.path = [encodedPath, endpoint.queryItemString].compactMap { $0 }.joined(separator: "?") + httpRequest.addHeaders(headers: headers.toHttpHeaders()) // HTTP2Request used with manual writes hence we need to set the body to nil // so that CRT does not write the body for us (we will write it manually) @@ -96,11 +84,11 @@ extension SdkHttpRequestBuilder { public func update(from crtRequest: HTTPRequestBase, originalRequest: SdkHttpRequest) -> SdkHttpRequestBuilder { headers = convertSignedHeadersToHeaders(crtRequest: crtRequest) methodType = originalRequest.method - host = originalRequest.endpoint.host - if let crtRequest = crtRequest as? HTTPRequest { - let pathAndQueryItems = URLComponents(string: crtRequest.path) - path = pathAndQueryItems?.path ?? "/" - queryItems = pathAndQueryItems?.percentEncodedQueryItems ?? [URLQueryItem]() + host = originalRequest.host + if let crtRequest = crtRequest as? HTTPRequest, let components = URLComponents(string: crtRequest.path) { + path = components.percentEncodedPath + queryItems = components.percentEncodedQueryItems?.map { URLQueryItem(name: $0.name, value: $0.value) } + ?? [URLQueryItem]() } else if crtRequest as? HTTP2Request != nil { assertionFailure("HTTP2Request not supported") } else { @@ -123,11 +111,11 @@ public class SdkHttpRequestBuilder { var host: String = "" var path: String = "/" var body: HttpBody = .none - var queryItems = [URLQueryItem]() + var queryItems: [URLQueryItem]? = nil var port: Int16 = 443 var protocolType: ProtocolType = .https - public var currentQueryItems: [URLQueryItem] { + public var currentQueryItems: [URLQueryItem]? { return queryItems } @@ -179,14 +167,14 @@ public class SdkHttpRequestBuilder { @discardableResult public func withQueryItems(_ value: [URLQueryItem]) -> SdkHttpRequestBuilder { - self.queryItems = value + self.queryItems = self.queryItems ?? [] + self.queryItems?.append(contentsOf: value) return self } @discardableResult public func withQueryItem(_ value: URLQueryItem) -> SdkHttpRequestBuilder { - self.queryItems.append(value) - return self + withQueryItems([value]) } @discardableResult @@ -206,11 +194,10 @@ public class SdkHttpRequestBuilder { path: path, port: port, queryItems: queryItems, - protocolType: protocolType) + protocolType: protocolType, + headers: headers) return SdkHttpRequest(method: methodType, endpoint: endpoint, - headers: headers, - queryItems: queryItems, body: body) } } diff --git a/Sources/ClientRuntime/Networking/Http/String+URLPercentEncoding.swift b/Sources/ClientRuntime/Networking/Http/String+URLPercentEncoding.swift new file mode 100644 index 000000000..0efb5377e --- /dev/null +++ b/Sources/ClientRuntime/Networking/Http/String+URLPercentEncoding.swift @@ -0,0 +1,35 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +// Creates a `CharacterSet` of the characters that need not be percent encoded in the +// resulting URL. This set consists of alphanumerics plus underscore, dash, tilde, and +// period. Any other character should be percent-encoded when used in a path segment. +// Forward-slash is added as well because the segments have already been joined into a path. +// +// See, for URL-allowed characters: +// https://www.rfc-editor.org/rfc/rfc3986#section-2.3 +private let allowedForPath = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "/_-.~")) +private let allowedForQuery = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "_-.~")) + +extension String { + + /// Encodes a URL component for inclusion in the path or query items, using percent-escaping. + /// + /// All characters except alphanumerics plus forward slash, underscore, dash, tilde, and period will be escaped. + var urlPercentEncodedForPath: String { + addingPercentEncoding(withAllowedCharacters: allowedForPath) ?? self + } + + /// Encodes a URL component for inclusion in query item name or value, using percent-escaping. + /// + /// All characters except alphanumerics plus forward slash, underscore, dash, tilde, and period will be escaped. + var urlPercentEncodedForQuery: String { + addingPercentEncoding(withAllowedCharacters: allowedForQuery) ?? self + } +} diff --git a/Sources/ClientRuntime/PrimitiveTypeExtensions/String+Extensions.swift b/Sources/ClientRuntime/PrimitiveTypeExtensions/String+Extensions.swift index 5450131df..ffbc29d46 100644 --- a/Sources/ClientRuntime/PrimitiveTypeExtensions/String+Extensions.swift +++ b/Sources/ClientRuntime/PrimitiveTypeExtensions/String+Extensions.swift @@ -101,10 +101,7 @@ extension String { extension String { public func urlPercentEncoding() -> String { - if let encodedString = self.addingPercentEncoding(withAllowedCharacters: .singleUrlQueryAllowed) { - return encodedString - } - return self + self.urlPercentEncodedForQuery } } diff --git a/Sources/ClientRuntime/PrimitiveTypeExtensions/URL+Extension.swift b/Sources/ClientRuntime/PrimitiveTypeExtensions/URL+Extension.swift index cff3c850d..1e12f159d 100644 --- a/Sources/ClientRuntime/PrimitiveTypeExtensions/URL+Extension.swift +++ b/Sources/ClientRuntime/PrimitiveTypeExtensions/URL+Extension.swift @@ -4,8 +4,14 @@ */ import Foundation + public typealias URL = Foundation.URL + extension URL { - func toQueryItems() -> [URLQueryItem]? { return URLComponents(url: self, - resolvingAgainstBaseURL: false)?.queryItems } + + func toQueryItems() -> [URLQueryItem]? { + URLComponents(url: self, resolvingAgainstBaseURL: false)? + .queryItems? + .map { URLQueryItem(name: $0.name, value: $0.value) } + } } diff --git a/Sources/ClientRuntime/PrimitiveTypeExtensions/URLQueryItem+Extensions.swift b/Sources/ClientRuntime/PrimitiveTypeExtensions/URLQueryItem+Extensions.swift index e3fb34753..67118e619 100644 --- a/Sources/ClientRuntime/PrimitiveTypeExtensions/URLQueryItem+Extensions.swift +++ b/Sources/ClientRuntime/PrimitiveTypeExtensions/URLQueryItem+Extensions.swift @@ -3,16 +3,14 @@ * SPDX-License-Identifier: Apache-2.0. */ -import struct Foundation.URLQueryItem -public typealias URLQueryItem = Foundation.URLQueryItem +public typealias URLQueryItem = MyURLQueryItem -extension URLQueryItem: Comparable { - /// Compares two `URLQueryItem` instances by their `name` property. - /// - Parameters: - /// - lhs: The first `URLQueryItem` to compare. - /// - rhs: The second `URLQueryItem` to compare. - /// - Returns: `true` if the `name` property of `lhs` is less than the `name` property of `rhs`. - public static func < (lhs: URLQueryItem, rhs: URLQueryItem) -> Bool { - lhs.name < rhs.name +public struct MyURLQueryItem: Hashable { + public var name: String + public var value: String? + + public init(name: String, value: String?) { + self.name = name + self.value = value } } diff --git a/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase+FormURL.swift b/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase+FormURL.swift index d2b2d3535..47c090621 100644 --- a/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase+FormURL.swift +++ b/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase+FormURL.swift @@ -4,6 +4,7 @@ // // SPDX-License-Identifier: Apache-2.0 // + import XCTest import ClientRuntime @@ -19,12 +20,12 @@ extension HttpRequestTestBase { assertQueryItems(expectedQueryItems, actualQueryItems, file: file, line: line) } - private func convertToQueryItems(data: Data) -> [URLQueryItem] { + private func convertToQueryItems(data: Data) -> [ClientRuntime.URLQueryItem] { guard let queryString = String(data: data, encoding: .utf8) else { XCTFail("Failed to decode data") return [] } - var queryItems: [URLQueryItem] = [] + var queryItems: [ClientRuntime.URLQueryItem] = [] let sanitizedQueryString = queryString.replacingOccurrences(of: "\n", with: "") let keyValuePairs = sanitizedQueryString.components(separatedBy: "&") for keyValue in keyValuePairs { diff --git a/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase.swift b/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase.swift index 423a12be3..c3f7e1b68 100644 --- a/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase.swift +++ b/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase.swift @@ -165,7 +165,7 @@ open class HttpRequestTestBase: XCTestCase { /** Check if a Query Item with given name exists in array of `URLQueryItem` */ - public func queryItemExists(_ queryItemName: String, in queryItems: [URLQueryItem]?) -> Bool { + public func queryItemExists(_ queryItemName: String, in queryItems: [ClientRuntime.URLQueryItem]?) -> Bool { guard let queryItems = queryItems else { return false } @@ -208,8 +208,8 @@ open class HttpRequestTestBase: XCTestCase { assertQueryItems(expected.queryItems, actual.queryItems, file: file, line: line) - XCTAssertEqual(expected.endpoint.path, actual.endpoint.path, file: file, line: line) - XCTAssertEqual(expected.endpoint.host, actual.endpoint.host, file: file, line: line) + XCTAssertEqual(expected.endpoint.path, actual.path, file: file, line: line) + XCTAssertEqual(expected.endpoint.host, actual.host, file: file, line: line) XCTAssertEqual(expected.method, actual.method, file: file, line: line) assertForbiddenQueryItems(expected.forbiddenQueryItems, actual.queryItems, file: file, line: line) @@ -348,8 +348,8 @@ open class HttpRequestTestBase: XCTestCase { } public func assertQueryItems( - _ expected: [URLQueryItem]?, - _ actual: [URLQueryItem]?, + _ expected: [ClientRuntime.URLQueryItem]?, + _ actual: [ClientRuntime.URLQueryItem]?, file: StaticString = #filePath, line: UInt = #line ) { @@ -382,8 +382,8 @@ open class HttpRequestTestBase: XCTestCase { } public func assertForbiddenQueryItems( - _ expected: [URLQueryItem]?, - _ actual: [URLQueryItem]?, + _ expected: [ClientRuntime.URLQueryItem]?, + _ actual: [ClientRuntime.URLQueryItem]?, file: StaticString = #filePath, line: UInt = #line ) { @@ -405,8 +405,8 @@ open class HttpRequestTestBase: XCTestCase { } public func assertRequiredQueryItems( - _ expected: [URLQueryItem]?, - _ actual: [URLQueryItem]?, + _ expected: [ClientRuntime.URLQueryItem]?, + _ actual: [ClientRuntime.URLQueryItem]?, file: StaticString = #filePath, line: UInt = #line ) { diff --git a/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/ProviderTests.swift b/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/ProviderTests.swift index 8367ccb59..bc910d160 100644 --- a/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/ProviderTests.swift +++ b/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/ProviderTests.swift @@ -104,7 +104,7 @@ extension MockInput: URLPathProvider, QueryItemProvider, HeaderProvider { var items = [ClientRuntime.URLQueryItem]() if let value = value { - let valueQueryItem = URLQueryItem(name: "test", value: "\(value)") + let valueQueryItem = ClientRuntime.URLQueryItem(name: "test", value: "\(value)") items.append(valueQueryItem) } return items diff --git a/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/SdkRequestBuilderTests.swift b/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/SdkRequestBuilderTests.swift index deffae560..5c62d6e0b 100644 --- a/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/SdkRequestBuilderTests.swift +++ b/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/SdkRequestBuilderTests.swift @@ -11,13 +11,13 @@ class SdkRequestBuilderTests: XCTestCase { func testSdkRequestBuilderUpdate() throws { let url = "https://stehlibstoragebucket144955-dev.s3.us-east-1.amazonaws.com/public/README.md?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIAVIBY7SZR4C4MBGAW%2F20220225%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20220225T034419Z&X-Amz-Expires=17999&X-Amz-SignedHeaders=host&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEEQaCXVzLWVhc3QtMSJHMEUCIEKUDdnCf1h3cZNdv10q6G24zLgn0r6th4m%2FXSS9TuR4AiEAwOwf2cG%2F1W%2FkAz1UMqFW9sZp7SY6j%2BmiicLy1dB8OXUqmgYInf%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARABGgwzNjA4OTY3NjM0OTEiDH6CiNUJiwL4sNy9KCruBbdJnGSx88A%2Be0SBpKEpSurxunasaDsJb2ZJPqVhC%2FcKPr87PYcp5BrnkGumcYhUAEknVbHACLLUx2%2Fnzfacp13PHmclSsLe52qGwjFFVMz0m41PV3HiCoHgIc7vVIHnwNySaX9ZJbVowhNvI4V9eixVKhjjx7Tqku1bsOzlq9dJP15qz8FVNjlKGjZFqrMGTzpTgmS9dNPqphwCcxx306RLUd35SEvXjxnWcidOdddYQs9j4wu47DOM8ftveag6cDptJQN71dDIRHgFTisVshD78Rm9pycKf3g0QvBAGtrzhcxUcJtNgIWv%2B10hsEBURsEYommcjI8vT1yX2K8pLVOxgL%2FRWXndbAeIzAu5vmLm6RqkfGwkHQPQl7uII6YzL2Gku%2FMDilVFw9TBKIg6KDP9l2GzzVRQsvLMpFIp%2Bx3a1s4OVduJRFpDYCwEsfKhIoVkb610gBbFayPKjQVcZfULdq1r5DOZzpHVDoijnKHAxHtFgaFPP6KtG%2BmdKeix8gccdbsdgMokWKtJhisFo%2BzLn02oSSX4ITkZKzZcriGxQO4E1YUlYyBhjlCg1b74faQfWstk24PrkCfNXYcQ5oxgglIA0tBOdOfwGn2Je3MBEj2T8Yz0GS%2BZib3DKVWRzU0Xk9pwDXH3iaBn9Uld%2BNyw9gxdOCBVKtTILtdfsjw9lJSVOJomlJn1h8gH96PToBg9lOc4ms6aA2Z%2FoN%2F3UV%2Beo5%2FpB9xfMkBIeOg6vAI0VtjkUhH052UouEKU%2BVGSGuCSuVzZmIBJLWHZaQUJZ3hJCdGqM%2FoM4Ud51Cidcnr%2Bni%2Bgp4RAfg0gvX6Eb2e%2FNMqNd0Eg7ftmnPXzQR3ZVC1yyNFu2kBt8icKdMnkLZT8YO1Racd1QgrZ5DZJlU%2FS2eisLGSTMb9Dmaq8AGbxgGK55acIvsQLzvJhavFWbh%2FyNudQBSLC0c5BZGxQk2J%2BJx%2FlR5wR%2B0gXOfTg5EImMrLzh7gOo56i2Rhj2xRFSDDDnuGQBjqHArU56DsGZNeKwkVK87lVJAfiAsWmKaDlBNXcuhKl084syaZMiVUQa7dPupgeg%2BvVPv4dAwNULjuE3B7V5lSea4RIDOfGwTtlj4Ekn3t4PrlxHpEyMeuRgekNhpdcfL5pgRcKH4AKgqI7fCrghhH1qDMTpUkORRv6EwPGqM0LPm93XPVOjrG3EJ5ZOYLJM05bbrcalzN0mbSbsRrcphme7Z8zpD5jNeSWdBhWA04DQ7RZuJpqFnn40yKq7G8kgtLmOJGRs7CGTWT9on7EOpH3RdKGPXL06%2BgLo7r9%2Fdkb%2BHGJF6spjqbH0SN%2FySjnvgNL1NrGAy%2FQgPwfDw6oJ6TPNdH8dfM8tmdd&X-Amz-Signature=90b9f3353ad4f2596fdd166c00da8fb86d5d255bf3c8b296c5e5a7db25fd41aa" let pathToMatch = "/public/README.md?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIAVIBY7SZR4C4MBGAW%2F20220225%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20220225T034419Z&X-Amz-Expires=17999&X-Amz-SignedHeaders=host&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEEQaCXVzLWVhc3QtMSJHMEUCIEKUDdnCf1h3cZNdv10q6G24zLgn0r6th4m%2FXSS9TuR4AiEAwOwf2cG%2F1W%2FkAz1UMqFW9sZp7SY6j%2BmiicLy1dB8OXUqmgYInf%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARABGgwzNjA4OTY3NjM0OTEiDH6CiNUJiwL4sNy9KCruBbdJnGSx88A%2Be0SBpKEpSurxunasaDsJb2ZJPqVhC%2FcKPr87PYcp5BrnkGumcYhUAEknVbHACLLUx2%2Fnzfacp13PHmclSsLe52qGwjFFVMz0m41PV3HiCoHgIc7vVIHnwNySaX9ZJbVowhNvI4V9eixVKhjjx7Tqku1bsOzlq9dJP15qz8FVNjlKGjZFqrMGTzpTgmS9dNPqphwCcxx306RLUd35SEvXjxnWcidOdddYQs9j4wu47DOM8ftveag6cDptJQN71dDIRHgFTisVshD78Rm9pycKf3g0QvBAGtrzhcxUcJtNgIWv%2B10hsEBURsEYommcjI8vT1yX2K8pLVOxgL%2FRWXndbAeIzAu5vmLm6RqkfGwkHQPQl7uII6YzL2Gku%2FMDilVFw9TBKIg6KDP9l2GzzVRQsvLMpFIp%2Bx3a1s4OVduJRFpDYCwEsfKhIoVkb610gBbFayPKjQVcZfULdq1r5DOZzpHVDoijnKHAxHtFgaFPP6KtG%2BmdKeix8gccdbsdgMokWKtJhisFo%2BzLn02oSSX4ITkZKzZcriGxQO4E1YUlYyBhjlCg1b74faQfWstk24PrkCfNXYcQ5oxgglIA0tBOdOfwGn2Je3MBEj2T8Yz0GS%2BZib3DKVWRzU0Xk9pwDXH3iaBn9Uld%2BNyw9gxdOCBVKtTILtdfsjw9lJSVOJomlJn1h8gH96PToBg9lOc4ms6aA2Z%2FoN%2F3UV%2Beo5%2FpB9xfMkBIeOg6vAI0VtjkUhH052UouEKU%2BVGSGuCSuVzZmIBJLWHZaQUJZ3hJCdGqM%2FoM4Ud51Cidcnr%2Bni%2Bgp4RAfg0gvX6Eb2e%2FNMqNd0Eg7ftmnPXzQR3ZVC1yyNFu2kBt8icKdMnkLZT8YO1Racd1QgrZ5DZJlU%2FS2eisLGSTMb9Dmaq8AGbxgGK55acIvsQLzvJhavFWbh%2FyNudQBSLC0c5BZGxQk2J%2BJx%2FlR5wR%2B0gXOfTg5EImMrLzh7gOo56i2Rhj2xRFSDDDnuGQBjqHArU56DsGZNeKwkVK87lVJAfiAsWmKaDlBNXcuhKl084syaZMiVUQa7dPupgeg%2BvVPv4dAwNULjuE3B7V5lSea4RIDOfGwTtlj4Ekn3t4PrlxHpEyMeuRgekNhpdcfL5pgRcKH4AKgqI7fCrghhH1qDMTpUkORRv6EwPGqM0LPm93XPVOjrG3EJ5ZOYLJM05bbrcalzN0mbSbsRrcphme7Z8zpD5jNeSWdBhWA04DQ7RZuJpqFnn40yKq7G8kgtLmOJGRs7CGTWT9on7EOpH3RdKGPXL06%2BgLo7r9%2Fdkb%2BHGJF6spjqbH0SN%2FySjnvgNL1NrGAy%2FQgPwfDw6oJ6TPNdH8dfM8tmdd&X-Amz-Signature=90b9f3353ad4f2596fdd166c00da8fb86d5d255bf3c8b296c5e5a7db25fd41aa" - let queryItems = [URLQueryItem(name: "Bucket", value: "stehlibstoragebucket144955-dev"), URLQueryItem(name: "Key", value: "public%2FREADME.md")] - let originalRequest = SdkHttpRequest(method: .get, endpoint: Endpoint(host: "stehlibstoragebucket144955-dev.s3.us-east-1.amazonaws.com", path: "/", port: 80, queryItems: queryItems, protocolType: .https), headers: Headers()) + let queryItems = [ClientRuntime.URLQueryItem(name: "Bucket", value: "stehlibstoragebucket144955-dev"), URLQueryItem(name: "Key", value: "public%2FREADME.md")] + let originalRequest = SdkHttpRequest(method: .get, endpoint: Endpoint(host: "stehlibstoragebucket144955-dev.s3.us-east-1.amazonaws.com", path: "/", port: 80, queryItems: queryItems, protocolType: .https)) let crtRequest = try HTTPRequest() crtRequest.path = pathToMatch let updatedRequest = SdkHttpRequestBuilder().update(from: crtRequest, originalRequest: originalRequest).build() - let updatedPath = updatedRequest.endpoint.path + updatedRequest.endpoint.queryItemString + let updatedPath = [updatedRequest.endpoint.path, updatedRequest.endpoint.queryItemString].compactMap { $0 }.joined(separator: "?") XCTAssertEqual(pathToMatch, updatedPath) XCTAssertEqual(url, updatedRequest.endpoint.url?.absoluteString) } diff --git a/Tests/ClientRuntimeTests/NetworkingTests/CRTClientEngineIntegrationTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/CRTClientEngineIntegrationTests.swift index 9802bfa7f..86d2decf7 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/CRTClientEngineIntegrationTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/CRTClientEngineIntegrationTests.swift @@ -32,7 +32,7 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { var headers = Headers() headers.add(name: "Content-type", value: "application/json") headers.add(name: "Host", value: "httpbin.org") - let request = SdkHttpRequest(method: .get, endpoint: Endpoint(host: "httpbin.org", path: "/get"), headers: headers) + let request = SdkHttpRequest(method: .get, endpoint: Endpoint(host: "httpbin.org", path: "/get", headers: headers)) let response = try await httpClient.execute(request: request) XCTAssertNotNil(response) @@ -48,8 +48,7 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { let encoder = JSONEncoder() let encodedData = try encoder.encode(body) let request = SdkHttpRequest(method: .post, - endpoint: Endpoint(host: "httpbin.org", path: "/post"), - headers: headers, + endpoint: Endpoint(host: "httpbin.org", path: "/post", headers: headers), body: HttpBody.data(encodedData)) let response = try await httpClient.execute(request: request) XCTAssertNotNil(response) @@ -62,8 +61,7 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { headers.add(name: "Content-type", value: "application/json") headers.add(name: "Host", value: "httpbin.org") let request = SdkHttpRequest(method: .get, - endpoint: Endpoint(host: "httpbin.org", path: "/stream-bytes/1024"), - headers: headers, + endpoint: Endpoint(host: "httpbin.org", path: "/stream-bytes/1024", headers: headers), body: HttpBody.stream(BufferedStream())) let response = try await httpClient.execute(request: request) XCTAssertNotNil(response) @@ -77,8 +75,7 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { headers.add(name: "Host", value: "httpbin.org") let request = SdkHttpRequest(method: .get, - endpoint: Endpoint(host: "httpbin.org", path: "/stream-bytes/1024"), - headers: headers, + endpoint: Endpoint(host: "httpbin.org", path: "/stream-bytes/1024", headers: headers), body: HttpBody.stream(BufferedStream())) let response = try await httpClient.execute(request: request) XCTAssertNotNil(response) @@ -97,8 +94,7 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { headers.add(name: "Host", value: "httpbin.org") let request = SdkHttpRequest(method: .get, - endpoint: Endpoint(host: "httpbin.org", path: "/stream-bytes/1"), - headers: headers, + endpoint: Endpoint(host: "httpbin.org", path: "/stream-bytes/1", headers: headers), body: HttpBody.stream(BufferedStream())) let response = try await httpClient.execute(request: request) XCTAssertNotNil(response) @@ -117,8 +113,7 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { headers.add(name: "Host", value: "httpbin.org") let request = SdkHttpRequest(method: .get, - endpoint: Endpoint(host: "httpbin.org", path: "/stream-bytes/3000"), - headers: headers, + endpoint: Endpoint(host: "httpbin.org", path: "/stream-bytes/3000", headers: headers), body: HttpBody.stream(BufferedStream())) let response = try await httpClient.execute(request: request) XCTAssertNotNil(response) @@ -140,8 +135,7 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { let encodedData = try encoder.encode(body) let request = SdkHttpRequest(method: .post, - endpoint: Endpoint(host: "httpbin.org", path: "/post"), - headers: headers, + endpoint: Endpoint(host: "httpbin.org", path: "/post", headers: headers), body: HttpBody.stream(BufferedStream(data: encodedData))) let response = try await httpClient.execute(request: request) XCTAssertNotNil(response) @@ -159,8 +153,7 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { let request = SdkHttpRequest( method: .put, - endpoint: Endpoint(host: "nghttp2.org", path: "/httpbin/put"), - headers: headers, + endpoint: Endpoint(host: "nghttp2.org", path: "/httpbin/put", headers: headers), body: .data(encodedData) ) diff --git a/Tests/ClientRuntimeTests/NetworkingTests/EndpointTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/EndpointTests.swift index b9bd515d0..e3649d763 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/EndpointTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/EndpointTests.swift @@ -15,7 +15,7 @@ class EndpointTests: XCTestCase { func test_queryItems_setsQueryItemsFromURLInOrder() throws { let endpoint = try Endpoint(url: url) let expectedQueryItems = [ - URLQueryItem(name: "abc", value: "def"), + ClientRuntime.URLQueryItem(name: "abc", value: "def"), URLQueryItem(name: "ghi", value: "jkl"), URLQueryItem(name: "mno", value: "pqr") ] @@ -28,4 +28,26 @@ class EndpointTests: XCTestCase { XCTAssertEqual(endpoint1, endpoint2) XCTAssertEqual(endpoint1.hashValue, endpoint2.hashValue) } + + func test_path_percentEncodedInput() throws { + let endpoint = Endpoint( + host: "xctest.amazonaws.com", + path: "/abc%2Bdef", + protocolType: .https + ) + let foundationURL = try XCTUnwrap(endpoint.url) + let absoluteString = foundationURL.absoluteString + XCTAssertEqual(absoluteString, "https://xctest.amazonaws.com/abc%2Bdef") + } + + func test_path_unencodedInput() throws { + let endpoint = Endpoint( + host: "xctest.amazonaws.com", + path: "/abc+def", + protocolType: .https + ) + let foundationURL = try XCTUnwrap(endpoint.url) + let absoluteString = foundationURL.absoluteString + XCTAssertEqual(absoluteString, "https://xctest.amazonaws.com/abc+def") + } } diff --git a/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift index eab7856f6..bb5259c94 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift @@ -15,13 +15,11 @@ class HttpRequestTests: NetworkingTestUtils { } func testSdkHttpRequestToHttpRequest() throws { - let endpoint = Endpoint(host: "host.com", path: "/") - var headers = Headers() - - headers.add(name: "header-item-name", value: "header-item-value") + var headers = Headers(["header-item-name": "header-item-value"]) + let endpoint = Endpoint(host: "host.com", path: "/", headers: headers) let httpBody = HttpBody.data(expectedMockRequestData) - let mockHttpRequest = SdkHttpRequest(method: .get, endpoint: endpoint, headers: headers, body: httpBody) + let mockHttpRequest = SdkHttpRequest(method: .get, endpoint: endpoint, body: httpBody) let httpRequest = try mockHttpRequest.toHttpRequest() XCTAssertNotNil(httpRequest) @@ -57,8 +55,8 @@ class HttpRequestTests: NetworkingTestUtils { } func testSdkPathAndQueryItemsToCRTPathAndQueryItems() throws { - let queryItem1 = URLQueryItem(name: "foo", value: "bar") - let queryItem2 = URLQueryItem(name: "quz", value: "baz") + let queryItem1 = ClientRuntime.URLQueryItem(name: "foo", value: "bar") + let queryItem2 = ClientRuntime.URLQueryItem(name: "quz", value: "baz") let builder = SdkHttpRequestBuilder() .withHeader(name: "Host", value: "amazon.aws.com") .withPath("/hello") @@ -70,8 +68,8 @@ class HttpRequestTests: NetworkingTestUtils { } func testCRTPathAndQueryItemsToSdkPathAndQueryItems() throws { - let queryItem1 = URLQueryItem(name: "foo", value: "bar") - let queryItem2 = URLQueryItem(name: "quz", value: "bar") + let queryItem1 = ClientRuntime.URLQueryItem(name: "foo", value: "bar") + let queryItem2 = ClientRuntime.URLQueryItem(name: "quz", value: "bar") let builder = SdkHttpRequestBuilder() .withHeader(name: "Host", value: "amazon.aws.com") .withPath("/hello") @@ -79,28 +77,37 @@ class HttpRequestTests: NetworkingTestUtils { .withQueryItem(queryItem2) .withHeader(name: "Content-Length", value: "6") - XCTAssert(builder.queryItems.count == 2) + XCTAssert(builder.queryItems?.count == 2) let httpRequest = try builder.build().toHttpRequest() httpRequest.path = "/hello?foo=bar&quz=bar&signedthing=signed" let updatedRequest = builder.update(from: httpRequest, originalRequest: builder.build()) XCTAssert(updatedRequest.path == "/hello") - XCTAssert(updatedRequest.queryItems.count == 3) - XCTAssert(updatedRequest.queryItems.contains(queryItem1)) - XCTAssert(updatedRequest.queryItems.contains(queryItem2)) - XCTAssert(updatedRequest.queryItems.contains(URLQueryItem(name: "signedthing", value: "signed"))) + XCTAssert(updatedRequest.queryItems?.count == 3) + XCTAssert(updatedRequest.queryItems?.contains(queryItem1) ?? false) + XCTAssert(updatedRequest.queryItems?.contains(queryItem2) ?? false) + XCTAssert(updatedRequest.queryItems?.contains(ClientRuntime.URLQueryItem(name: "signedthing", value: "signed")) ?? false) } func testPathInInHttpRequestIsEscapedPerRFC3986() throws { let builder = SdkHttpRequestBuilder() .withHeader(name: "Host", value: "xctest.amazon.com") .withPath("/space /colon:/dollar$/tilde~/dash-/underscore_/period.") - let httpRequest = try builder.build().toHttpRequest() + let httpRequest = try builder.build().toHttpRequest(escaping: true) let escapedPath = "/space%20/colon%3A/dollar%24/tilde~/dash-/underscore_/period." XCTAssertEqual(httpRequest.path, escapedPath) } + func testPathInInHttpRequestIsNotEscapedPerRFC3986WhenNotDesired() throws { + let path = "/space /colon:/dollar$/tilde~/dash-/underscore_/period." + let builder = SdkHttpRequestBuilder() + .withHeader(name: "Host", value: "xctest.amazon.com") + .withPath(path) + let httpRequest = try builder.build().toHttpRequest() + XCTAssertEqual(httpRequest.path, path) + } + func testConversionToUrlRequestFailsWithInvalidEndpoint() { // TODO:: When is the endpoint invalid or endpoint.url nil? _ = Endpoint(host: "", path: "", protocolType: nil) diff --git a/Tests/ClientRuntimeTests/NetworkingTests/NetworkingTestUtils.swift b/Tests/ClientRuntimeTests/NetworkingTests/NetworkingTestUtils.swift index b6c06c2d4..1f1fffae2 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/NetworkingTestUtils.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/NetworkingTestUtils.swift @@ -27,35 +27,32 @@ class NetworkingTestUtils: XCTestCase { Create a mock HttpRequest with valid data payload */ func setMockHttpDataRequest() { - let endpoint = getMockEndpoint() - var headers = Headers() - - headers.add(name: "header-item-name", value: "header-item-value") + var headers = Headers(["header-item-name": "header-item-value"]) + let endpoint = getMockEndpoint(headers: headers) let httpBody = HttpBody.data(expectedMockRequestData) - mockHttpDataRequest = SdkHttpRequest(method: .get, endpoint: endpoint, headers: headers, body: httpBody) + mockHttpDataRequest = SdkHttpRequest(method: .get, endpoint: endpoint, body: httpBody) } /* Create a mock HttpRequest with valid InputStream */ func setMockHttpStreamRequest() { - let endpoint = getMockEndpoint() - var headers = Headers() - headers.add(name: "header-item-name", value: "header-item-value") + var headers = Headers(["header-item-name": "header-item-value"]) + var endpoint = getMockEndpoint(headers: headers) let httpBody = HttpBody(byteStream: ByteStream.from(data: expectedMockRequestData)) - mockHttpStreamRequest = SdkHttpRequest(method: .get, endpoint: endpoint, headers: headers, body: httpBody) + mockHttpStreamRequest = SdkHttpRequest(method: .get, endpoint: endpoint, body: httpBody) } - func getMockEndpoint() -> Endpoint { + func getMockEndpoint(headers: Headers) -> Endpoint { let path = "/path/to/endpoint" let host = "myapi.host.com" - var queryItems: [URLQueryItem] = [] + var queryItems: [ClientRuntime.URLQueryItem] = [] let endpoint: Endpoint! queryItems.append(URLQueryItem(name: "qualifier", value: "qualifier-value")) - endpoint = Endpoint(host: host, path: path, queryItems: queryItems) + endpoint = Endpoint(host: host, path: path, queryItems: queryItems, headers: headers) return endpoint } diff --git a/Tests/SmithyTestUtilTests/RequestTestUtilTests/HttpRequestTestBaseTests.swift b/Tests/SmithyTestUtilTests/RequestTestUtilTests/HttpRequestTestBaseTests.swift index 374e944e8..b67968e45 100644 --- a/Tests/SmithyTestUtilTests/RequestTestUtilTests/HttpRequestTestBaseTests.swift +++ b/Tests/SmithyTestUtilTests/RequestTestUtilTests/HttpRequestTestBaseTests.swift @@ -48,8 +48,8 @@ class HttpRequestTestBaseTests: HttpRequestTestBase { Self.Context == H.Context, Self.MInput == H.Input, Self.MOutput == H.Output { - var queryItems: [URLQueryItem] = [] - var queryItem: URLQueryItem + var queryItems: [ClientRuntime.URLQueryItem] = [] + var queryItem: ClientRuntime.URLQueryItem if let requiredQuery = input.operationInput.requiredQuery { queryItem = URLQueryItem(name: "RequiredQuery".urlPercentEncoding(), value: String(requiredQuery).urlPercentEncoding()) queryItems.append(queryItem)