@@ -69,6 +69,11 @@ public class HTTPCookie : NSObject {
69
69
let _version : Int
70
70
var _properties : [ String : Any ]
71
71
72
+ static let _attributes : [ String ] = [ NSHTTPCookieName, NSHTTPCookieValue, NSHTTPCookieOriginURL, NSHTTPCookieVersion,
73
+ NSHTTPCookieDomain, NSHTTPCookiePath, NSHTTPCookieSecure, NSHTTPCookieExpires,
74
+ NSHTTPCookieComment, NSHTTPCookieCommentURL, NSHTTPCookieDiscard, NSHTTPCookieMaximumAge,
75
+ NSHTTPCookiePort]
76
+
72
77
/// Initialize a NSHTTPCookie object with a dictionary of parameters
73
78
///
74
79
/// - Parameter properties: The dictionary of properties to be used to
@@ -327,16 +332,106 @@ public class HTTPCookie : NSObject {
327
332
}
328
333
return [ " Cookie " : cookieString]
329
334
}
330
-
335
+
331
336
/// Return an array of cookies parsed from the specified response header fields and URL.
332
337
///
333
338
/// This method will ignore irrelevant header fields so
334
339
/// you can pass a dictionary containing data other than cookie data.
335
340
/// - Parameter headerFields: The response header fields to check for cookies.
336
341
/// - Parameter URL: The URL that the cookies came from - relevant to how the cookies are interpeted.
337
342
/// - Returns: An array of NSHTTPCookie objects
338
- public class func cookies( withResponseHeaderFields headerFields: [ String : String] , forURL url: URL) - > [ HTTPCookie] { NSUnimplemented ( ) }
343
+ public class func cookies( withResponseHeaderFields headerFields: [ String : String] , forURL url: URL) - > [ HTTPCookie] {
344
+
345
+ //HTTP Cookie parsing based on RFC 6265: https://tools.ietf.org/html/rfc6265
346
+ //Though RFC6265 suggests that multiple cookies cannot be folded into a single Set-Cookie field, this is
347
+ //pretty common. It also suggests that commas and semicolons among other characters, cannot be a part of
348
+ // names and values. This implementation takes care of multiple cookies in the same field, however it doesn't
349
+ //support commas and semicolons in names and values(except for dates)
350
+
351
+ guard let cookies: String = headerFields [ " Set-Cookie " ] else { return [ ] }
352
+
353
+ let nameValuePairs = cookies. components ( separatedBy: " ; " ) //split the name/value and attribute/value pairs
354
+ . map ( { $0. trim ( ) } ) //trim whitespaces
355
+ . map ( { removeCommaFromDate ( $0) } ) //get rid of commas in dates
356
+ . flatMap ( { $0. components ( separatedBy: " , " ) } ) //cookie boundaries are marked by commas
357
+ . map ( { $0. trim ( ) } ) //trim again
358
+ . filter ( { $0. caseInsensitiveCompare ( " HTTPOnly " ) != . orderedSame} ) //we don't use HTTPOnly, do we?
359
+ . flatMap ( { createNameValuePair ( pair: $0) } ) //create Name and Value properties
360
+
361
+ //mark cookie boundaries in the name-value array
362
+ var cookieIndices = ( 0 ..< nameValuePairs. count) . filter ( { nameValuePairs [ $0] . hasPrefix ( " Name " ) } )
363
+ cookieIndices. append ( nameValuePairs. count)
364
+
365
+ //bake the cookies
366
+ var httpCookies : [ HTTPCookie ] = [ ]
367
+ for i in 0 ..< cookieIndices. count- 1 {
368
+ if let aCookie = createHttpCookie ( url: url, pairs: nameValuePairs, start: cookieIndices [ i] , end: cookieIndices [ i+ 1 ] ) {
369
+ httpCookies. append ( aCookie)
370
+ }
371
+ }
339
372
373
+ return httpCookies
374
+ }
375
+
376
+ //Bake a cookie
377
+ private class func createHttpCookie( url: URL, pairs: [ String] , start: Int, end: Int) - > HTTPCookie? {
378
+ var properties : [ String : Any ] = [ : ]
379
+ for index in start..< end {
380
+ let name = pairs [ index] . components ( separatedBy: " = " ) [ 0 ]
381
+ var value = pairs [ index] . components ( separatedBy: " \( name) = " ) [ 1 ] //a value can have an "="
382
+ if canonicalize ( name) == " Expires " {
383
+ value = value. insertComma ( at: 3 ) //re-insert the comma
384
+ }
385
+ properties [ canonicalize ( name) ] = value
386
+ }
387
+
388
+ //if domain wasn't provided use the URL
389
+ if properties [ NSHTTPCookieDomain] == nil {
390
+ properties [ NSHTTPCookieDomain] = url. absoluteString
391
+ }
392
+
393
+ //the default Path is "/"
394
+ if properties [ NSHTTPCookiePath] == nil {
395
+ properties [ NSHTTPCookiePath] = " / "
396
+ }
397
+
398
+ return HTTPCookie ( properties: properties)
399
+ }
400
+
401
+ //we pass this to a map()
402
+ private class func removeCommaFromDate( _ value: String) - > String {
403
+ if value. hasPrefix ( " Expires " ) || value. hasPrefix ( " expires " ) {
404
+ return value. removeCommas ( )
405
+ }
406
+ return value
407
+ }
408
+
409
+ //These cookie attributes are defined in RFC 6265 and 2965(which is obsolete)
410
+ //HTTPCookie supports these
411
+ private class func isCookieAttribute( _ string: String) - > Bool {
412
+ return _attributes. first ( where: { $0. caseInsensitiveCompare ( string) == . orderedSame} ) != nil
413
+ }
414
+
415
+ //Cookie attribute names are case-insensitive as per RFC6265: https://tools.ietf.org/html/rfc6265
416
+ //but HTTPCookie needs only the first letter of each attribute in uppercase
417
+ private class func canonicalize( _ name: String) - > String {
418
+ let idx = _attributes. index ( where: { $0. caseInsensitiveCompare ( name) == . orderedSame} ) !
419
+ return _attributes [ idx]
420
+ }
421
+
422
+ //A name=value pair should be translated to two properties, Name=name and Value=value
423
+ private class func createNameValuePair( pair: String) - > [ String] {
424
+ if pair. caseInsensitiveCompare ( NSHTTPCookieSecure) == . orderedSame {
425
+ return [ " Secure=TRUE " ]
426
+ }
427
+ let name = pair. components ( separatedBy: " = " ) [ 0 ]
428
+ let value = pair. components ( separatedBy: " \( name) = " ) [ 1 ]
429
+ if !isCookieAttribute( name) {
430
+ return [ " Name= \( name) " , " Value= \( value) " ]
431
+ }
432
+ return [ pair]
433
+ }
434
+
340
435
/// Returns a dictionary representation of the receiver.
341
436
///
342
437
/// This method returns a dictionary representation of the
@@ -459,3 +554,19 @@ public class HTTPCookie : NSObject {
459
554
return _portList
460
555
}
461
556
}
557
+
558
+ //utils for cookie parsing
559
+ internal extension String {
560
+ func trim( ) -> String {
561
+ return self . trimmingCharacters ( in: NSCharacterSet . whitespacesAndNewlines ( ) )
562
+ }
563
+
564
+ func removeCommas( ) -> String {
565
+ return self . replacingOccurrences ( of: " , " , with: " " )
566
+ }
567
+
568
+ func insertComma( at index: Int ) -> String {
569
+ return String ( self . characters. prefix ( index) ) + " , " + String( self . characters. suffix ( self . characters. count- index) )
570
+ }
571
+ }
572
+
0 commit comments