URLResponse.swift
1 // This source file is part of the Swift.org open source project 2 // 3 // Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors 4 // Licensed under Apache License v2.0 with Runtime Library Exception 5 // 6 // See http://swift.org/LICENSE.txt for license information 7 // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors 8 // 9 10 #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) 11 import SwiftFoundation 12 #else 13 import Foundation 14 #endif 15 16 /// An `URLResponse` object represents a URL load response in a 17 /// manner independent of protocol and URL scheme. 18 /// 19 /// `URLResponse` encapsulates the metadata associated 20 /// with a URL load. Note that URLResponse objects do not contain 21 /// the actual bytes representing the content of a URL. See 22 /// `URLSession` for more information about receiving the content 23 /// data for a URL load. 24 open class URLResponse : NSObject, NSSecureCoding, NSCopying { 25 26 static public var supportsSecureCoding: Bool { 27 return true 28 } 29 30 public required init?(coder aDecoder: NSCoder) { 31 guard aDecoder.allowsKeyedCoding else { 32 preconditionFailure("Unkeyed coding is unsupported.") 33 } 34 35 guard let nsurl = aDecoder.decodeObject(of: NSURL.self, forKey: "NS.url") else { return nil } 36 self.url = nsurl as URL 37 38 39 if let mimetype = aDecoder.decodeObject(of: NSString.self, forKey: "NS.mimeType") { 40 self.mimeType = mimetype as String 41 } 42 43 self.expectedContentLength = aDecoder.decodeInt64(forKey: "NS.expectedContentLength") 44 45 if let encodedEncodingName = aDecoder.decodeObject(of: NSString.self, forKey: "NS.textEncodingName") { 46 self.textEncodingName = encodedEncodingName as String 47 } 48 49 if let encodedFilename = aDecoder.decodeObject(of: NSString.self, forKey: "NS.suggestedFilename") { 50 self.suggestedFilename = encodedFilename as String 51 } 52 } 53 54 open func encode(with aCoder: NSCoder) { 55 guard aCoder.allowsKeyedCoding else { 56 preconditionFailure("Unkeyed coding is unsupported.") 57 } 58 59 aCoder.encode(self.url?._bridgeToObjectiveC(), forKey: "NS.url") 60 aCoder.encode(self.mimeType?._bridgeToObjectiveC(), forKey: "NS.mimeType") 61 aCoder.encode(self.expectedContentLength, forKey: "NS.expectedContentLength") 62 aCoder.encode(self.textEncodingName?._bridgeToObjectiveC(), forKey: "NS.textEncodingName") 63 aCoder.encode(self.suggestedFilename?._bridgeToObjectiveC(), forKey: "NS.suggestedFilename") 64 } 65 66 open override func copy() -> Any { 67 return copy(with: nil) 68 } 69 70 open func copy(with zone: NSZone? = nil) -> Any { 71 return self 72 } 73 74 /// Initialize an URLResponse with the provided values. 75 /// 76 /// This is the designated initializer for URLResponse. 77 /// - Parameter URL: the URL 78 /// - Parameter mimeType: the MIME content type of the response 79 /// - Parameter expectedContentLength: the expected content length of the associated data 80 /// - Parameter textEncodingName the name of the text encoding for the associated data, if applicable, else nil 81 /// - Returns: The initialized URLResponse. 82 public init(url: URL, mimeType: String?, expectedContentLength length: Int, textEncodingName name: String?) { 83 self.url = url 84 self.mimeType = mimeType 85 self.expectedContentLength = Int64(length) 86 self.textEncodingName = name 87 let c = url.lastPathComponent 88 self.suggestedFilename = c.isEmpty ? "Unknown" : c 89 } 90 91 /// The URL of the receiver. 92 /*@NSCopying*/ open private(set) var url: URL? 93 94 95 /// The MIME type of the receiver. 96 /// 97 /// The MIME type is based on the information provided 98 /// from an origin source. However, that value may be changed or 99 /// corrected by a protocol implementation if it can be determined 100 /// that the origin server or source reported the information 101 /// incorrectly or imprecisely. An attempt to guess the MIME type may 102 /// be made if the origin source did not report any such information. 103 open fileprivate(set) var mimeType: String? 104 105 /// The expected content length of the receiver. 106 /// 107 /// Some protocol implementations report a content length 108 /// as part of delivering load metadata, but not all protocols 109 /// guarantee the amount of data that will be delivered in actuality. 110 /// Hence, this method returns an expected amount. Clients should use 111 /// this value as an advisory, and should be prepared to deal with 112 /// either more or less data. 113 /// 114 /// The expected content length of the receiver, or `-1` if 115 /// there is no expectation that can be arrived at regarding expected 116 /// content length. 117 open fileprivate(set) var expectedContentLength: Int64 118 119 /// The name of the text encoding of the receiver. 120 /// 121 /// This name will be the actual string reported by the 122 /// origin source during the course of performing a protocol-specific 123 /// URL load. Clients can inspect this string and convert it to an 124 /// NSStringEncoding or CFStringEncoding using the methods and 125 /// functions made available in the appropriate framework. 126 open fileprivate(set) var textEncodingName: String? 127 128 /// A suggested filename if the resource were saved to disk. 129 /// 130 /// The method first checks if the server has specified a filename 131 /// using the content disposition header. If no valid filename is 132 /// specified using that mechanism, this method checks the last path 133 /// component of the URL. If no valid filename can be obtained using 134 /// the last path component, this method uses the URL's host as the 135 /// filename. If the URL's host can't be converted to a valid 136 /// filename, the filename "unknown" is used. In mose cases, this 137 /// method appends the proper file extension based on the MIME type. 138 /// 139 /// This method always returns a valid filename. 140 open fileprivate(set) var suggestedFilename: String? 141 142 open override func isEqual(_ value: Any?) -> Bool { 143 switch value { 144 case let other as URLResponse: 145 return self.isEqual(to: other) 146 default: 147 return false 148 } 149 } 150 151 private func isEqual(to other: URLResponse) -> Bool { 152 if self === other { 153 return true 154 } 155 156 return self.url == other.url && 157 self.expectedContentLength == other.expectedContentLength && 158 self.mimeType == other.mimeType && 159 self.textEncodingName == other.textEncodingName 160 } 161 162 open override var hash: Int { 163 var hasher = Hasher() 164 hasher.combine(url) 165 hasher.combine(expectedContentLength) 166 hasher.combine(mimeType) 167 hasher.combine(textEncodingName) 168 return hasher.finalize() 169 } 170 } 171 172 /// A Response to an HTTP URL load. 173 /// 174 /// An HTTPURLResponse object represents a response to an 175 /// HTTP URL load. It is a specialization of URLResponse which 176 /// provides conveniences for accessing information specific to HTTP 177 /// protocol responses. 178 open class HTTPURLResponse : URLResponse { 179 180 /// Initializer for HTTPURLResponse objects. 181 /// 182 /// - Parameter url: the URL from which the response was generated. 183 /// - Parameter statusCode: an HTTP status code. 184 /// - Parameter httpVersion: The version of the HTTP response as represented by the server. This is typically represented as "HTTP/1.1". 185 /// - Parameter headerFields: A dictionary representing the header keys and values of the server response. 186 /// - Returns: the instance of the object, or `nil` if an error occurred during initialization. 187 public init?(url: URL, statusCode: Int, httpVersion: String?, headerFields: [String : String]?) { 188 self.statusCode = statusCode 189 190 self._allHeaderFields = { 191 // Canonicalize the header fields by capitalizing the field names, but not X- Headers 192 // This matches the behaviour of Darwin. 193 guard let headerFields = headerFields else { return [:] } 194 var canonicalizedFields: [String: String] = [:] 195 196 for (key, value) in headerFields { 197 if key.isEmpty { continue } 198 if key.hasPrefix("x-") || key.hasPrefix("X-") { 199 canonicalizedFields[key] = value 200 } else if key.caseInsensitiveCompare("WWW-Authenticate") == .orderedSame { 201 canonicalizedFields["WWW-Authenticate"] = value 202 } else { 203 canonicalizedFields[key.capitalized] = value 204 } 205 } 206 return canonicalizedFields 207 }() 208 209 super.init(url: url, mimeType: nil, expectedContentLength: 0, textEncodingName: nil) 210 expectedContentLength = getExpectedContentLength(fromHeaderFields: headerFields) ?? -1 211 suggestedFilename = getSuggestedFilename(fromHeaderFields: headerFields) ?? "Unknown" 212 if let type = ContentTypeComponents(headerFields: headerFields) { 213 mimeType = type.mimeType.lowercased() 214 textEncodingName = type.textEncoding?.lowercased() 215 } 216 } 217 218 public required init?(coder aDecoder: NSCoder) { 219 guard aDecoder.allowsKeyedCoding else { 220 preconditionFailure("Unkeyed coding is unsupported.") 221 } 222 223 self.statusCode = aDecoder.decodeInteger(forKey: "NS.statusCode") 224 225 if aDecoder.containsValue(forKey: "NS.allHeaderFields") { 226 self._allHeaderFields = aDecoder.decodeObject(of: NSDictionary.self, forKey: "NS.allHeaderFields") as! [String: String] 227 } else { 228 self._allHeaderFields = [:] 229 } 230 231 super.init(coder: aDecoder) 232 } 233 234 open override func encode(with aCoder: NSCoder) { 235 super.encode(with: aCoder) //Will fail if .allowsKeyedCoding == false 236 237 aCoder.encode(self.statusCode, forKey: "NS.statusCode") 238 aCoder.encode(self.allHeaderFields as NSDictionary, forKey: "NS.allHeaderFields") 239 240 } 241 242 /// The HTTP status code of the receiver. 243 public let statusCode: Int 244 245 /// Returns a dictionary containing all the HTTP header fields 246 /// of the receiver. 247 /// 248 /// By examining this header dictionary, clients can see 249 /// the "raw" header information which was reported to the protocol 250 /// implementation by the HTTP server. This may be of use to 251 /// sophisticated or special-purpose HTTP clients. 252 /// 253 /// - Returns: A dictionary containing all the HTTP header fields of the 254 /// receiver. 255 /// 256 /// - Important: This is an *experimental* change from the 257 /// `[NSObject: AnyObject]` type that Darwin Foundation uses. 258 private let _allHeaderFields: [String: String] 259 public var allHeaderFields: [AnyHashable : Any] { 260 _allHeaderFields as [AnyHashable : Any] 261 } 262 263 public func value(forHTTPHeaderField field: String) -> String? { 264 return valueForCaseInsensitiveKey(field, fields: _allHeaderFields) 265 } 266 267 /// Convenience method which returns a localized string 268 /// corresponding to the status code for this response. 269 /// - Parameter forStatusCode: the status code to use to produce a localized string. 270 open class func localizedString(forStatusCode statusCode: Int) -> String { 271 switch statusCode { 272 case 100: return "Continue" 273 case 101: return "Switching Protocols" 274 case 102: return "Processing" 275 case 100...199: return "Informational" 276 case 200: return "OK" 277 case 201: return "Created" 278 case 202: return "Accepted" 279 case 203: return "Non-Authoritative Information" 280 case 204: return "No Content" 281 case 205: return "Reset Content" 282 case 206: return "Partial Content" 283 case 207: return "Multi-Status" 284 case 208: return "Already Reported" 285 case 226: return "IM Used" 286 case 200...299: return "Success" 287 case 300: return "Multiple Choices" 288 case 301: return "Moved Permanently" 289 case 302: return "Found" 290 case 303: return "See Other" 291 case 304: return "Not Modified" 292 case 305: return "Use Proxy" 293 case 307: return "Temporary Redirect" 294 case 308: return "Permanent Redirect" 295 case 300...399: return "Redirection" 296 case 400: return "Bad Request" 297 case 401: return "Unauthorized" 298 case 402: return "Payment Required" 299 case 403: return "Forbidden" 300 case 404: return "Not Found" 301 case 405: return "Method Not Allowed" 302 case 406: return "Not Acceptable" 303 case 407: return "Proxy Authentication Required" 304 case 408: return "Request Timeout" 305 case 409: return "Conflict" 306 case 410: return "Gone" 307 case 411: return "Length Required" 308 case 412: return "Precondition Failed" 309 case 413: return "Payload Too Large" 310 case 414: return "URI Too Long" 311 case 415: return "Unsupported Media Type" 312 case 416: return "Range Not Satisfiable" 313 case 417: return "Expectation Failed" 314 case 421: return "Misdirected Request" 315 case 422: return "Unprocessable Entity" 316 case 423: return "Locked" 317 case 424: return "Failed Dependency" 318 case 426: return "Upgrade Required" 319 case 428: return "Precondition Required" 320 case 429: return "Too Many Requests" 321 case 431: return "Request Header Fields Too Large" 322 case 451: return "Unavailable For Legal Reasons" 323 case 400...499: return "Client Error" 324 case 500: return "Internal Server Error" 325 case 501: return "Not Implemented" 326 case 502: return "Bad Gateway" 327 case 503: return "Service Unavailable" 328 case 504: return "Gateway Timeout" 329 case 505: return "HTTP Version Not Supported" 330 case 506: return "Variant Also Negotiates" 331 case 507: return "Insufficient Storage" 332 case 508: return "Loop Detected" 333 case 510: return "Not Extended" 334 case 511: return "Network Authentication Required" 335 case 500...599: return "Server Error" 336 default: return "Server Error" 337 } 338 } 339 340 /// A string that represents the contents of the HTTPURLResponse Object. 341 /// This property is intended to produce readable output. 342 override open var description: String { 343 var result = "<\(type(of: self)) \(Unmanaged.passUnretained(self).toOpaque())> { URL: \(url!.absoluteString) }{ status: \(statusCode), headers {\n" 344 for key in _allHeaderFields.keys.sorted() { 345 guard let value = _allHeaderFields[key] else { continue } 346 if((key.lowercased() == "content-disposition" && suggestedFilename != "Unknown") || key.lowercased() == "content-type") { 347 result += " \"\(key)\" = \"\(value)\";\n" 348 } else { 349 result += " \"\(key)\" = \(value);\n" 350 } 351 } 352 result += "} }" 353 return result 354 } 355 } 356 357 /// Parses the expected content length from the headers. 358 /// 359 /// Note that the message content length is different from the message 360 /// transfer length. 361 /// The transfer length can only be derived when the Transfer-Encoding is identity (default). 362 /// For compressed content (Content-Encoding other than identity), there is not way to derive the 363 /// content length from the transfer length. 364 private func getExpectedContentLength(fromHeaderFields headerFields: [String : String]?) -> Int64? { 365 guard 366 let f = headerFields, 367 let contentLengthS = valueForCaseInsensitiveKey("content-length", fields: f), 368 let contentLength = Int64(contentLengthS) 369 else { return nil } 370 return contentLength 371 } 372 /// Parses the suggested filename from the `Content-Disposition` header. 373 /// 374 /// - SeeAlso: [RFC 2183](https://tools.ietf.org/html/rfc2183) 375 private func getSuggestedFilename(fromHeaderFields headerFields: [String : String]?) -> String? { 376 // Typical use looks like this: 377 // Content-Disposition: attachment; filename="fname.ext" 378 guard 379 let f = headerFields, 380 let contentDisposition = valueForCaseInsensitiveKey("content-disposition", fields: f), 381 let field = contentDisposition.httpHeaderParts 382 else { return nil } 383 for part in field.parameters where part.attribute == "filename" { 384 if let path = part.value { 385 return (path as NSString).pathComponents.map{ $0 == "/" ? "" : $0}.joined(separator: "_") 386 } else { 387 return nil 388 } 389 } 390 return nil 391 } 392 /// Parts corresponding to the `Content-Type` header field in a HTTP message. 393 private struct ContentTypeComponents { 394 /// For `text/html; charset=ISO-8859-4` this would be `text/html` 395 let mimeType: String 396 /// For `text/html; charset=ISO-8859-4` this would be `ISO-8859-4`. Will be 397 /// `nil` when no `charset` is specified. 398 let textEncoding: String? 399 } 400 extension ContentTypeComponents { 401 /// Parses the `Content-Type` header field 402 /// 403 /// `Content-Type: text/html; charset=ISO-8859-4` would result in `("text/html", "ISO-8859-4")`, while 404 /// `Content-Type: text/html` would result in `("text/html", nil)`. 405 init?(headerFields: [String : String]?) { 406 guard 407 let f = headerFields, 408 let contentType = valueForCaseInsensitiveKey("content-type", fields: f), 409 let field = contentType.httpHeaderParts 410 else { return nil } 411 for parameter in field.parameters where parameter.attribute == "charset" { 412 self.mimeType = field.value 413 self.textEncoding = parameter.value 414 return 415 } 416 self.mimeType = field.value 417 self.textEncoding = nil 418 } 419 } 420 421 /// A type with parameters 422 /// 423 /// RFC 2616 specifies a few types that can have parameters, e.g. `Content-Type`. 424 /// These are specified like so 425 /// ``` 426 /// field = value *( ";" parameter ) 427 /// value = token 428 /// ``` 429 /// where parameters are attribute/value as specified by 430 /// ``` 431 /// parameter = attribute "=" value 432 /// attribute = token 433 /// value = token | quoted-string 434 /// ``` 435 private struct ValueWithParameters { 436 let value: String 437 let parameters: [Parameter] 438 struct Parameter { 439 let attribute: String 440 let value: String? 441 } 442 } 443 444 private extension String { 445 /// Split the string at each ";", remove any quoting. 446 /// 447 /// The trouble is if there's a 448 /// ";" inside something that's quoted. And we can escape the separator and 449 /// the quotes with a "\". 450 var httpHeaderParts: ValueWithParameters? { 451 var type: String? 452 var parameters: [ValueWithParameters.Parameter] = [] 453 let ws = CharacterSet.whitespaces 454 func append(_ string: String) { 455 if type == nil { 456 type = string 457 } else { 458 if let r = string.range(of: "=") { 459 let name = String(string[string.startIndex..<r.lowerBound]).trimmingCharacters(in: ws) 460 let value = String(string[r.upperBound..<string.endIndex]).trimmingCharacters(in: ws) 461 parameters.append(ValueWithParameters.Parameter(attribute: name, value: value)) 462 } else { 463 let name = string.trimmingCharacters(in: ws) 464 parameters.append(ValueWithParameters.Parameter(attribute: name, value: nil)) 465 } 466 } 467 } 468 469 let escape = UnicodeScalar(0x5c)! // \ 470 let quote = UnicodeScalar(0x22)! // " 471 let separator = UnicodeScalar(0x3b)! // ; 472 enum State { 473 case nonQuoted(String) 474 case nonQuotedEscaped(String) 475 case quoted(String) 476 case quotedEscaped(String) 477 } 478 var state = State.nonQuoted("") 479 for next in unicodeScalars { 480 switch (state, next) { 481 case (.nonQuoted(let s), separator): 482 append(s) 483 state = .nonQuoted("") 484 case (.nonQuoted(let s), escape): 485 state = .nonQuotedEscaped(s + String(next)) 486 case (.nonQuoted(let s), quote): 487 state = .quoted(s) 488 case (.nonQuoted(let s), _): 489 state = .nonQuoted(s + String(next)) 490 491 case (.nonQuotedEscaped(let s), _): 492 state = .nonQuoted(s + String(next)) 493 494 case (.quoted(let s), quote): 495 state = .nonQuoted(s) 496 case (.quoted(let s), escape): 497 state = .quotedEscaped(s + String(next)) 498 case (.quoted(let s), _): 499 state = .quoted(s + String(next)) 500 501 case (.quotedEscaped(let s), _): 502 state = .quoted(s + String(next)) 503 } 504 } 505 switch state { 506 case .nonQuoted(let s): append(s) 507 case .nonQuotedEscaped(let s): append(s) 508 case .quoted(let s): append(s) 509 case .quotedEscaped(let s): append(s) 510 } 511 guard let t = type else { return nil } 512 return ValueWithParameters(value: t, parameters: parameters) 513 } 514 } 515 private func valueForCaseInsensitiveKey(_ key: String, fields: [String: String]) -> String? { 516 let kk = key.lowercased() 517 for (k, v) in fields { 518 if k.lowercased() == kk { 519 return v 520 } 521 } 522 return nil 523 }