/ Sources / FoundationNetworking / URLResponse.swift
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  }