@@ -17,14 +17,6 @@ public struct HTTPCookiePropertyKey : RawRepresentable, Equatable, Hashable {
17
17
public init ( rawValue: String ) {
18
18
self . rawValue = rawValue
19
19
}
20
-
21
- public var hashValue : Int {
22
- return self . rawValue. hashValue
23
- }
24
-
25
- public static func == ( _ lhs: HTTPCookiePropertyKey , _ rhs: HTTPCookiePropertyKey ) -> Bool {
26
- return lhs. rawValue == rhs. rawValue
27
- }
28
20
}
29
21
30
22
extension HTTPCookiePropertyKey {
@@ -71,6 +63,28 @@ extension HTTPCookiePropertyKey {
71
63
internal static let created = HTTPCookiePropertyKey ( rawValue: " Created " )
72
64
}
73
65
66
+ internal extension HTTPCookiePropertyKey {
67
+ static let httpOnly = HTTPCookiePropertyKey ( rawValue: " HttpOnly " )
68
+
69
+ static private let _setCookieAttributes : [ String : HTTPCookiePropertyKey ] = {
70
+ // Only some attributes are valid in the Set-Cookie header.
71
+ let validProperties : [ HTTPCookiePropertyKey ] = [
72
+ . expires, . maximumAge, . domain, . path, . secure, . comment,
73
+ . commentURL, . discard, . port, . version, . httpOnly
74
+ ]
75
+ let canonicalNames = validProperties. map { $0. rawValue. lowercased ( ) }
76
+ return Dictionary ( uniqueKeysWithValues: zip ( canonicalNames, validProperties) )
77
+ } ( )
78
+
79
+ init ? ( attributeName: String ) {
80
+ let canonical = attributeName. lowercased ( )
81
+ switch HTTPCookiePropertyKey . _setCookieAttributes [ canonical] {
82
+ case let property? : self = property
83
+ case nil : return nil
84
+ }
85
+ }
86
+ }
87
+
74
88
/// `HTTPCookie` represents an http cookie.
75
89
///
76
90
/// An `HTTPCookie` instance represents a single http cookie. It is
@@ -125,11 +139,6 @@ open class HTTPCookie : NSObject {
125
139
static let _allFormatters : [ DateFormatter ]
126
140
= [ _formatter1, _formatter2, _formatter3]
127
141
128
- static let _attributes : [ HTTPCookiePropertyKey ]
129
- = [ . name, . value, . originURL, . version, . domain,
130
- . path, . secure, . expires, . comment, . commentURL,
131
- . discard, . maximumAge, . port]
132
-
133
142
/// Initialize a HTTPCookie object with a dictionary of parameters
134
143
///
135
144
/// - Parameter properties: The dictionary of properties to be used to
@@ -255,10 +264,16 @@ open class HTTPCookie : NSObject {
255
264
/// - Experiment: This is a draft API currently under consideration for official import into Foundation as a suitable alternative
256
265
/// - Note: Since this API is under consideration it may be either removed or revised in the near future
257
266
public init ? ( properties: [ HTTPCookiePropertyKey : Any ] ) {
267
+ func stringValue( _ strVal: Any ? ) -> String ? {
268
+ if let subStr = strVal as? Substring {
269
+ return String ( subStr)
270
+ }
271
+ return strVal as? String
272
+ }
258
273
guard
259
- let path = properties [ . path] as? String ,
260
- let name = properties [ . name] as? String ,
261
- let value = properties [ . value] as? String
274
+ let path = stringValue ( properties [ . path] ) ,
275
+ let name = stringValue ( properties [ . name] ) ,
276
+ let value = stringValue ( properties [ . value] )
262
277
else {
263
278
return nil
264
279
}
@@ -313,7 +328,7 @@ open class HTTPCookie : NSObject {
313
328
}
314
329
315
330
var expDate : Date ? = nil
316
- // Maximum-Age is prefered over expires-Date but only version 1 cookies use Maximum-Age
331
+ // Maximum-Age is preferred over expires-Date but only version 1 cookies use Maximum-Age
317
332
if let maximumAge = properties [ . maximumAge] as? String ,
318
333
let secondsFromNow = Int ( maximumAge) {
319
334
if version == 1 {
@@ -344,8 +359,12 @@ open class HTTPCookie : NSObject {
344
359
} else {
345
360
_commentURL = nil
346
361
}
347
- _HTTPOnly = false
348
362
363
+ if let httpOnlyString = properties [ . httpOnly] as? String {
364
+ _HTTPOnly = httpOnlyString == " TRUE "
365
+ } else {
366
+ _HTTPOnly = false
367
+ }
349
368
350
369
_properties = [
351
370
. created : Date ( ) . timeIntervalSinceReferenceDate, // Cocoa Compatibility
@@ -404,34 +423,78 @@ open class HTTPCookie : NSObject {
404
423
/// This method will ignore irrelevant header fields so
405
424
/// you can pass a dictionary containing data other than cookie data.
406
425
/// - Parameter headerFields: The response header fields to check for cookies.
407
- /// - Parameter URL: The URL that the cookies came from - relevant to how the cookies are interpeted .
426
+ /// - Parameter URL: The URL that the cookies came from - relevant to how the cookies are interpreted .
408
427
/// - Returns: An array of HTTPCookie objects
409
428
open class func cookies( withResponseHeaderFields headerFields: [ String : String ] , for URL: URL ) -> [ HTTPCookie ] {
410
429
411
- //HTTP Cookie parsing based on RFC 6265: https://tools.ietf.org/html/rfc6265
412
- //Though RFC6265 suggests that multiple cookies cannot be folded into a single Set-Cookie field, this is
413
- //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
414
433
// names and values. This implementation takes care of multiple cookies in the same field, however it doesn't
415
- //support commas and semicolons in names and values(except for dates)
434
+ // support commas and semicolons in names and values(except for dates)
416
435
417
436
guard let cookies: String = headerFields [ " Set-Cookie " ] else { return [ ] }
418
437
419
- let nameValuePairs = cookies. components ( separatedBy: " ; " ) //split the name/value and attribute/value pairs
420
- . map ( { $0. trim ( ) } ) //trim whitespaces
421
- . map ( { removeCommaFromDate ( $0) } ) //get rid of commas in dates
422
- . flatMap ( { $0. components ( separatedBy: " , " ) } ) //cookie boundaries are marked by commas
423
- . map ( { $0. trim ( ) } ) //trim again
424
- . filter ( { $0. caseInsensitiveCompare ( " HTTPOnly " ) != . orderedSame} ) //we don't use HTTPOnly, do we?
425
- . flatMap ( { createNameValuePair ( pair: $0) } ) //create Name and Value properties
438
+ var httpCookies : [ HTTPCookie ] = [ ]
439
+
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
+ }
426
492
427
- //mark cookie boundaries in the name-value array
428
- var cookieIndices = ( 0 ..< nameValuePairs . count ) . filter ( { nameValuePairs [ $0 ] . hasPrefix ( " Name " ) } )
429
- cookieIndices . append ( nameValuePairs . count )
493
+ if cookieEndIdx <= cookieStartIdx {
494
+ continue
495
+ }
430
496
431
- //bake the cookies
432
- var httpCookies : [ HTTPCookie ] = [ ]
433
- for i in 0 ..< cookieIndices. count- 1 {
434
- if let aCookie = createHttpCookie ( url: URL, pairs: nameValuePairs [ cookieIndices [ i] ..< cookieIndices [ i+ 1 ] ] ) {
497
+ if let aCookie = createHttpCookie ( url: URL, cookie: String ( cookies [ cookieStartIdx..< cookieEndIdx] ) ) {
435
498
httpCookies. append ( aCookie)
436
499
}
437
500
}
@@ -440,62 +503,118 @@ open class HTTPCookie : NSObject {
440
503
}
441
504
442
505
//Bake a cookie
443
- private class func createHttpCookie( url: URL , pairs : ArraySlice < String > ) -> HTTPCookie ? {
506
+ private class func createHttpCookie( url: URL , cookie : String ) -> HTTPCookie ? {
444
507
var properties : [ HTTPCookiePropertyKey : Any ] = [ : ]
445
- for pair in pairs {
446
- let name = pair. components ( separatedBy: " = " ) [ 0 ]
447
- var value = pair. components ( separatedBy: " \( name) = " ) [ 1 ] //a value can have an "="
448
- if canonicalize ( name) == . expires {
449
- 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
+ case . httpOnly? :
536
+ properties [ . httpOnly] = " TRUE "
537
+ default :
538
+ // ignore unknown attributes
539
+ break
540
+ }
541
+ case ( let name? , let value? ) :
542
+ switch HTTPCookiePropertyKey ( attributeName: name) {
543
+ case . comment? :
544
+ properties [ . comment] = value
545
+ case . commentURL? :
546
+ properties [ . commentURL] = value
547
+ case . domain? :
548
+ properties [ . domain] = value
549
+ case . maximumAge? :
550
+ properties [ . maximumAge] = value
551
+ case . path? :
552
+ properties [ . path] = value
553
+ case . port? :
554
+ properties [ . port] = value
555
+ case . version? :
556
+ properties [ . version] = value
557
+ case . expires? :
558
+ properties [ . expires] = value
559
+ default :
560
+ // ignore unknown attributes
561
+ break
562
+ }
563
+ }
450
564
}
451
- properties [ canonicalize ( name) ] = value
452
565
}
453
566
454
- // If domain wasn't provided, extract it from the URL
455
- if properties [ . domain] == nil {
567
+ if let domain = properties [ . domain] as? String {
568
+ // The provided domain string has to be prepended with a dot,
569
+ // because the domain field indicates that it can be sent
570
+ // subdomains of the domain (but only if it is not an IP address).
571
+ if ( !domain. hasPrefix ( " . " ) && !isIPv4Address( domain) ) {
572
+ properties [ . domain] = " . \( domain) "
573
+ }
574
+ } else {
575
+ // If domain wasn't provided, extract it from the URL. No dots in
576
+ // this case, only exact matching.
456
577
properties [ . domain] = url. host
457
578
}
579
+ // Always lowercase the domain.
580
+ if let domain = properties [ . domain] as? String {
581
+ properties [ . domain] = domain. lowercased ( )
582
+ }
458
583
459
- //the default Path is "/"
460
- if properties [ . path] == nil {
584
+ // the default Path is "/"
585
+ if let path = properties [ . path] as? String , path. first == " / " {
586
+ // do nothing
587
+ } else {
461
588
properties [ . path] = " / "
462
589
}
463
590
464
591
return HTTPCookie ( properties: properties)
465
592
}
466
593
467
- //we pass this to a map()
468
- private class func removeCommaFromDate( _ value: String ) -> String {
469
- if value. hasPrefix ( " Expires " ) || value. hasPrefix ( " expires " ) {
470
- return value. maskCommas ( )
594
+ private class func splitNameValue( _ pair: String ) -> ( name: String ? , value: String ? ) {
595
+ let scanner = Scanner ( string: pair)
596
+
597
+ guard let name = scanner. scanUpToString ( " = " ) ? . trim ( ) ,
598
+ !name. isEmpty else {
599
+ // if the scanner does not read anything, or the trimmed name is
600
+ // empty, there's no name=value
601
+ return ( nil , nil )
471
602
}
472
- return value
473
- }
474
603
475
- //These cookie attributes are defined in RFC 6265 and 2965(which is obsolete)
476
- //HTTPCookie supports these
477
- private class func isCookieAttribute( _ string: String ) -> Bool {
478
- return _attributes. first ( where: { $0. rawValue. caseInsensitiveCompare ( string) == . orderedSame} ) != nil
479
- }
604
+ guard scanner. scanString ( " = " ) != nil else {
605
+ // if the scanner does not find =, there's no value
606
+ return ( name, nil )
607
+ }
480
608
481
- //Cookie attribute names are case-insensitive as per RFC6265: https://tools.ietf.org/html/rfc6265
482
- //but HTTPCookie needs only the first letter of each attribute in uppercase
483
- private class func canonicalize( _ name: String ) -> HTTPCookiePropertyKey {
484
- let idx = _attributes. index ( where: { $0. rawValue. caseInsensitiveCompare ( name) == . orderedSame} ) !
485
- return _attributes [ idx]
609
+ let location = scanner. scanLocation
610
+ let value = String ( pair [ pair. index ( pair. startIndex, offsetBy: location) ..< pair. endIndex] ) . trim ( )
611
+
612
+ return ( name, value)
486
613
}
487
614
488
- //A name=value pair should be translated to two properties, Name=name and Value=value
489
- private class func createNameValuePair( pair: String ) -> [ String ] {
490
- if pair. caseInsensitiveCompare ( HTTPCookiePropertyKey . secure. rawValue) == . orderedSame {
491
- return [ " Secure=TRUE " ]
492
- }
493
- let name = pair. components ( separatedBy: " = " ) [ 0 ]
494
- let value = pair. components ( separatedBy: " \( name) = " ) [ 1 ]
495
- if !isCookieAttribute( name) {
496
- return [ " Name= \( name) " , " Value= \( value) " ]
497
- }
498
- return [ pair]
615
+ private class func isIPv4Address( _ string: String ) -> Bool {
616
+ var x = in_addr ( )
617
+ return inet_pton ( AF_INET, string, & x) == 1
499
618
}
500
619
501
620
/// Returns a dictionary representation of the receiver.
@@ -573,7 +692,7 @@ open class HTTPCookie : NSObject {
573
692
///
574
693
/// Cookies may be marked secure by a server (or by a javascript).
575
694
/// Cookies marked as such must only be sent via an encrypted connection to
576
- /// trusted servers (i.e. via SSL or TLS), and should not be delievered to any
695
+ /// trusted servers (i.e. via SSL or TLS), and should not be delivered to any
577
696
/// javascript applications to prevent cross-site scripting vulnerabilities.
578
697
open var isSecure : Bool {
579
698
return _secure
@@ -650,13 +769,24 @@ fileprivate extension String {
650
769
func trim( ) -> String {
651
770
return self . trimmingCharacters ( in: . whitespacesAndNewlines)
652
771
}
772
+ }
653
773
654
- func maskCommas( ) -> String {
655
- return self . replacingOccurrences ( of: " , " , with: " &comma " )
774
+ fileprivate extension Character {
775
+ var isSpace : Bool {
776
+ return self == " " || self == " \t " || self == " \n " || self == " \r "
656
777
}
657
778
658
- func unmaskCommas( ) -> String {
659
- return self . replacingOccurrences ( of: " &comma " , with: " , " )
779
+ var isTokenCharacter : Bool {
780
+ guard let asciiValue = self . asciiValue else {
781
+ return false
782
+ }
783
+
784
+ // CTL, 0-31 and DEL (127)
785
+ if asciiValue <= 31 || asciiValue >= 127 {
786
+ return false
787
+ }
788
+
789
+ let nonTokenCharacters = " ()<>@,;: \\ \" /[]?={} \t "
790
+ return !nonTokenCharacters. contains ( self )
660
791
}
661
792
}
662
-
0 commit comments