@@ -69,6 +69,26 @@ extension HTTPCookiePropertyKey {
69
69
internal static let created = HTTPCookiePropertyKey ( rawValue: " Created " )
70
70
}
71
71
72
+ internal extension HTTPCookiePropertyKey {
73
+ static private let _setCookieAttributes : [ String : HTTPCookiePropertyKey ] = {
74
+ // Only some attributes are valid in the Set-Cookie header.
75
+ let validProperties : [ HTTPCookiePropertyKey ] = [
76
+ . expires, . maximumAge, . domain, . path, . secure, . comment, . discard,
77
+ . port, . version
78
+ ]
79
+ let canonicalNames = validProperties. map { $0. rawValue. lowercased ( ) }
80
+ return Dictionary ( uniqueKeysWithValues: zip ( canonicalNames, validProperties) )
81
+ } ( )
82
+
83
+ init ? ( attributeName: String ) {
84
+ let canonical = attributeName. lowercased ( )
85
+ switch HTTPCookiePropertyKey . _setCookieAttributes [ canonical] {
86
+ case let property? : self = property
87
+ case nil : return nil
88
+ }
89
+ }
90
+ }
91
+
72
92
/// `HTTPCookie` represents an http cookie.
73
93
///
74
94
/// An `HTTPCookie` instance represents a single http cookie. It is
@@ -123,11 +143,6 @@ open class HTTPCookie : NSObject {
123
143
static let _allFormatters : [ DateFormatter ]
124
144
= [ _formatter1, _formatter2, _formatter3]
125
145
126
- static let _attributes : [ HTTPCookiePropertyKey ]
127
- = [ . name, . value, . originURL, . version, . domain,
128
- . path, . secure, . expires, . comment, . commentURL,
129
- . discard, . maximumAge, . port]
130
-
131
146
/// Initialize a HTTPCookie object with a dictionary of parameters
132
147
///
133
148
/// - Parameter properties: The dictionary of properties to be used to
@@ -412,30 +427,74 @@ open class HTTPCookie : NSObject {
412
427
/// - Returns: An array of HTTPCookie objects
413
428
open class func cookies( withResponseHeaderFields headerFields: [ String : String ] , for URL: URL ) -> [ HTTPCookie ] {
414
429
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
430
+ // HTTP Cookie parsing based on RFC 6265: https://tools.ietf.org/html/rfc6265
431
+ // Though RFC6265 suggests that multiple cookies cannot be folded into a single Set-Cookie field, this is
432
+ // pretty common. It also suggests that commas and semicolons among other characters, cannot be a part of
418
433
// 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)
434
+ // support commas and semicolons in names and values(except for dates)
420
435
421
436
guard let cookies: String = headerFields [ " Set-Cookie " ] else { return [ ] }
422
437
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
438
+ var httpCookies : [ HTTPCookie ] = [ ]
430
439
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)
440
+ // Let's do old school parsing, which should allow us to handle the
441
+ // embedded commas correctly.
442
+ var idx : String . Index = cookies. startIndex
443
+ let end : String . Index = cookies. endIndex
444
+ while idx < end {
445
+ // Skip leading spaces.
446
+ while idx < end && cookies [ idx] . isSpace {
447
+ idx = cookies. index ( after: idx)
448
+ }
449
+ let cookieStartIdx : String . Index = idx
450
+ var cookieEndIdx : String . Index = idx
451
+
452
+ while idx < end {
453
+ // Scan to the next comma, but check that the comma is not a
454
+ // legal comma in a value, by looking ahead for the token,
455
+ // which indicates the comma was separating cookies.
456
+ let cookiesRest = cookies [ idx..< end]
457
+ if let commaIdx = cookiesRest. firstIndex ( of: " , " ) {
458
+ // We are looking for WSP* TOKEN_CHAR+ WSP* '='
459
+ var lookaheadIdx = cookies. index ( after: commaIdx)
460
+ // Skip whitespace
461
+ while lookaheadIdx < end && cookies [ lookaheadIdx] . isSpace {
462
+ lookaheadIdx = cookies. index ( after: lookaheadIdx)
463
+ }
464
+ // Skip over the token characters
465
+ var tokenLength = 0
466
+ while lookaheadIdx < end && cookies [ lookaheadIdx] . isTokenCharacter {
467
+ lookaheadIdx = cookies. index ( after: lookaheadIdx)
468
+ tokenLength += 1
469
+ }
470
+ // Skip whitespace
471
+ while lookaheadIdx < end && cookies [ lookaheadIdx] . isSpace {
472
+ lookaheadIdx = cookies. index ( after: lookaheadIdx)
473
+ }
474
+ // Check there was a token, and there's an equals.
475
+ if lookaheadIdx < end && cookies [ lookaheadIdx] == " = " && tokenLength > 0 {
476
+ // We found a token after the comma, this is a cookie
477
+ // separator, and not an embedded comma.
478
+ idx = cookies. index ( after: commaIdx)
479
+ cookieEndIdx = commaIdx
480
+ break
481
+ }
482
+ // Otherwise, keep scanning from the comma.
483
+ idx = cookies. index ( after: commaIdx)
484
+ cookieEndIdx = idx
485
+ } else {
486
+ // No more commas, skip to the end.
487
+ idx = end
488
+ cookieEndIdx = end
489
+ break
490
+ }
491
+ }
434
492
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 ] ] ) {
493
+ if cookieEndIdx <= cookieStartIdx {
494
+ continue
495
+ }
496
+
497
+ if let aCookie = createHttpCookie ( url: URL, cookie: String ( cookies [ cookieStartIdx..< cookieEndIdx] ) ) {
439
498
httpCookies. append ( aCookie)
440
499
}
441
500
}
@@ -444,62 +503,99 @@ open class HTTPCookie : NSObject {
444
503
}
445
504
446
505
//Bake a cookie
447
- private class func createHttpCookie( url: URL , pairs : ArraySlice < String > ) -> HTTPCookie ? {
506
+ private class func createHttpCookie( url: URL , cookie : String ) -> HTTPCookie ? {
448
507
var properties : [ HTTPCookiePropertyKey : Any ] = [ : ]
449
- for pair in pairs {
450
- 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
508
+ let scanner = Scanner ( string: cookie)
509
+
510
+ guard let nameValuePair = scanner. scanUpToString ( " ; " ) else {
511
+ // if the scanner does not read anything, there's no cookie
512
+ return nil
513
+ }
514
+
515
+ guard case ( let name? , let value? ) = splitNameValue ( nameValuePair) else {
516
+ return nil
517
+ }
518
+
519
+ properties [ . name] = name
520
+ properties [ . value] = value
521
+ properties [ . originURL] = url
522
+
523
+ while scanner. scanString ( " ; " ) != nil {
524
+ if let attribute = scanner. scanUpToString ( " ; " ) {
525
+ switch splitNameValue ( attribute) {
526
+ case ( nil , _) :
527
+ // ignore empty attribute names
528
+ break
529
+ case ( let name? , nil ) :
530
+ switch HTTPCookiePropertyKey ( attributeName: name) {
531
+ case . secure? :
532
+ properties [ . secure] = " TRUE "
533
+ case . discard? :
534
+ properties [ . discard] = " TRUE "
535
+ default :
536
+ // ignore unknown attributes like HttpOnly
537
+ break
538
+ }
539
+ case ( let name? , let value? ) :
540
+ switch HTTPCookiePropertyKey ( attributeName: name) {
541
+ case . comment? :
542
+ properties [ . comment] = value
543
+ case . commentURL? :
544
+ properties [ . commentURL] = value
545
+ case . domain? :
546
+ properties [ . domain] = value
547
+ case . maximumAge? :
548
+ properties [ . maximumAge] = value
549
+ case . path? :
550
+ properties [ . path] = value
551
+ case . port? :
552
+ properties [ . port] = value
553
+ case . version? :
554
+ properties [ . version] = value
555
+ case . expires? :
556
+ properties [ . expires] = value
557
+ default :
558
+ // ignore unknown attributes
559
+ break
560
+ }
561
+ }
454
562
}
455
- properties [ canonicalize ( name) ] = value
456
563
}
457
564
458
565
// If domain wasn't provided, extract it from the URL
459
566
if properties [ . domain] == nil {
460
567
properties [ . domain] = url. host
461
568
}
462
569
463
- //the default Path is "/"
464
- if properties [ . path] == nil {
570
+ // the default Path is "/"
571
+ if let path = properties [ . path] as? String , path. first == " / " {
572
+ // do nothing
573
+ } else {
465
574
properties [ . path] = " / "
466
575
}
467
576
468
577
return HTTPCookie ( properties: properties)
469
578
}
470
579
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 ( )
580
+ private class func splitNameValue( _ pair: String ) -> ( name: String ? , value: String ? ) {
581
+ let scanner = Scanner ( string: pair)
582
+
583
+ guard let name = scanner. scanUpToString ( " = " ) ? . trim ( ) ,
584
+ !name. isEmpty else {
585
+ // if the scanner does not read anything, or the trimmed name is
586
+ // empty, there's no name=value
587
+ return ( nil , nil )
475
588
}
476
- return value
477
- }
478
589
479
- //These cookie attributes are defined in RFC 6265 and 2965(which is obsolete)
480
- //HTTPCookie supports these
481
- private class func isCookieAttribute( _ string: String ) -> Bool {
482
- return _attributes. first ( where: { $0. rawValue. caseInsensitiveCompare ( string) == . orderedSame} ) != nil
483
- }
590
+ guard scanner. scanString ( " = " ) != nil else {
591
+ // if the scanner does not find =, there's no value
592
+ return ( name, nil )
593
+ }
484
594
485
- //Cookie attribute names are case-insensitive as per RFC6265: https://tools.ietf.org/html/rfc6265
486
- //but HTTPCookie needs only the first letter of each attribute in uppercase
487
- private class func canonicalize( _ name: String ) -> HTTPCookiePropertyKey {
488
- let idx = _attributes. firstIndex ( where: { $0. rawValue. caseInsensitiveCompare ( name) == . orderedSame} ) !
489
- return _attributes [ idx]
490
- }
595
+ let location = scanner. scanLocation
596
+ let value = String ( pair [ pair. index ( pair. startIndex, offsetBy: location) ..< pair. endIndex] ) . trim ( )
491
597
492
- //A name=value pair should be translated to two properties, Name=name and Value=value
493
- private class func createNameValuePair( pair: String ) -> [ String ] {
494
- if pair. caseInsensitiveCompare ( HTTPCookiePropertyKey . secure. rawValue) == . orderedSame {
495
- return [ " Secure=TRUE " ]
496
- }
497
- let name = pair. components ( separatedBy: " = " ) [ 0 ]
498
- let value = pair. components ( separatedBy: " \( name) = " ) [ 1 ]
499
- if !isCookieAttribute( name) {
500
- return [ " Name= \( name) " , " Value= \( value) " ]
501
- }
502
- return [ pair]
598
+ return ( name, value)
503
599
}
504
600
505
601
/// Returns a dictionary representation of the receiver.
@@ -654,12 +750,24 @@ fileprivate extension String {
654
750
func trim( ) -> String {
655
751
return self . trimmingCharacters ( in: . whitespacesAndNewlines)
656
752
}
753
+ }
657
754
658
- func maskCommas( ) -> String {
659
- return self . replacingOccurrences ( of: " , " , with: " &comma " )
755
+ fileprivate extension Character {
756
+ var isSpace : Bool {
757
+ return self == " " || self == " \t " || self == " \n " || self == " \r "
660
758
}
661
759
662
- func unmaskCommas( ) -> String {
663
- return self . replacingOccurrences ( of: " &comma " , with: " , " )
760
+ var isTokenCharacter : Bool {
761
+ guard let asciiValue = self . asciiValue else {
762
+ return false
763
+ }
764
+
765
+ // CTL, 0-31 and DEL (127)
766
+ if asciiValue <= 31 || asciiValue >= 127 {
767
+ return false
768
+ }
769
+
770
+ let nonTokenCharacters = " ()<>@,;: \\ \" /[]?={} \t "
771
+ return !nonTokenCharacters. contains ( self )
664
772
}
665
773
}
0 commit comments