@@ -412,30 +412,74 @@ open class HTTPCookie : NSObject {
412
412
/// - Returns: An array of HTTPCookie objects
413
413
open class func cookies( withResponseHeaderFields headerFields: [ String : String ] , for URL: URL ) -> [ HTTPCookie ] {
414
414
415
- //HTTP Cookie parsing based on RFC 6265: https://tools.ietf.org/html/rfc6265
416
- //Though RFC6265 suggests that multiple cookies cannot be folded into a single Set-Cookie field, this is
417
- //pretty common. It also suggests that commas and semicolons among other characters, cannot be a part of
415
+ // HTTP Cookie parsing based on RFC 6265: https://tools.ietf.org/html/rfc6265
416
+ // Though RFC6265 suggests that multiple cookies cannot be folded into a single Set-Cookie field, this is
417
+ // pretty common. It also suggests that commas and semicolons among other characters, cannot be a part of
418
418
// names and values. This implementation takes care of multiple cookies in the same field, however it doesn't
419
- //support commas and semicolons in names and values(except for dates)
419
+ // support commas and semicolons in names and values(except for dates)
420
420
421
421
guard let cookies: String = headerFields [ " Set-Cookie " ] else { return [ ] }
422
422
423
- let nameValuePairs = cookies. components ( separatedBy: " ; " ) //split the name/value and attribute/value pairs
424
- . map ( { $0. trim ( ) } ) //trim whitespaces
425
- . map ( { removeCommaFromDate ( $0) } ) //get rid of commas in dates
426
- . flatMap ( { $0. components ( separatedBy: " , " ) } ) //cookie boundaries are marked by commas
427
- . map ( { $0. trim ( ) } ) //trim again
428
- . filter ( { $0. caseInsensitiveCompare ( " HTTPOnly " ) != . orderedSame} ) //we don't use HTTPOnly, do we?
429
- . flatMap ( { createNameValuePair ( pair: $0) } ) //create Name and Value properties
423
+ var httpCookies : [ HTTPCookie ] = [ ]
430
424
431
- //mark cookie boundaries in the name-value array
432
- var cookieIndices = ( 0 ..< nameValuePairs. count) . filter ( { nameValuePairs [ $0] . hasPrefix ( " Name " ) } )
433
- cookieIndices. append ( nameValuePairs. count)
425
+ // Let's do old school parsing, which should allow us to handle the
426
+ // embedded commas correctly.
427
+ var idx : String . Index = cookies. startIndex
428
+ let end : String . Index = cookies. endIndex
429
+ while idx < end {
430
+ // Skip leading spaces.
431
+ while idx < end && cookies [ idx] . isSpace {
432
+ idx = cookies. index ( after: idx)
433
+ }
434
+ let cookieStartIdx : String . Index = idx
435
+ var cookieEndIdx : String . Index = idx
436
+
437
+ while idx < end {
438
+ // Scan to the next comma, but check that the comma is not a
439
+ // legal comma in a value, by looking ahead for the token,
440
+ // which indicates the comma was separating cookies.
441
+ let cookiesRest = cookies [ idx..< end]
442
+ if let commaIdx = cookiesRest. firstIndex ( of: " , " ) {
443
+ // We are looking for WSP* TOKEN_CHAR+ WSP* '='
444
+ var lookaheadIdx = cookies. index ( after: commaIdx)
445
+ // Skip whitespace
446
+ while lookaheadIdx < end && cookies [ lookaheadIdx] . isSpace {
447
+ lookaheadIdx = cookies. index ( after: lookaheadIdx)
448
+ }
449
+ // Skip over the token characters
450
+ var tokenLength = 0
451
+ while lookaheadIdx < end && cookies [ lookaheadIdx] . isTokenCharacter {
452
+ lookaheadIdx = cookies. index ( after: lookaheadIdx)
453
+ tokenLength += 1
454
+ }
455
+ // Skip whitespace
456
+ while lookaheadIdx < end && cookies [ lookaheadIdx] . isSpace {
457
+ lookaheadIdx = cookies. index ( after: lookaheadIdx)
458
+ }
459
+ // Check there was a token, and there's an equals.
460
+ if lookaheadIdx < end && cookies [ lookaheadIdx] == " = " && tokenLength > 0 {
461
+ // We found a token after the comma, this is a cookie
462
+ // separator, and not an embedded comma.
463
+ idx = cookies. index ( after: commaIdx)
464
+ cookieEndIdx = commaIdx
465
+ break
466
+ }
467
+ // Otherwise, keep scanning from the comma.
468
+ idx = cookies. index ( after: commaIdx)
469
+ cookieEndIdx = idx
470
+ } else {
471
+ // No more commas, skip to the end.
472
+ idx = end
473
+ cookieEndIdx = end
474
+ break
475
+ }
476
+ }
434
477
435
- //bake the cookies
436
- var httpCookies : [ HTTPCookie ] = [ ]
437
- for i in 0 ..< cookieIndices. count- 1 {
438
- if let aCookie = createHttpCookie ( url: URL, pairs: nameValuePairs [ cookieIndices [ i] ..< cookieIndices [ i+ 1 ] ] ) {
478
+ if cookieEndIdx <= cookieStartIdx {
479
+ continue
480
+ }
481
+
482
+ if let aCookie = createHttpCookie ( url: URL, cookie: cookies [ cookieStartIdx..< cookieEndIdx] ) {
439
483
httpCookies. append ( aCookie)
440
484
}
441
485
}
@@ -444,14 +488,16 @@ open class HTTPCookie : NSObject {
444
488
}
445
489
446
490
//Bake a cookie
447
- private class func createHttpCookie( url: URL , pairs: ArraySlice < String > ) -> HTTPCookie ? {
448
- var properties : [ HTTPCookiePropertyKey : Any ] = [ : ]
491
+ private class func createHttpCookie( url: URL , cookie: Substring ) -> HTTPCookie ? {
492
+ let pairs = cookie. components ( separatedBy: " ; " ) // split the name/value and attribute/value pairs
493
+ . map { $0. trim ( ) } // trim whitespaces
494
+ . filter { $0. caseInsensitiveCompare ( " HTTPOnly " ) != . orderedSame } // we don't use HTTPOnly, do we?
495
+ . flatMap ( HTTPCookie . createNameValuePair) // create Name and Value properties
496
+
497
+ var properties : [ HTTPCookiePropertyKey : String ] = [ : ]
449
498
for pair in pairs {
450
499
let name = pair. components ( separatedBy: " = " ) [ 0 ]
451
- var value = pair. components ( separatedBy: " \( name) = " ) [ 1 ] //a value can have an "="
452
- if canonicalize ( name) == . expires {
453
- value = value. unmaskCommas ( ) //re-insert the comma
454
- }
500
+ let value = pair. components ( separatedBy: " \( name) = " ) [ 1 ] //a value can have an "="
455
501
properties [ canonicalize ( name) ] = value
456
502
}
457
503
@@ -468,14 +514,6 @@ open class HTTPCookie : NSObject {
468
514
return HTTPCookie ( properties: properties)
469
515
}
470
516
471
- //we pass this to a map()
472
- private class func removeCommaFromDate( _ value: String ) -> String {
473
- if value. hasPrefix ( " Expires " ) || value. hasPrefix ( " expires " ) {
474
- return value. maskCommas ( )
475
- }
476
- return value
477
- }
478
-
479
517
//These cookie attributes are defined in RFC 6265 and 2965(which is obsolete)
480
518
//HTTPCookie supports these
481
519
private class func isCookieAttribute( _ string: String ) -> Bool {
@@ -497,7 +535,7 @@ open class HTTPCookie : NSObject {
497
535
let name = pair. components ( separatedBy: " = " ) [ 0 ]
498
536
let value = pair. components ( separatedBy: " \( name) = " ) [ 1 ]
499
537
if !isCookieAttribute( name) {
500
- return [ " Name= \( name) " , " Value= \( value) " ]
538
+ return [ " Name= \( name. trim ( ) ) " , " Value= \( value) " ]
501
539
}
502
540
return [ pair]
503
541
}
@@ -654,12 +692,24 @@ fileprivate extension String {
654
692
func trim( ) -> String {
655
693
return self . trimmingCharacters ( in: . whitespacesAndNewlines)
656
694
}
695
+ }
657
696
658
- func maskCommas( ) -> String {
659
- return self . replacingOccurrences ( of: " , " , with: " &comma " )
697
+ fileprivate extension Character {
698
+ var isSpace : Bool {
699
+ return self == " " || self == " \t " || self == " \n " || self == " \r "
660
700
}
661
701
662
- func unmaskCommas( ) -> String {
663
- return self . replacingOccurrences ( of: " &comma " , with: " , " )
702
+ var isTokenCharacter : Bool {
703
+ guard let asciiValue = self . asciiValue else {
704
+ return false
705
+ }
706
+
707
+ // CTL, 0-31 and DEL (127)
708
+ if asciiValue <= 31 || asciiValue >= 127 {
709
+ return false
710
+ }
711
+
712
+ let nonTokenCharacters = " ()<>@,;: \\ \" /[]?={} \t "
713
+ return !nonTokenCharacters. contains ( self )
664
714
}
665
715
}
0 commit comments