| // This source file is part of the Swift.org open source project |
| // |
| // Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors |
| // Licensed under Apache License v2.0 with Runtime Library Exception |
| // |
| // See http://swift.org/LICENSE.txt for license information |
| // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors |
| // |
| |
| |
| /// An `NSURLResponse` object represents a URL load response in a |
| /// manner independent of protocol and URL scheme. |
| /// |
| /// `NSURLResponse` encapsulates the metadata associated |
| /// with a URL load. Note that NSURLResponse objects do not contain |
| /// the actual bytes representing the content of a URL. See |
| /// `NSURLSession` for more information about receiving the content |
| /// data for a URL load. |
| open class URLResponse : NSObject, NSSecureCoding, NSCopying { |
| |
| static public var supportsSecureCoding: Bool { |
| return true |
| } |
| |
| public required init?(coder aDecoder: NSCoder) { |
| guard aDecoder.allowsKeyedCoding else { |
| preconditionFailure("Unkeyed coding is unsupported.") |
| } |
| |
| if let encodedUrl = aDecoder.decodeObject(forKey: "NS.url") as? NSURL { |
| self.url = encodedUrl._swiftObject |
| } |
| |
| if let encodedMimeType = aDecoder.decodeObject(forKey: "NS.mimeType") as? NSString { |
| self.mimeType = encodedMimeType._swiftObject |
| } |
| |
| self.expectedContentLength = aDecoder.decodeInt64(forKey: "NS.expectedContentLength") |
| |
| if let encodedEncodingName = aDecoder.decodeObject(forKey: "NS.textEncodingName") as? NSString { |
| self.textEncodingName = encodedEncodingName._swiftObject |
| } |
| |
| if let encodedFilename = aDecoder.decodeObject(forKey: "NS.suggestedFilename") as? NSString { |
| self.suggestedFilename = encodedFilename._swiftObject |
| } |
| } |
| |
| open func encode(with aCoder: NSCoder) { |
| guard aCoder.allowsKeyedCoding else { |
| preconditionFailure("Unkeyed coding is unsupported.") |
| } |
| |
| aCoder.encode(self.url?._bridgeToObjectiveC(), forKey: "NS.url") |
| aCoder.encode(self.mimeType?._bridgeToObjectiveC(), forKey: "NS.mimeType") |
| aCoder.encode(self.expectedContentLength, forKey: "NS.expectedContentLength") |
| aCoder.encode(self.textEncodingName?._bridgeToObjectiveC(), forKey: "NS.textEncodingName") |
| aCoder.encode(self.suggestedFilename?._bridgeToObjectiveC(), forKey: "NS.suggestedFilename") |
| } |
| |
| open override func copy() -> Any { |
| return copy(with: nil) |
| } |
| |
| open func copy(with zone: NSZone? = nil) -> Any { |
| return self |
| } |
| |
| /// Initialize an NSURLResponse with the provided values. |
| /// |
| /// This is the designated initializer for NSURLResponse. |
| /// - Parameter URL: the URL |
| /// - Parameter mimeType: the MIME content type of the response |
| /// - Parameter expectedContentLength: the expected content length of the associated data |
| /// - Parameter textEncodingName the name of the text encoding for the associated data, if applicable, else nil |
| /// - Returns: The initialized NSURLResponse. |
| public init(url: URL, mimeType: String?, expectedContentLength length: Int, textEncodingName name: String?) { |
| self.url = url |
| self.mimeType = mimeType |
| self.expectedContentLength = Int64(length) |
| self.textEncodingName = name |
| let c = url.lastPathComponent |
| self.suggestedFilename = c.isEmpty ? "Unknown" : c |
| } |
| |
| /// The URL of the receiver. |
| /*@NSCopying*/ open private(set) var url: URL? |
| |
| |
| /// The MIME type of the receiver. |
| /// |
| /// The MIME type is based on the information provided |
| /// from an origin source. However, that value may be changed or |
| /// corrected by a protocol implementation if it can be determined |
| /// that the origin server or source reported the information |
| /// incorrectly or imprecisely. An attempt to guess the MIME type may |
| /// be made if the origin source did not report any such information. |
| open fileprivate(set) var mimeType: String? |
| |
| /// The expected content length of the receiver. |
| /// |
| /// Some protocol implementations report a content length |
| /// as part of delivering load metadata, but not all protocols |
| /// guarantee the amount of data that will be delivered in actuality. |
| /// Hence, this method returns an expected amount. Clients should use |
| /// this value as an advisory, and should be prepared to deal with |
| /// either more or less data. |
| /// |
| /// The expected content length of the receiver, or `-1` if |
| /// there is no expectation that can be arrived at regarding expected |
| /// content length. |
| open fileprivate(set) var expectedContentLength: Int64 |
| |
| /// The name of the text encoding of the receiver. |
| /// |
| /// This name will be the actual string reported by the |
| /// origin source during the course of performing a protocol-specific |
| /// URL load. Clients can inspect this string and convert it to an |
| /// NSStringEncoding or CFStringEncoding using the methods and |
| /// functions made available in the appropriate framework. |
| open fileprivate(set) var textEncodingName: String? |
| |
| /// A suggested filename if the resource were saved to disk. |
| /// |
| /// The method first checks if the server has specified a filename |
| /// using the content disposition header. If no valid filename is |
| /// specified using that mechanism, this method checks the last path |
| /// component of the URL. If no valid filename can be obtained using |
| /// the last path component, this method uses the URL's host as the |
| /// filename. If the URL's host can't be converted to a valid |
| /// filename, the filename "unknown" is used. In mose cases, this |
| /// method appends the proper file extension based on the MIME type. |
| /// |
| /// This method always returns a valid filename. |
| open fileprivate(set) var suggestedFilename: String? |
| } |
| |
| /// A Response to an HTTP URL load. |
| /// |
| /// An NSHTTPURLResponse object represents a response to an |
| /// HTTP URL load. It is a specialization of NSURLResponse which |
| /// provides conveniences for accessing information specific to HTTP |
| /// protocol responses. |
| open class HTTPURLResponse : URLResponse { |
| |
| /// Initializer for NSHTTPURLResponse objects. |
| /// |
| /// - Parameter url: the URL from which the response was generated. |
| /// - Parameter statusCode: an HTTP status code. |
| /// - Parameter httpVersion: The version of the HTTP response as represented by the server. This is typically represented as "HTTP/1.1". |
| /// - Parameter headerFields: A dictionary representing the header keys and values of the server response. |
| /// - Returns: the instance of the object, or `nil` if an error occurred during initialization. |
| public init?(url: URL, statusCode: Int, httpVersion: String?, headerFields: [String : String]?) { |
| self.statusCode = statusCode |
| self.allHeaderFields = headerFields ?? [:] |
| super.init(url: url, mimeType: nil, expectedContentLength: 0, textEncodingName: nil) |
| expectedContentLength = getExpectedContentLength(fromHeaderFields: headerFields) ?? -1 |
| suggestedFilename = getSuggestedFilename(fromHeaderFields: headerFields) ?? "Unknown" |
| if let type = ContentTypeComponents(headerFields: headerFields) { |
| mimeType = type.mimeType.lowercased() |
| textEncodingName = type.textEncoding?.lowercased() |
| } |
| } |
| |
| public required init?(coder aDecoder: NSCoder) { |
| guard aDecoder.allowsKeyedCoding else { |
| preconditionFailure("Unkeyed coding is unsupported.") |
| } |
| |
| self.statusCode = aDecoder.decodeInteger(forKey: "NS.statusCode") |
| |
| if let encodedHeaders = aDecoder.decodeObject(forKey: "NS.allHeaderFields") as? NSDictionary { |
| self.allHeaderFields = encodedHeaders._swiftObject |
| } else { |
| self.allHeaderFields = [:] |
| } |
| |
| super.init(coder: aDecoder) |
| } |
| |
| open override func encode(with aCoder: NSCoder) { |
| super.encode(with: aCoder) //Will fail if .allowsKeyedCoding == false |
| |
| aCoder.encode(self.statusCode, forKey: "NS.statusCode") |
| aCoder.encode(self.allHeaderFields._bridgeToObjectiveC(), forKey: "NS.allHeaderFields") |
| |
| } |
| |
| /// The HTTP status code of the receiver. |
| public let statusCode: Int |
| |
| /// Returns a dictionary containing all the HTTP header fields |
| /// of the receiver. |
| /// |
| /// By examining this header dictionary, clients can see |
| /// the "raw" header information which was reported to the protocol |
| /// implementation by the HTTP server. This may be of use to |
| /// sophisticated or special-purpose HTTP clients. |
| /// |
| /// - Returns: A dictionary containing all the HTTP header fields of the |
| /// receiver. |
| /// |
| /// - Important: This is an *experimental* change from the |
| /// `[NSObject: AnyObject]` type that Darwin Foundation uses. |
| public let allHeaderFields: [AnyHashable : Any] |
| |
| /// Convenience method which returns a localized string |
| /// corresponding to the status code for this response. |
| /// - Parameter forStatusCode: the status code to use to produce a localized string. |
| open class func localizedString(forStatusCode statusCode: Int) -> String { |
| switch statusCode { |
| case 100: return "Continue" |
| case 101: return "Switching Protocols" |
| case 102: return "Processing" |
| case 100...199: return "Informational" |
| case 200: return "OK" |
| case 201: return "Created" |
| case 202: return "Accepted" |
| case 203: return "Non-Authoritative Information" |
| case 204: return "No Content" |
| case 205: return "Reset Content" |
| case 206: return "Partial Content" |
| case 207: return "Multi-Status" |
| case 208: return "Already Reported" |
| case 226: return "IM Used" |
| case 200...299: return "Success" |
| case 300: return "Multiple Choices" |
| case 301: return "Moved Permanently" |
| case 302: return "Found" |
| case 303: return "See Other" |
| case 304: return "Not Modified" |
| case 305: return "Use Proxy" |
| case 307: return "Temporary Redirect" |
| case 308: return "Permanent Redirect" |
| case 300...399: return "Redirection" |
| case 400: return "Bad Request" |
| case 401: return "Unauthorized" |
| case 402: return "Payment Required" |
| case 403: return "Forbidden" |
| case 404: return "Not Found" |
| case 405: return "Method Not Allowed" |
| case 406: return "Not Acceptable" |
| case 407: return "Proxy Authentication Required" |
| case 408: return "Request Timeout" |
| case 409: return "Conflict" |
| case 410: return "Gone" |
| case 411: return "Length Required" |
| case 412: return "Precondition Failed" |
| case 413: return "Payload Too Large" |
| case 414: return "URI Too Long" |
| case 415: return "Unsupported Media Type" |
| case 416: return "Range Not Satisfiable" |
| case 417: return "Expectation Failed" |
| case 421: return "Misdirected Request" |
| case 422: return "Unprocessable Entity" |
| case 423: return "Locked" |
| case 424: return "Failed Dependency" |
| case 426: return "Upgrade Required" |
| case 428: return "Precondition Required" |
| case 429: return "Too Many Requests" |
| case 431: return "Request Header Fields Too Large" |
| case 451: return "Unavailable For Legal Reasons" |
| case 400...499: return "Client Error" |
| case 500: return "Internal Server Error" |
| case 501: return "Not Implemented" |
| case 502: return "Bad Gateway" |
| case 503: return "Service Unavailable" |
| case 504: return "Gateway Timeout" |
| case 505: return "HTTP Version Not Supported" |
| case 506: return "Variant Also Negotiates" |
| case 507: return "Insufficient Storage" |
| case 508: return "Loop Detected" |
| case 510: return "Not Extended" |
| case 511: return "Network Authentication Required" |
| case 500...599: return "Server Error" |
| default: return "Server Error" |
| } |
| } |
| |
| /// A string that represents the contents of the NSHTTPURLResponse Object. |
| /// This property is intended to produce readable output. |
| override open var description: String { |
| var result = "<\(type(of: self)) \(Unmanaged.passUnretained(self).toOpaque())> { URL: \(url!.absoluteString) }{ status: \(statusCode), headers {\n" |
| for(aKey, aValue) in allHeaderFields { |
| guard let key = aKey as? String, let value = aValue as? String else { continue } //shouldn't typically fail here |
| if((key.lowercased() == "content-disposition" && suggestedFilename != "Unknown") || key.lowercased() == "content-type") { |
| result += " \"\(key)\" = \"\(value)\";\n" |
| } else { |
| result += " \"\(key)\" = \(value);\n" |
| } |
| } |
| result += "} }" |
| return result |
| } |
| } |
| |
| /// Parses the expected content length from the headers. |
| /// |
| /// Note that the message content length is different from the message |
| /// transfer length. |
| /// The transfer length can only be derived when the Transfer-Encoding is identity (default). |
| /// For compressed content (Content-Encoding other than identity), there is not way to derive the |
| /// content length from the transfer length. |
| private func getExpectedContentLength(fromHeaderFields headerFields: [String : String]?) -> Int64? { |
| guard |
| let f = headerFields, |
| let contentLengthS = valueForCaseInsensitiveKey("content-length", fields: f), |
| let contentLength = Int64(contentLengthS) |
| else { return nil } |
| return contentLength |
| } |
| /// Parses the suggested filename from the `Content-Disposition` header. |
| /// |
| /// - SeeAlso: [RFC 2183](https://tools.ietf.org/html/rfc2183) |
| private func getSuggestedFilename(fromHeaderFields headerFields: [String : String]?) -> String? { |
| // Typical use looks like this: |
| // Content-Disposition: attachment; filename="fname.ext" |
| guard |
| let f = headerFields, |
| let contentDisposition = valueForCaseInsensitiveKey("content-disposition", fields: f), |
| let field = contentDisposition.httpHeaderParts |
| else { return nil } |
| for part in field.parameters where part.attribute == "filename" { |
| if let path = part.value { |
| return _pathComponents(path)?.map{ $0 == "/" ? "" : $0}.joined(separator: "_") |
| } else { |
| return nil |
| } |
| } |
| return nil |
| } |
| /// Parts corresponding to the `Content-Type` header field in a HTTP message. |
| private struct ContentTypeComponents { |
| /// For `text/html; charset=ISO-8859-4` this would be `text/html` |
| let mimeType: String |
| /// For `text/html; charset=ISO-8859-4` this would be `ISO-8859-4`. Will be |
| /// `nil` when no `charset` is specified. |
| let textEncoding: String? |
| } |
| extension ContentTypeComponents { |
| /// Parses the `Content-Type` header field |
| /// |
| /// `Content-Type: text/html; charset=ISO-8859-4` would result in `("text/html", "ISO-8859-4")`, while |
| /// `Content-Type: text/html` would result in `("text/html", nil)`. |
| init?(headerFields: [String : String]?) { |
| guard |
| let f = headerFields, |
| let contentType = valueForCaseInsensitiveKey("content-type", fields: f), |
| let field = contentType.httpHeaderParts |
| else { return nil } |
| for parameter in field.parameters where parameter.attribute == "charset" { |
| self.mimeType = field.value |
| self.textEncoding = parameter.value |
| return |
| } |
| self.mimeType = field.value |
| self.textEncoding = nil |
| } |
| } |
| |
| /// A type with paramteres |
| /// |
| /// RFC 2616 specifies a few types that can have parameters, e.g. `Content-Type`. |
| /// These are specified like so |
| /// ``` |
| /// field = value *( ";" parameter ) |
| /// value = token |
| /// ``` |
| /// where parameters are attribute/value as specified by |
| /// ``` |
| /// parameter = attribute "=" value |
| /// attribute = token |
| /// value = token | quoted-string |
| /// ``` |
| private struct ValueWithParameters { |
| let value: String |
| let parameters: [Parameter] |
| struct Parameter { |
| let attribute: String |
| let value: String? |
| } |
| } |
| |
| private extension String { |
| /// Split the string at each ";", remove any quoting. |
| /// |
| /// The trouble is if there's a |
| /// ";" inside something that's quoted. And we can escape the separator and |
| /// the quotes with a "\". |
| var httpHeaderParts: ValueWithParameters? { |
| var type: String? |
| var parameters: [ValueWithParameters.Parameter] = [] |
| let ws = CharacterSet.whitespaces |
| func append(_ string: String) { |
| if type == nil { |
| type = string |
| } else { |
| if let r = string.range(of: "=") { |
| let name = string[string.startIndex..<r.lowerBound].trimmingCharacters(in: ws) |
| let value = string[r.upperBound..<string.endIndex].trimmingCharacters(in: ws) |
| parameters.append(ValueWithParameters.Parameter(attribute: name, value: value)) |
| } else { |
| let name = string.trimmingCharacters(in: ws) |
| parameters.append(ValueWithParameters.Parameter(attribute: name, value: nil)) |
| } |
| } |
| } |
| |
| let escape = UnicodeScalar(0x5c)! // \ |
| let quote = UnicodeScalar(0x22)! // " |
| let separator = UnicodeScalar(0x3b)! // ; |
| enum State { |
| case nonQuoted(String) |
| case nonQuotedEscaped(String) |
| case quoted(String) |
| case quotedEscaped(String) |
| } |
| var state = State.nonQuoted("") |
| for next in unicodeScalars { |
| switch (state, next) { |
| case (.nonQuoted(let s), separator): |
| append(s) |
| state = .nonQuoted("") |
| case (.nonQuoted(let s), escape): |
| state = .nonQuotedEscaped(s + String(next)) |
| case (.nonQuoted(let s), quote): |
| state = .quoted(s) |
| case (.nonQuoted(let s), _): |
| state = .nonQuoted(s + String(next)) |
| |
| case (.nonQuotedEscaped(let s), _): |
| state = .nonQuoted(s + String(next)) |
| |
| case (.quoted(let s), quote): |
| state = .nonQuoted(s) |
| case (.quoted(let s), escape): |
| state = .quotedEscaped(s + String(next)) |
| case (.quoted(let s), _): |
| state = .quoted(s + String(next)) |
| |
| case (.quotedEscaped(let s), _): |
| state = .quoted(s + String(next)) |
| } |
| } |
| switch state { |
| case .nonQuoted(let s): append(s) |
| case .nonQuotedEscaped(let s): append(s) |
| case .quoted(let s): append(s) |
| case .quotedEscaped(let s): append(s) |
| } |
| guard let t = type else { return nil } |
| return ValueWithParameters(value: t, parameters: parameters) |
| } |
| } |
| private func valueForCaseInsensitiveKey(_ key: String, fields: [String: String]) -> String? { |
| let kk = key.lowercased() |
| for (k, v) in fields { |
| if k.lowercased() == kk { |
| return v |
| } |
| } |
| return nil |
| } |