From 1d12720565c93cc43081378010f6ac4cc1c84683 Mon Sep 17 00:00:00 2001 From: Jon Shier Date: Mon, 26 Nov 2018 14:39:37 -0500 Subject: [PATCH] Real HTTPHeaders type. (#2629) * Work towards server trust enhancements. * Refactor the rewrite! (#2585) * Refactor request storage out of SessionDelegate. * Continue development. * Rename SessionManager -> Session, update environment. * Rename global Alamofire enum to AF, to avoid collision. * Sort project. * Whitespace cleanup. * Reimplement module changes from bad rebase. * Finalize errors, refactor testing. * Standardize self-signed support, add error descriptions. * Remove per-target setting. * Make RequestAdapter async. * Add HTTPHeaders type. * Update for review suggestions. * Add HTTPHeaders tests, cleanup whitespace. * Add inline documentation. * Updates for review. * Whitespace cleanup. * Squashed commit of the following: commit 7a73af699037fb447c33412f92cfe032cfd84019 Author: Jon Shier Date: Wed Nov 21 19:39:20 2018 -0500 Async RequestAdapter (#2628) * Work towards server trust enhancements. * Refactor the rewrite! (#2585) * Refactor request storage out of SessionDelegate. * Continue development. * Rename SessionManager -> Session, update environment. * Rename global Alamofire enum to AF, to avoid collision. * Sort project. * Whitespace cleanup. * Reimplement module changes from bad rebase. * Finalize errors, refactor testing. * Standardize self-signed support, add error descriptions. * Remove per-target setting. * Make RequestAdapter async. commit ccfb96acd46c4477d24ea3f6a01c395cd273c5bd Author: Jon Shier Date: Wed Nov 21 19:32:04 2018 -0500 Alamofire 5: Server Trust Errors (#2608) * Work towards server trust enhancements. * Refactor the rewrite! (#2585) * Refactor request storage out of SessionDelegate. * Continue development. * Rename SessionManager -> Session, update environment. * Rename global Alamofire enum to AF, to avoid collision. * Sort project. * Whitespace cleanup. * Reimplement module changes from bad rebase. * Finalize errors, refactor testing. * Standardize self-signed support, add error descriptions. * Remove per-target setting. * Refactor evaluation API, DRY up a little bit. * Update convienience property. * Add comment for public `Error` API. * Add add methods to HTTPHeaders, whitespace cleanup. * Call add instead of update. --- Alamofire.xcodeproj/project.pbxproj | 24 +- Source/HTTPHeaders.swift | 381 ++++++++++++++++-- Source/MultipartFormData.swift | 15 +- Source/Response.swift | 26 +- Source/ServerTrustEvaluation.swift | 22 +- ...URLConvertible+URLRequestConvertible.swift | 7 +- .../URLSessionConfiguration+Alamofire.swift | 2 +- Tests/AuthenticationTests.swift | 2 +- Tests/CacheTests.swift | 2 +- Tests/DownloadTests.swift | 4 +- Tests/HTTPHeadersTests.swift | 133 ++++++ Tests/MultipartFormDataTests.swift | 72 ++-- Tests/RequestTests.swift | 12 +- Tests/ResponseSerializationTests.swift | 2 +- Tests/ServerTrustEvaluatorTests.swift | 2 +- ...nManagerTests.swift => SessionTests.swift} | 36 +- Tests/URLProtocolTests.swift | 2 +- Tests/UploadTests.swift | 6 +- Tests/ValidationTests.swift | 2 +- 19 files changed, 603 insertions(+), 149 deletions(-) create mode 100644 Tests/HTTPHeadersTests.swift rename Tests/{SessionManagerTests.swift => SessionTests.swift} (98%) diff --git a/Alamofire.xcodeproj/project.pbxproj b/Alamofire.xcodeproj/project.pbxproj index 4888d6cb7..b44cf3000 100644 --- a/Alamofire.xcodeproj/project.pbxproj +++ b/Alamofire.xcodeproj/project.pbxproj @@ -33,9 +33,9 @@ 3107EA3F20A1267C00445260 /* SessionDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9DCE771CB1BCE2003E6463 /* SessionDelegateTests.swift */; }; 3107EA4020A1267C00445260 /* SessionDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9DCE771CB1BCE2003E6463 /* SessionDelegateTests.swift */; }; 3107EA4120A1267D00445260 /* SessionDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9DCE771CB1BCE2003E6463 /* SessionDelegateTests.swift */; }; - 3111CE8420A7636E008315E2 /* SessionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D1C6F419D52968002E74FE /* SessionManagerTests.swift */; }; - 3111CE8520A7636F008315E2 /* SessionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D1C6F419D52968002E74FE /* SessionManagerTests.swift */; }; - 3111CE8620A76370008315E2 /* SessionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D1C6F419D52968002E74FE /* SessionManagerTests.swift */; }; + 3111CE8420A7636E008315E2 /* SessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D1C6F419D52968002E74FE /* SessionTests.swift */; }; + 3111CE8520A7636F008315E2 /* SessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D1C6F419D52968002E74FE /* SessionTests.swift */; }; + 3111CE8620A76370008315E2 /* SessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D1C6F419D52968002E74FE /* SessionTests.swift */; }; 3111CE8820A77843008315E2 /* EventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3111CE8720A77843008315E2 /* EventMonitor.swift */; }; 3111CE8920A77944008315E2 /* EventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3111CE8720A77843008315E2 /* EventMonitor.swift */; }; 3111CE8A20A77945008315E2 /* EventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3111CE8720A77843008315E2 /* EventMonitor.swift */; }; @@ -55,6 +55,9 @@ 3111CE9B20A7EC57008315E2 /* URLProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCFA7991B2BE71600B6F460 /* URLProtocolTests.swift */; }; 3111CE9C20A7EC58008315E2 /* URLProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCFA7991B2BE71600B6F460 /* URLProtocolTests.swift */; }; 3111CE9D20A7EC58008315E2 /* URLProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCFA7991B2BE71600B6F460 /* URLProtocolTests.swift */; }; + 3113D46B21878227001CCD21 /* HTTPHeadersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3113D46A21878227001CCD21 /* HTTPHeadersTests.swift */; }; + 3113D46C21878227001CCD21 /* HTTPHeadersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3113D46A21878227001CCD21 /* HTTPHeadersTests.swift */; }; + 3113D46D21878227001CCD21 /* HTTPHeadersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3113D46A21878227001CCD21 /* HTTPHeadersTests.swift */; }; 311B199020B0D3B40036823B /* MultipartUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311B198F20B0D3B40036823B /* MultipartUpload.swift */; }; 311B199120B0E3470036823B /* MultipartUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311B198F20B0D3B40036823B /* MultipartUpload.swift */; }; 311B199220B0E3480036823B /* MultipartUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311B198F20B0D3B40036823B /* MultipartUpload.swift */; }; @@ -321,6 +324,7 @@ /* Begin PBXFileReference section */ 3111CE8720A77843008315E2 /* EventMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMonitor.swift; sourceTree = ""; }; + 3113D46A21878227001CCD21 /* HTTPHeadersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeadersTests.swift; sourceTree = ""; }; 311B198F20B0D3B40036823B /* MultipartUpload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipartUpload.swift; sourceTree = ""; }; 312D1E0B1FC2551400E51FF1 /* Usage.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = Usage.md; path = Documentation/Usage.md; sourceTree = ""; }; 312D1E0C1FC2551400E51FF1 /* AdvancedUsage.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = AdvancedUsage.md; path = Documentation/AdvancedUsage.md; sourceTree = ""; }; @@ -425,7 +429,7 @@ F86AEFE51AE6A282007D9C76 /* TLSEvaluationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TLSEvaluationTests.swift; sourceTree = ""; }; F897FF4019AA800700AB5182 /* Alamofire.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Alamofire.swift; sourceTree = ""; }; F8AE910119D28DCC0078C7B2 /* ValidationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValidationTests.swift; sourceTree = ""; }; - F8D1C6F419D52968002E74FE /* SessionManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionManagerTests.swift; sourceTree = ""; }; + F8D1C6F419D52968002E74FE /* SessionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionTests.swift; sourceTree = ""; }; F8E6024419CB46A800A3E7F1 /* AuthenticationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -495,7 +499,7 @@ F8111E5E19A9674D0040E7D1 /* ResponseTests.swift */, 4CA028C41B7466C500C84163 /* ResultTests.swift */, 4C9DCE771CB1BCE2003E6463 /* SessionDelegateTests.swift */, - F8D1C6F419D52968002E74FE /* SessionManagerTests.swift */, + F8D1C6F419D52968002E74FE /* SessionTests.swift */, F8111E5F19A9674D0040E7D1 /* UploadTests.swift */, ); name = Core; @@ -505,6 +509,7 @@ isa = PBXGroup; children = ( 4C341BB91B1A865A00C1B34D /* CacheTests.swift */, + 3113D46A21878227001CCD21 /* HTTPHeadersTests.swift */, 4C3238E61B3604DB00FE04AE /* MultipartFormDataTests.swift */, 4C3D00571C66A8B900D1F709 /* NetworkReachabilityManagerTests.swift */, 4C0B58381B747A4400C0B99C /* ResponseSerializationTests.swift */, @@ -1286,11 +1291,12 @@ 4CFB02921D7CF28F0056F249 /* FileManager+AlamofireTests.swift in Sources */, 4CF627141BA7CC240011A099 /* BaseTestCase.swift in Sources */, 31EBD9C320D1D89D00D1FF34 /* ValidationTests.swift in Sources */, - 3111CE8620A76370008315E2 /* SessionManagerTests.swift in Sources */, + 3111CE8620A76370008315E2 /* SessionTests.swift in Sources */, 31C2B0F220B271380089BA7C /* TLSEvaluationTests.swift in Sources */, 3111CE9D20A7EC58008315E2 /* URLProtocolTests.swift in Sources */, 317A6A7820B2208000A9FEC5 /* DownloadTests.swift in Sources */, 31F9683E20BB70290009606F /* NSLoggingEventMonitor.swift in Sources */, + 3113D46D21878227001CCD21 /* HTTPHeadersTests.swift in Sources */, 3107EA4120A1267D00445260 /* SessionDelegateTests.swift in Sources */, 31C2B0EC20B271060089BA7C /* CacheTests.swift in Sources */, 3111CE9120A7EC27008315E2 /* NetworkReachabilityManagerTests.swift in Sources */, @@ -1410,11 +1416,12 @@ F8858DDD19A96B4300F55F93 /* RequestTests.swift in Sources */, 4C256A531B096C770065714F /* BaseTestCase.swift in Sources */, 31EBD9C120D1D89C00D1FF34 /* ValidationTests.swift in Sources */, - 3111CE8420A7636E008315E2 /* SessionManagerTests.swift in Sources */, + 3111CE8420A7636E008315E2 /* SessionTests.swift in Sources */, 31C2B0F020B271370089BA7C /* TLSEvaluationTests.swift in Sources */, 3111CE9B20A7EC57008315E2 /* URLProtocolTests.swift in Sources */, 317A6A7620B2207F00A9FEC5 /* DownloadTests.swift in Sources */, 31F9683C20BB70290009606F /* NSLoggingEventMonitor.swift in Sources */, + 3113D46B21878227001CCD21 /* HTTPHeadersTests.swift in Sources */, 3107EA3F20A1267C00445260 /* SessionDelegateTests.swift in Sources */, 31C2B0EA20B271040089BA7C /* CacheTests.swift in Sources */, 3111CE8F20A7EC26008315E2 /* NetworkReachabilityManagerTests.swift in Sources */, @@ -1438,11 +1445,12 @@ F829C6BE1A7A950600A2CD59 /* ParameterEncodingTests.swift in Sources */, F829C6BF1A7A950600A2CD59 /* RequestTests.swift in Sources */, 31EBD9C220D1D89C00D1FF34 /* ValidationTests.swift in Sources */, - 3111CE8520A7636F008315E2 /* SessionManagerTests.swift in Sources */, + 3111CE8520A7636F008315E2 /* SessionTests.swift in Sources */, 31C2B0F120B271370089BA7C /* TLSEvaluationTests.swift in Sources */, 3111CE9C20A7EC58008315E2 /* URLProtocolTests.swift in Sources */, 317A6A7720B2208000A9FEC5 /* DownloadTests.swift in Sources */, 31F9683D20BB70290009606F /* NSLoggingEventMonitor.swift in Sources */, + 3113D46C21878227001CCD21 /* HTTPHeadersTests.swift in Sources */, 3107EA4020A1267C00445260 /* SessionDelegateTests.swift in Sources */, 31C2B0EB20B271050089BA7C /* CacheTests.swift in Sources */, 3111CE9020A7EC27008315E2 /* NetworkReachabilityManagerTests.swift in Sources */, diff --git a/Source/HTTPHeaders.swift b/Source/HTTPHeaders.swift index c09d77146..921182348 100644 --- a/Source/HTTPHeaders.swift +++ b/Source/HTTPHeaders.swift @@ -24,40 +24,333 @@ import Foundation -public typealias HTTPHeaders = [String: String] -extension Dictionary where Key == String, Value == String { - public static func authorization(username: String, password: String) -> HTTPHeaders { - let credential = Data("\(username):\(password)".utf8).base64EncodedString() +/// An order-preserving and case-insensitive representation of HTTP headers. +public struct HTTPHeaders { + private var headers = [HTTPHeader]() + + /// Create an empty instance. + public init() { } + + /// Create an instance from an array of `HTTPHeader`s. Duplicate case-insensitive names are collapsed into the last + /// name and value encountered. + public init(_ headers: [HTTPHeader]) { + self.init() + + headers.forEach { update($0) } + } + + /// Create an instance from a `[String: String]`. Duplicate case-insensitive names are collapsed into the last name + /// and value encountered. + public init(_ dictionary: [String: String]) { + self.init() + + dictionary.forEach { update(HTTPHeader(name: $0.key, value: $0.value)) } + } + + /// Case-insensitively updates or appends an `HTTPHeader` into the instance using the provided `name` and `value`. + /// + /// - Parameters: + /// - name: The `HTTPHeader` name. + /// - value: The `HTTPHeader value. + public mutating func add(name: String, value: String) { + update(HTTPHeader(name: name, value: value)) + } + + /// Case-insensitively updates or appends the provided `HTTPHeader` into the instance. + /// + /// - Parameter header: The `HTTPHeader` to update or append. + public mutating func add(_ header: HTTPHeader) { + update(header) + } + + /// Case-insensitively updates or appends an `HTTPHeader` into the instance using the provided `name` and `value`. + /// + /// - Parameters: + /// - name: The `HTTPHeader` name. + /// - value: The `HTTPHeader value. + public mutating func update(name: String, value: String) { + update(HTTPHeader(name: name, value: value)) + } - return ["Authorization": "Basic \(credential)"] + /// Case-insensitively updates or appends the provided `HTTPHeader` into the instance. + /// + /// - Parameter header: The `HTTPHeader` to update or append. + public mutating func update(_ header: HTTPHeader) { + guard let index = headers.index(of: header.name) else { + headers.append(header) + return + } + + headers.replaceSubrange(index...index, with: [header]) + } + + /// Case-insensitively removes an `HTTPHeader`, if it exists, from the instance. + /// + /// - Parameter name: The name of the `HTTPHeader` to remove. + public mutating func remove(name: String) { + guard let index = headers.index(of: name) else { return } + + headers.remove(at: index) + } + + /// Sort the current instance by header name. + mutating public func sort() { + headers.sort { $0.name < $1.name } + } + + /// Returns an instance sorted by header name. + /// + /// - Returns: A copy of the current instance sorted by name. + public func sorted() -> HTTPHeaders { + return HTTPHeaders(headers.sorted { $0.name < $1.name }) + } + + /// Case-insensitively find a header's value by name. + /// + /// - Parameter name: The name of the header to search for, case-insensitively. + /// - Returns: The value of header, if it exists. + public func value(for name: String) -> String? { + guard let index = headers.index(of: name) else { return nil } + + return headers[index].value } - /// Creates default values for the "Accept-Encoding", "Accept-Language" and "User-Agent" headers. - public static let defaultHTTPHeaders: HTTPHeaders = { - // Accept-Encoding HTTP Header; see https://tools.ietf.org/html/rfc7230#section-4.2.3 - let acceptEncoding: String = { - let encodings: [String] - if #available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *) { - encodings = ["br", "gzip", "deflate"] + /// Case-insensitively access the header with the given name. + /// + /// - Parameter name: The name of the header. + public subscript(_ name: String) -> String? { + get { return value(for: name) } + set { + if let value = newValue { + update(name: name, value: value) } else { - encodings = ["gzip", "deflate"] + remove(name: name) } + } + } - return encodings.enumerated().map { (index, encoding) in - let quality = 1.0 - (Double(index) * 0.1) - return "\(encoding);q=\(quality)" - }.joined(separator: ", ") - }() + /// The dictionary representation of all headers. + /// + /// This representation does not preserve the current order of the instance. + public var dictionary: [String: String] { + let namesAndValues = headers.map { ($0.name, $0.value) } - // Accept-Language HTTP Header; see https://tools.ietf.org/html/rfc7231#section-5.3.5 - let acceptLanguage = Locale.preferredLanguages.prefix(6).enumerated().map { (index, languageCode) in - let quality = 1.0 - (Double(index) * 0.1) - return "\(languageCode);q=\(quality)" - }.joined(separator: ", ") + return Dictionary(namesAndValues, uniquingKeysWith: { (_, last) in last }) + } +} + +extension HTTPHeaders: ExpressibleByDictionaryLiteral { + public init(dictionaryLiteral elements: (String, String)...) { + self.init() + + elements.forEach { update(name: $0.0, value: $0.1) } + } +} + +extension HTTPHeaders: ExpressibleByArrayLiteral { + public init(arrayLiteral elements: HTTPHeader...) { + self.init(elements) + } +} + +extension HTTPHeaders: Sequence { + public func makeIterator() -> IndexingIterator> { + return headers.makeIterator() + } +} + +extension HTTPHeaders: Collection { + public var startIndex: Int { + return headers.startIndex + } + + public var endIndex: Int { + return headers.endIndex + } + + public subscript(position: Int) -> HTTPHeader { + return headers[position] + } + + public func index(after i: Int) -> Int { + return headers.index(after: i) + } +} + +extension HTTPHeaders: CustomStringConvertible { + public var description: String { + return headers.map { $0.description } + .joined(separator: "\n") + } +} + +// MARK: - HTTPHeader + +/// A representation of a single HTTP header's name / value pair. +public struct HTTPHeader: Hashable { + /// Name of the header. + public let name: String - // User-Agent Header; see https://tools.ietf.org/html/rfc7231#section-5.5.3 - // Example: `iOS Example/1.0 (org.alamofire.iOS-Example; build:1; iOS 10.0.0) Alamofire/4.0.0` + /// Value of the header. + public let value: String + + /// Creates an instance from the given `name` and `value`. + /// + /// - Parameters: + /// - name: The name of the header. + /// - value: The value of the header. + public init(name: String, value: String) { + self.name = name + self.value = value + } +} + +extension HTTPHeader: CustomStringConvertible { + public var description: String { + return "\(name): \(value)" + } +} + +extension HTTPHeader { + /// Returns an `Accept-Charset` header. + /// + /// - Parameter value: The `Accept-Charset` value. + /// - Returns: The header. + public static func acceptCharset(_ value: String) -> HTTPHeader { + return HTTPHeader(name: "Accept-Charset", value: value) + } + + /// Returns an `Accept-Language` header. + /// + /// Alamofire offers a default Accept-Language header that accumulates and encodes the system's preferred languages. + /// Use `HTTPHeader.defaultAcceptLanguage`. + /// + /// - Parameter value: The `Accept-Language` value. + /// - Returns: The header. + public static func acceptLanguage(_ value: String) -> HTTPHeader { + return HTTPHeader(name: "Accept-Language", value: value) + } + + /// Returns an `Accept-Encoding` header. + /// + /// Alamofire offers a default accept encoding value that provides the most common values. Use + /// `HTTPHeader.defaultAcceptEncoding`. + /// + /// - Parameter value: The `Accept-Encoding` value. + /// - Returns: The header + public static func acceptEncoding(_ value: String) -> HTTPHeader { + return HTTPHeader(name: "Accept-Encoding", value: value) + } + + /// Returns a `Basic` `Authorization` header using the `username` and `password` provided. + /// + /// - Parameters: + /// - username: The username of the header. + /// - password: The password of the header. + /// - Returns: The header. + public static func authorization(username: String, password: String) -> HTTPHeader { + let credential = Data("\(username):\(password)".utf8).base64EncodedString() + + return authorization("Basic \(credential)") + } + + /// Returns a `Bearer` `Authorization` header using the `bearerToken` provided + /// + /// - Parameter bearerToken: The bearer token. + /// - Returns: The header. + public static func authorization(bearerToken: String) -> HTTPHeader { + return authorization("Bearer \(bearerToken)") + } + + /// Returns an `Authorization` header. + /// + /// Alamofire provides built-in methods to produce `Authorization` headers. For a Basic `Authorization` header use + /// `HTTPHeader.authorization(username: password:)`. For a Bearer `Authorization` header, use + /// `HTTPHeader.authorization(bearerToken:)`. + /// + /// - Parameter value: The `Authorization` value. + /// - Returns: The header. + public static func authorization(_ value: String) -> HTTPHeader { + return HTTPHeader(name: "Authorization", value: value) + } + + /// Returns a `Content-Disposition` header. + /// + /// - Parameter value: The `Content-Disposition` value. + /// - Returns: The header. + public static func contentDisposition(_ value: String) -> HTTPHeader { + return HTTPHeader(name: "Content-Disposition", value: value) + } + + /// Returns a `Content-Type` header. + /// + /// All Alamofire `ParameterEncoding`s set the `Content-Type` of the request, so it may not be necessary to manually + /// set this value. + /// + /// - Parameter value: The `Content-Type` value. + /// - Returns: The header. + public static func contentType(_ value: String) -> HTTPHeader { + return HTTPHeader(name: "Content-Type", value: value) + } + + /// Returns a `User-Agent` header. + /// + /// - Parameter value: The `User-Agent` value. + /// - Returns: The header. + public static func userAgent(_ value: String) -> HTTPHeader { + return HTTPHeader(name: "User-Agent", value: value) + } +} + +extension Array where Element == HTTPHeader { + /// Case-insensitively finds the index of an `HTTPHeader` with the provided name, if it exists. + func index(of name: String) -> Int? { + let lowercasedName = name.lowercased() + return firstIndex { $0.name.lowercased() == lowercasedName } + } +} + +// MARK: - Defaults + +extension HTTPHeaders { + /// The default set of `HTTPHeaders` used by Alamofire. Includes `Accept-Encoding`, `Accept-Language`, and + /// `User-Agent`. + public static let `default`: HTTPHeaders = [.defaultAcceptEncoding, + .defaultAcceptLanguage, + .defaultUserAgent] +} + +extension HTTPHeader { + /// Returns Alamofire's default `Accept-Encoding` header, appropriate for the encodings supporte by particular OS + /// versions. + /// + /// See the [Accept-Encoding HTTP header documentation](https://tools.ietf.org/html/rfc7230#section-4.2.3) . + public static let defaultAcceptEncoding: HTTPHeader = { + let encodings: [String] + if #available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *) { + encodings = ["br", "gzip", "deflate"] + } else { + encodings = ["gzip", "deflate"] + } + + return .acceptEncoding(encodings.qualityEncoded) + }() + + /// Returns Alamofire's default `Accept-Language` header, generated by querying `Locale` for the user's + /// `preferredLanguages`. + /// + /// See the [Accept-Language HTTP header documentation](https://tools.ietf.org/html/rfc7231#section-5.3.5). + public static let defaultAcceptLanguage: HTTPHeader = { + .acceptLanguage(Locale.preferredLanguages.prefix(6).qualityEncoded) + }() + + /// Returns Alamofire's default `User-Agent` header. + /// + /// See the [User-Agent header documentation](https://tools.ietf.org/html/rfc7231#section-5.5.3). + /// + /// Example: `iOS Example/1.0 (org.alamofire.iOS-Example; build:1; iOS 12.0.0) Alamofire/5.0.0` + public static let defaultUserAgent: HTTPHeader = { let userAgent: String = { if let info = Bundle.main.infoDictionary { let executable = info[kCFBundleExecutableKey as String] as? String ?? "Unknown" @@ -103,8 +396,40 @@ extension Dictionary where Key == String, Value == String { return "Alamofire" }() - return ["Accept-Encoding": acceptEncoding, - "Accept-Language": acceptLanguage, - "User-Agent": userAgent] + return .userAgent(userAgent) }() } + +extension Collection where Element == String { + var qualityEncoded: String { + return enumerated().map { (index, encoding) in + let quality = 1.0 - (Double(index) * 0.1) + return "\(encoding);q=\(quality)" + }.joined(separator: ", ") + } +} + +// MARK: - System Type Extensions + +extension URLRequest { + /// Returns `allHTTPHeaderFields` as `HTTPHeaders`. + public var httpHeaders: HTTPHeaders { + get { return allHTTPHeaderFields.map(HTTPHeaders.init) ?? HTTPHeaders() } + set { allHTTPHeaderFields = newValue.dictionary } + } +} + +extension HTTPURLResponse { + /// Returns `allHeaderFields` as `HTTPHeaders`. + public var httpHeaders: HTTPHeaders { + return (allHeaderFields as? [String: String]).map(HTTPHeaders.init) ?? HTTPHeaders() + } +} + +extension URLSessionConfiguration { + /// Returns `httpAdditionalHeaders` as `HTTPHeaders`. + public var httpHeaders: HTTPHeaders { + get { return (httpAdditionalHeaders as? [String: String]).map(HTTPHeaders.init) ?? HTTPHeaders() } + set { httpAdditionalHeaders = newValue.dictionary } + } +} diff --git a/Source/MultipartFormData.swift b/Source/MultipartFormData.swift index e56266eb7..9b258f756 100644 --- a/Source/MultipartFormData.swift +++ b/Source/MultipartFormData.swift @@ -421,12 +421,9 @@ open class MultipartFormData { } private func encodeHeaders(for bodyPart: BodyPart) -> Data { - var headerText = "" - - for (key, value) in bodyPart.headers { - headerText += "\(key): \(value)\(EncodingCharacters.crlf)" - } - headerText += EncodingCharacters.crlf + let headerText = bodyPart.headers.map { "\($0.name): \($0.value)\(EncodingCharacters.crlf)" } + .joined() + + EncodingCharacters.crlf return Data(headerText.utf8) } @@ -549,12 +546,12 @@ open class MultipartFormData { // MARK: - Private - Content Headers - private func contentHeaders(withName name: String, fileName: String? = nil, mimeType: String? = nil) -> [String: String] { + private func contentHeaders(withName name: String, fileName: String? = nil, mimeType: String? = nil) -> HTTPHeaders { var disposition = "form-data; name=\"\(name)\"" if let fileName = fileName { disposition += "; filename=\"\(fileName)\"" } - var headers = ["Content-Disposition": disposition] - if let mimeType = mimeType { headers["Content-Type"] = mimeType } + var headers: HTTPHeaders = [.contentDisposition(disposition)] + if let mimeType = mimeType { headers.add(.contentType(mimeType)) } return headers } diff --git a/Source/Response.swift b/Source/Response.swift index 41ec64fe5..142a178a1 100644 --- a/Source/Response.swift +++ b/Source/Response.swift @@ -89,20 +89,19 @@ extension DataResponse: CustomStringConvertible, CustomDebugStringConvertible { public var debugDescription: String { let requestDescription = request.map { "\($0.httpMethod!) \($0)" } ?? "nil" let responseDescription = response.map { (response) in - let headers = response.allHeaderFields as! HTTPHeaders - let keys = headers.keys.sorted(by: >) - let sortedHeaders = keys.map { "\($0): \(headers[$0]!)" }.joined(separator: "\n") + let sortedHeaders = response.httpHeaders.sorted() return """ - Status Code: \(response.statusCode) - Headers: \(sortedHeaders) - """ + [Status Code]: \(response.statusCode) + [Headers]: + \(sortedHeaders) + """ } ?? "nil" let metricsDescription = metrics.map { "\($0.taskInterval.duration)s" } ?? "None" return """ [Request]: \(requestDescription) - [Response]: \(responseDescription) + [Response]: \n\(responseDescription) [Data]: \(data?.description ?? "None") [Network Duration]: \(metricsDescription) [Serialization Duration]: \(serializationDuration)s @@ -274,21 +273,20 @@ extension DownloadResponse: CustomStringConvertible, CustomDebugStringConvertibl public var debugDescription: String { let requestDescription = request.map { "\($0.httpMethod!) \($0)" } ?? "nil" let responseDescription = response.map { (response) in - let headers = response.allHeaderFields as! HTTPHeaders - let keys = headers.keys.sorted(by: >) - let sortedHeaders = keys.map { "\($0): \(headers[$0]!)" }.joined(separator: "\n") + let sortedHeaders = response.httpHeaders.sorted() return """ - Status Code: \(response.statusCode) - Headers: \(sortedHeaders) - """ + [Status Code]: \(response.statusCode) + [Headers]: + \(sortedHeaders) + """ } ?? "nil" let metricsDescription = metrics.map { "\($0.taskInterval.duration)s" } ?? "None" let resumeDataDescription = resumeData.map { "\($0)" } ?? "None" return """ [Request]: \(requestDescription) - [Response]: \(responseDescription) + [Response]: \n\(responseDescription) [File URL]: \(fileURL?.path ?? "nil") [ResumeData]: \(resumeDataDescription) [Network Duration]: \(metricsDescription) diff --git a/Source/ServerTrustEvaluation.swift b/Source/ServerTrustEvaluation.swift index 9e5f2689b..17fede880 100644 --- a/Source/ServerTrustEvaluation.swift +++ b/Source/ServerTrustEvaluation.swift @@ -122,7 +122,7 @@ public final class DefaultTrustEvaluator: ServerTrustEvaluating { if validateHost { try trust.validateHost(host) } - + try trust.performDefaultEvaluation(forHost: host) } } @@ -188,7 +188,7 @@ public final class RevocationTrustEvaluator: ServerTrustEvaluating { if performDefaultValidation { try trust.performDefaultEvaluation(forHost: host) } - + if validateHost { try trust.validateHost(host) } @@ -237,19 +237,19 @@ public final class PinnedCertificatesTrustEvaluator: ServerTrustEvaluating { guard !certificates.isEmpty else { throw AFError.serverTrustEvaluationFailed(reason: .noCertificatesFound) } - + if acceptSelfSignedCertificates { try trust.setAnchorCertificates(certificates) } - + if performDefaultValidation { try trust.performDefaultEvaluation(forHost: host) } - + if validateHost { try trust.validateHost(host) } - + let serverCertificatesData = Set(trust.certificateData) let pinnedCertificatesData = Set(certificates.data) let pinnedCertificatesInServerData = !serverCertificatesData.isDisjoint(with: pinnedCertificatesData) @@ -292,7 +292,7 @@ public final class PublicKeysTrustEvaluator: ServerTrustEvaluating { self.performDefaultValidation = performDefaultValidation self.validateHost = validateHost } - + public func evaluate(_ trust: SecTrust, forHost host: String) throws { guard !keys.isEmpty else { throw AFError.serverTrustEvaluationFailed(reason: .noPublicKeysFound) @@ -301,7 +301,7 @@ public final class PublicKeysTrustEvaluator: ServerTrustEvaluating { if performDefaultValidation { try trust.performDefaultEvaluation(forHost: host) } - + if validateHost { try trust.validateHost(host) } @@ -435,13 +435,13 @@ public extension SecTrust { SecTrustGetCertificateAtIndex(self, index) } } - + func performDefaultEvaluation(forHost host: String) throws { try validate(policy: .default) { (status, result) in AFError.serverTrustEvaluationFailed(reason: .defaultEvaluationFailed(output: .init(host, self, status, result))) } } - + func validateHost(_ host: String) throws { try validate(policy: .hostname(host)) { (status, result) in AFError.serverTrustEvaluationFailed(reason: .hostValidationFailed(output: .init(host, self, status, result))) @@ -468,7 +468,7 @@ extension Array where Element == SecCertificate { var data: [Data] { return map { SecCertificateCopyData($0) as Data } } - + /// All public `SecKey` values for the contained `SecCertificate`s. public var publicKeys: [SecKey] { return compactMap { $0.publicKey } diff --git a/Source/URLConvertible+URLRequestConvertible.swift b/Source/URLConvertible+URLRequestConvertible.swift index 201a1694d..369e8e4e1 100644 --- a/Source/URLConvertible+URLRequestConvertible.swift +++ b/Source/URLConvertible+URLRequestConvertible.swift @@ -100,11 +100,6 @@ extension URLRequest { self.init(url: url) httpMethod = method.rawValue - - if let headers = headers { - for (headerField, headerValue) in headers { - setValue(headerValue, forHTTPHeaderField: headerField) - } - } + allHTTPHeaderFields = headers?.dictionary } } diff --git a/Source/URLSessionConfiguration+Alamofire.swift b/Source/URLSessionConfiguration+Alamofire.swift index b21d5d3e1..eb8331768 100644 --- a/Source/URLSessionConfiguration+Alamofire.swift +++ b/Source/URLSessionConfiguration+Alamofire.swift @@ -27,7 +27,7 @@ import Foundation extension URLSessionConfiguration { public static var alamofireDefault: URLSessionConfiguration { let configuration = URLSessionConfiguration.default - configuration.httpAdditionalHeaders = HTTPHeaders.defaultHTTPHeaders + configuration.httpHeaders = .default return configuration } diff --git a/Tests/AuthenticationTests.swift b/Tests/AuthenticationTests.swift index e58ecfaca..965473632 100644 --- a/Tests/AuthenticationTests.swift +++ b/Tests/AuthenticationTests.swift @@ -113,7 +113,7 @@ class BasicAuthenticationTestCase: AuthenticationTestCase { // Given let urlString = "http://httpbin.org/hidden-basic-auth/\(user)/\(password)" let expectation = self.expectation(description: "\(urlString) 200") - let headers = HTTPHeaders.authorization(username: user, password: password) + let headers: HTTPHeaders = [.authorization(username: user, password: password)] var response: DataResponse? diff --git a/Tests/CacheTests.swift b/Tests/CacheTests.swift index 5ab51f3b7..7b5a18951 100644 --- a/Tests/CacheTests.swift +++ b/Tests/CacheTests.swift @@ -94,7 +94,7 @@ class CacheTestCase: BaseTestCase { manager = { let configuration: URLSessionConfiguration = { let configuration = URLSessionConfiguration.default - configuration.httpAdditionalHeaders = HTTPHeaders.defaultHTTPHeaders + configuration.httpHeaders = HTTPHeaders.default configuration.requestCachePolicy = .useProtocolCachePolicy configuration.urlCache = urlCache diff --git a/Tests/DownloadTests.swift b/Tests/DownloadTests.swift index 574afc2aa..bd2fb811a 100644 --- a/Tests/DownloadTests.swift +++ b/Tests/DownloadTests.swift @@ -49,7 +49,7 @@ class DownloadInitializationTestCase: BaseTestCase { func testDownloadClassMethodWithMethodURLHeadersAndDestination() { // Given let urlString = "https://httpbin.org/get" - let headers = ["Authorization": "123456"] + let headers: HTTPHeaders = ["Authorization": "123456"] let expectation = self.expectation(description: "download should complete") // When @@ -224,7 +224,7 @@ class DownloadResponseTestCase: BaseTestCase { // Given let fileURL = randomCachesFileURL let urlString = "https://httpbin.org/get" - let headers = ["Authorization": "123456"] + let headers: HTTPHeaders = ["Authorization": "123456"] let destination: DownloadRequest.Destination = { _, _ in (fileURL, []) } let expectation = self.expectation(description: "Download request should download data to file: \(fileURL)") diff --git a/Tests/HTTPHeadersTests.swift b/Tests/HTTPHeadersTests.swift new file mode 100644 index 000000000..038153ffe --- /dev/null +++ b/Tests/HTTPHeadersTests.swift @@ -0,0 +1,133 @@ +// +// HTTPHeadersTests.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Alamofire +import XCTest + +class HTTPHeadersTests: BaseTestCase { + func testHeadersAreStoreUniquelyByCaseInsensitiveName() { + // Given + let headersFromDictionaryLiteral: HTTPHeaders = ["key": "", "Key": "", "KEY": ""] + let headersFromDictionary = HTTPHeaders(["key": "", "Key": "", "KEY": ""]) + let headersFromArrayLiteral: HTTPHeaders = [HTTPHeader(name: "key", value: ""), + HTTPHeader(name: "Key", value: ""), + HTTPHeader(name: "KEY", value: "")] + let headersFromArray = HTTPHeaders([HTTPHeader(name: "key", value: ""), + HTTPHeader(name: "Key", value: ""), + HTTPHeader(name: "KEY", value: "")]) + var headersCreatedManually = HTTPHeaders() + headersCreatedManually.update(HTTPHeader(name: "key", value: "")) + headersCreatedManually.update(name: "Key", value: "") + headersCreatedManually.update(name: "KEY", value: "") + + // When, Then + XCTAssertEqual(headersFromDictionaryLiteral.count, 1) + XCTAssertEqual(headersFromDictionary.count, 1) + XCTAssertEqual(headersFromArrayLiteral.count, 1) + XCTAssertEqual(headersFromArray.count, 1) + XCTAssertEqual(headersCreatedManually.count, 1) + } + + func testHeadersPreserveOrderOfInsertion() { + // Given + let headersFromDictionaryLiteral: HTTPHeaders = ["c": "", "a": "", "b": ""] + // Dictionary initializer can't preserve order. + let headersFromArrayLiteral: HTTPHeaders = [HTTPHeader(name: "b", value: ""), + HTTPHeader(name: "a", value: ""), + HTTPHeader(name: "c", value: "")] + let headersFromArray = HTTPHeaders([HTTPHeader(name: "b", value: ""), + HTTPHeader(name: "a", value: ""), + HTTPHeader(name: "c", value: "")]) + var headersCreatedManually = HTTPHeaders() + headersCreatedManually.update(HTTPHeader(name: "c", value: "")) + headersCreatedManually.update(name: "b", value: "") + headersCreatedManually.update(name: "a", value: "") + + // When + let dictionaryLiteralNames = headersFromDictionaryLiteral.map { $0.name } + let arrayLiteralNames = headersFromArrayLiteral.map { $0.name } + let arrayNames = headersFromArray.map { $0.name } + let manualNames = headersCreatedManually.map { $0.name } + + // Then + XCTAssertEqual(dictionaryLiteralNames, ["c", "a", "b"]) + XCTAssertEqual(arrayLiteralNames, ["b", "a", "c"]) + XCTAssertEqual(arrayNames, ["b", "a", "c"]) + XCTAssertEqual(manualNames, ["c", "b", "a"]) + } + + func testHeadersCanBeProperlySortedByName() { + // Given + let headers: HTTPHeaders = ["c": "", "a": "", "b": ""] + + // When + let sortedHeaders = headers.sorted() + + // Then + XCTAssertEqual(headers.map { $0.name }, ["c", "a", "b"]) + XCTAssertEqual(sortedHeaders.map { $0.name }, ["a", "b", "c"]) + } + + func testHeadersCanInsensitivelyGetAndSetThroughSubscript() { + // Given + var headers: HTTPHeaders = ["c": "", "a": "", "b": ""] + + // When + headers["C"] = "c" + headers["a"] = "a" + headers["b"] = "b" + + // Then + XCTAssertEqual(headers["c"], "c") + XCTAssertEqual(headers.map { $0.value }, ["c", "a", "b"]) + XCTAssertEqual(headers.count, 3) + } + + func testHeadersPreserveLastFormAndValueOfAName() { + // Given + var headers: HTTPHeaders = ["c": "a"] + + // When + headers["C"] = "c" + + // Then + XCTAssertEqual(headers.description, "C: c") + } + + func testHeadersHaveUnsortedDescription() { + // Given + let headers: HTTPHeaders = ["c": "c", "a": "a", "b": "b"] + + // When + let description = headers.description + let expectedDescription = """ + c: c + a: a + b: b + """ + + // Then + XCTAssertEqual(description, expectedDescription) + } +} diff --git a/Tests/MultipartFormDataTests.swift b/Tests/MultipartFormDataTests.swift index d994e7d2d..91251a2da 100644 --- a/Tests/MultipartFormDataTests.swift +++ b/Tests/MultipartFormDataTests.swift @@ -160,12 +160,12 @@ class MultipartFormDataEncodingTestCase: BaseTestCase { "Content-Disposition: form-data; name=\"french\"\(crlf)\(crlf)" + "français" + BoundaryGenerator.boundary(forBoundaryType: .encapsulated, boundaryKey: boundary) + - "Content-Type: text/plain\(crlf)" + - "Content-Disposition: form-data; name=\"japanese\"\(crlf)\(crlf)" + + "Content-Disposition: form-data; name=\"japanese\"\(crlf)" + + "Content-Type: text/plain\(crlf)\(crlf)" + "日本語" + BoundaryGenerator.boundary(forBoundaryType: .encapsulated, boundaryKey: boundary) + - "Content-Type: text/plain\(crlf)" + - "Content-Disposition: form-data; name=\"emoji\"\(crlf)\(crlf)" + + "Content-Disposition: form-data; name=\"emoji\"\(crlf)" + + "Content-Type: text/plain\(crlf)\(crlf)" + "😃👍🏻🍻🎉" + BoundaryGenerator.boundary(forBoundaryType: .final, boundaryKey: boundary) ) @@ -200,8 +200,8 @@ class MultipartFormDataEncodingTestCase: BaseTestCase { var expectedData = Data() expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedData.append(Data(( - "Content-Type: image/png\(crlf)" + - "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)" + + "Content-Type: image/png\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: unicornImageURL)) @@ -239,15 +239,15 @@ class MultipartFormDataEncodingTestCase: BaseTestCase { var expectedData = Data() expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedData.append(Data(( - "Content-Type: image/png\(crlf)" + - "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)" + + "Content-Type: image/png\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: unicornImageURL)) expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedData.append(Data(( - "Content-Type: image/jpeg\(crlf)" + - "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)" + + "Content-Type: image/jpeg\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: rainbowImageURL)) @@ -291,8 +291,8 @@ class MultipartFormDataEncodingTestCase: BaseTestCase { var expectedData = Data() expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedData.append(Data(( - "Content-Type: image/png\(crlf)" + - "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)" + + "Content-Type: image/png\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: unicornImageURL)) @@ -347,15 +347,15 @@ class MultipartFormDataEncodingTestCase: BaseTestCase { var expectedData = Data() expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedData.append(Data(( - "Content-Type: image/png\(crlf)" + - "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)" + + "Content-Type: image/png\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: unicornImageURL)) expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedData.append(Data(( - "Content-Type: image/jpeg\(crlf)" + - "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)" + + "Content-Type: image/jpeg\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: rainbowImageURL)) @@ -411,15 +411,15 @@ class MultipartFormDataEncodingTestCase: BaseTestCase { expectedData.append(loremData) expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedData.append(Data(( - "Content-Type: image/png\(crlf)" + - "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)" + + "Content-Type: image/png\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: unicornImageURL)) expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedData.append(Data(( - "Content-Type: image/jpeg\(crlf)" + - "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)" + + "Content-Type: image/jpeg\(crlf)\(crlf)").utf8 ) ) expectedData.append(try! Data(contentsOf: rainbowImageURL)) @@ -544,8 +544,8 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase { var expectedFileData = Data() expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedFileData.append(Data(( - "Content-Type: image/png\(crlf)" + - "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)" + + "Content-Type: image/png\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: unicornImageURL)) @@ -586,15 +586,15 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase { var expectedFileData = Data() expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedFileData.append(Data(( - "Content-Type: image/png\(crlf)" + - "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)" + + "Content-Type: image/png\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: unicornImageURL)) expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedFileData.append(Data(( - "Content-Type: image/jpeg\(crlf)" + - "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)" + + "Content-Type: image/jpeg\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: rainbowImageURL)) @@ -641,8 +641,8 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase { var expectedFileData = Data() expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedFileData.append(Data(( - "Content-Type: image/png\(crlf)" + - "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)" + + "Content-Type: image/png\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: unicornImageURL)) @@ -701,15 +701,15 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase { var expectedFileData = Data() expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary)) expectedFileData.append(Data(( - "Content-Type: image/png\(crlf)" + - "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)" + + "Content-Type: image/png\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: unicornImageURL)) expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedFileData.append(Data(( - "Content-Type: image/jpeg\(crlf)" + - "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)" + + "Content-Type: image/jpeg\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: rainbowImageURL)) @@ -768,15 +768,15 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase { expectedFileData.append(loremData) expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedFileData.append(Data(( - "Content-Type: image/png\(crlf)" + - "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"unicorn\"; filename=\"unicorn.png\"\(crlf)" + + "Content-Type: image/png\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: unicornImageURL)) expectedFileData.append(BoundaryGenerator.boundaryData(boundaryType: .encapsulated, boundaryKey: boundary)) expectedFileData.append(Data(( - "Content-Type: image/jpeg\(crlf)" + - "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)\(crlf)").utf8 + "Content-Disposition: form-data; name=\"rainbow\"; filename=\"rainbow.jpg\"\(crlf)" + + "Content-Type: image/jpeg\(crlf)\(crlf)").utf8 ) ) expectedFileData.append(try! Data(contentsOf: rainbowImageURL)) diff --git a/Tests/RequestTests.swift b/Tests/RequestTests.swift index dd54a0991..4c69817c4 100644 --- a/Tests/RequestTests.swift +++ b/Tests/RequestTests.swift @@ -248,11 +248,11 @@ class RequestDebugDescriptionTestCase: BaseTestCase { }() let managerWithAcceptLanguageHeader: Session = { - var headers = HTTPHeaders.defaultHTTPHeaders + var headers = HTTPHeaders.default headers["Accept-Language"] = "en-US" let configuration = URLSessionConfiguration.alamofireDefault - configuration.httpAdditionalHeaders = headers + configuration.httpHeaders = headers let manager = Session(configuration: configuration) @@ -260,11 +260,11 @@ class RequestDebugDescriptionTestCase: BaseTestCase { }() let managerWithContentTypeHeader: Session = { - var headers = HTTPHeaders.defaultHTTPHeaders + var headers = HTTPHeaders.default headers["Content-Type"] = "application/json" let configuration = URLSessionConfiguration.alamofireDefault - configuration.httpAdditionalHeaders = headers + configuration.httpHeaders = headers let manager = Session(configuration: configuration) @@ -313,7 +313,7 @@ class RequestDebugDescriptionTestCase: BaseTestCase { let expectation = self.expectation(description: "request should complete") // When - let headers: [String: String] = [ "X-Custom-Header": "{\"key\": \"value\"}" ] + let headers: HTTPHeaders = [ "X-Custom-Header": "{\"key\": \"value\"}" ] let request = manager.request(urlString, headers: headers).response { _ in expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) @@ -328,7 +328,7 @@ class RequestDebugDescriptionTestCase: BaseTestCase { let expectation = self.expectation(description: "request should complete") // When - let headers = [ "Accept-Language": "en-GB" ] + let headers: HTTPHeaders = [ "Accept-Language": "en-GB" ] let request = managerWithAcceptLanguageHeader.request(urlString, headers: headers).response { _ in expectation.fulfill() } waitForExpectations(timeout: timeout, handler: nil) diff --git a/Tests/ResponseSerializationTests.swift b/Tests/ResponseSerializationTests.swift index 992c575f2..f4492e344 100644 --- a/Tests/ResponseSerializationTests.swift +++ b/Tests/ResponseSerializationTests.swift @@ -1051,6 +1051,6 @@ class DownloadResponseSerializationTestCase: BaseTestCase { extension HTTPURLResponse { convenience init(statusCode: Int, headers: HTTPHeaders? = nil) { let url = URL(string: "https://httpbin.org/get")! - self.init(url: url, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: headers)! + self.init(url: url, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: headers?.dictionary)! } } diff --git a/Tests/ServerTrustEvaluatorTests.swift b/Tests/ServerTrustEvaluatorTests.swift index de4d792c6..3854f313b 100644 --- a/Tests/ServerTrustEvaluatorTests.swift +++ b/Tests/ServerTrustEvaluatorTests.swift @@ -176,7 +176,7 @@ extension SecTrust { var isValid: Bool { var result = SecTrustResultType.invalid let status = SecTrustEvaluate(self, &result) - + return (status == errSecSuccess) ? (result == .unspecified || result == .proceed) : false } } diff --git a/Tests/SessionManagerTests.swift b/Tests/SessionTests.swift similarity index 98% rename from Tests/SessionManagerTests.swift rename to Tests/SessionTests.swift index 8d2f243f6..5ebb6c072 100644 --- a/Tests/SessionManagerTests.swift +++ b/Tests/SessionTests.swift @@ -1,5 +1,5 @@ // -// SessionManagerTests.swift +// SessionTests.swift // // Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) // @@ -26,7 +26,7 @@ import Foundation import XCTest -class SessionManagerTestCase: BaseTestCase { +class SessionTestCase: BaseTestCase { // MARK: Helper Types @@ -42,13 +42,13 @@ class SessionManagerTestCase: BaseTestCase { func adapt(_ urlRequest: URLRequest, completion: @escaping (Result) -> Void) { let result: Result = Result { guard !throwsError else { throw AFError.invalidURL(url: "") } - + var urlRequest = urlRequest urlRequest.httpMethod = method.rawValue - + return urlRequest } - + completion(result) } } @@ -67,20 +67,18 @@ class SessionManagerTestCase: BaseTestCase { throwsErrorOnSecondAdapt = false throw AFError.invalidURL(url: "") } - + var urlRequest = urlRequest - + adaptedCount += 1 - + if shouldApplyAuthorizationHeader && adaptedCount > 1 { - if let header = HTTPHeaders.authorization(username: "user", password: "password").first { - urlRequest.setValue(header.value, forHTTPHeaderField: header.key) - } + urlRequest.httpHeaders.update(.authorization(username: "user", password: "password")) } - + return urlRequest } - + completion(result) } @@ -104,12 +102,12 @@ class SessionManagerTestCase: BaseTestCase { func adapt(_ urlRequest: URLRequest, completion: @escaping (Result) -> Void) { let result: Result = Result { adaptedCount += 1 - + if adaptedCount == 1 { throw AFError.invalidURL(url: "") } - + return urlRequest } - + completion(result) } @@ -195,7 +193,7 @@ class SessionManagerTestCase: BaseTestCase { func testDefaultUserAgentHeader() { // Given, When - let userAgent = HTTPHeaders.defaultHTTPHeaders["User-Agent"] + let userAgent = HTTPHeaders.default["User-Agent"] // Then let osNameVersion: String = { @@ -920,9 +918,9 @@ class SessionManagerConfigurationHeadersTestCase: BaseTestCase { configuration = .background(withIdentifier: identifier) } - var headers = HTTPHeaders.defaultHTTPHeaders + var headers = HTTPHeaders.default headers["Authorization"] = "Bearer 123456" - configuration.httpAdditionalHeaders = headers + configuration.httpHeaders = headers return configuration }() diff --git a/Tests/URLProtocolTests.swift b/Tests/URLProtocolTests.swift index 11e78c908..7cf486ae7 100644 --- a/Tests/URLProtocolTests.swift +++ b/Tests/URLProtocolTests.swift @@ -37,7 +37,7 @@ class ProxyURLProtocol: URLProtocol { lazy var session: URLSession = { let configuration: URLSessionConfiguration = { let configuration = URLSessionConfiguration.ephemeral - configuration.httpAdditionalHeaders = HTTPHeaders.defaultHTTPHeaders + configuration.httpHeaders = HTTPHeaders.default return configuration }() diff --git a/Tests/UploadTests.swift b/Tests/UploadTests.swift index edbb7162b..eaff24665 100644 --- a/Tests/UploadTests.swift +++ b/Tests/UploadTests.swift @@ -50,7 +50,7 @@ class UploadFileInitializationTestCase: BaseTestCase { func testUploadClassMethodWithMethodURLHeadersAndFile() { // Given let urlString = "https://httpbin.org/post" - let headers = ["Authorization": "123456"] + let headers: HTTPHeaders = ["Authorization": "123456"] let imageURL = url(forResource: "rainbow", withExtension: "jpg") let expectation = self.expectation(description: "upload should complete") @@ -98,7 +98,7 @@ class UploadDataInitializationTestCase: BaseTestCase { func testUploadClassMethodWithMethodURLHeadersAndData() { // Given let urlString = "https://httpbin.org/post" - let headers = ["Authorization": "123456"] + let headers: HTTPHeaders = ["Authorization": "123456"] let expectation = self.expectation(description: "upload should complete") // When @@ -148,7 +148,7 @@ class UploadStreamInitializationTestCase: BaseTestCase { // Given let urlString = "https://httpbin.org/post" let imageURL = url(forResource: "rainbow", withExtension: "jpg") - let headers = ["Authorization": "123456"] + let headers: HTTPHeaders = ["Authorization": "123456"] let imageStream = InputStream(url: imageURL)! let expectation = self.expectation(description: "upload should complete") diff --git a/Tests/ValidationTests.swift b/Tests/ValidationTests.swift index c9bbc4093..216363f70 100644 --- a/Tests/ValidationTests.swift +++ b/Tests/ValidationTests.swift @@ -395,7 +395,7 @@ class ContentTypeValidationTestCase: BaseTestCase { let manager: Session = { let configuration: URLSessionConfiguration = { let configuration = URLSessionConfiguration.ephemeral - configuration.httpAdditionalHeaders = HTTPHeaders.defaultHTTPHeaders + configuration.httpHeaders = HTTPHeaders.default return configuration }()