12
12
//
13
13
//===----------------------------------------------------------------------===//
14
14
15
- import Foundation
16
15
import NIOHTTP1
16
+ #if canImport(Darwin)
17
+ import Darwin
18
+ #elseif canImport(Glibc)
19
+ import Glibc
20
+ #endif
21
+ import CAsyncHTTPClient
17
22
18
23
extension HTTPClient {
19
24
/// A representation of an HTTP cookie.
@@ -26,8 +31,8 @@ extension HTTPClient {
26
31
public var path : String
27
32
/// The domain of the cookie.
28
33
public var domain : String ?
29
- /// The cookie's expiration date.
30
- public var expires : Date ?
34
+ /// The cookie's expiration date, as a number of seconds since the Unix epoch .
35
+ var expires_timestamp : Int64 ?
31
36
/// The cookie's age in seconds.
32
37
public var maxAge : Int ?
33
38
/// Whether the cookie should only be sent to HTTP servers.
@@ -42,74 +47,39 @@ extension HTTPClient {
42
47
/// - defaultDomain: Default domain to use if cookie was sent without one.
43
48
/// - returns: nil if the header is invalid.
44
49
public init ? ( header: String , defaultDomain: String ) {
45
- let components = header. components ( separatedBy: " ; " ) . map {
46
- $0. trimmingCharacters ( in: . whitespaces)
47
- }
48
-
49
- if components. isEmpty {
50
+ var components = header. utf8. split ( separator: UInt8 ( ascii: " ; " ) , omittingEmptySubsequences: false ) [ ... ]
51
+ guard let keyValuePair = components. popFirst ( ) ? . trimmingASCIISpaces ( ) else {
50
52
return nil
51
53
}
52
-
53
- let nameAndValue = components [ 0 ] . split ( separator: " = " , maxSplits: 1 , omittingEmptySubsequences: false ) . map {
54
- $0. trimmingCharacters ( in: . whitespaces)
55
- }
56
-
57
- guard nameAndValue. count == 2 else {
54
+ guard let ( trimmedName, trimmedValue) = keyValuePair. parseKeyValuePair ( ) else {
58
55
return nil
59
56
}
60
-
61
- self . name = nameAndValue [ 0 ]
62
- self . value = nameAndValue [ 1 ] . omittingQuotes ( )
63
-
64
- guard !self . name. isEmpty else {
57
+ guard !trimmedName. isEmpty else {
65
58
return nil
66
59
}
67
60
61
+ self . name = String ( decoding: trimmedName, as: UTF8 . self)
62
+ self . value = String ( decoding: trimmedValue. trimmingPairedASCIIQuote ( ) , as: UTF8 . self)
68
63
self . path = " / "
69
64
self . domain = defaultDomain
70
- self . expires = nil
65
+ self . expires_timestamp = nil
71
66
self . maxAge = nil
72
67
self . httpOnly = false
73
68
self . secure = false
74
69
75
- for component in components [ 1 ... ] {
76
- switch self . parseComponent ( component) {
77
- case ( nil , nil ) :
78
- continue
79
- case ( " path " , . some( let value) ) :
70
+ for component in components {
71
+ switch component. parseCookieComponent ( ) {
72
+ case ( " path " , . some( let value) ) ? :
80
73
self . path = value
81
- case ( " domain " , . some( let value) ) :
74
+ case ( " domain " , . some( let value) ) ? :
82
75
self . domain = value
83
- case ( " expires " , let value) :
84
- guard let value = value else {
85
- continue
86
- }
87
-
88
- let formatter = DateFormatter ( )
89
- formatter. locale = Locale ( identifier: " en_US " )
90
- formatter. timeZone = TimeZone ( identifier: " GMT " )
91
-
92
- formatter. dateFormat = " EEE, dd MMM yyyy HH:mm:ss z "
93
- if let date = formatter. date ( from: value) {
94
- self . expires = date
95
- continue
96
- }
97
-
98
- formatter. dateFormat = " EEE, dd-MMM-yy HH:mm:ss z "
99
- if let date = formatter. date ( from: value) {
100
- self . expires = date
101
- continue
102
- }
103
-
104
- formatter. dateFormat = " EEE MMM d hh:mm:s yyyy "
105
- if let date = formatter. date ( from: value) {
106
- self . expires = date
107
- }
108
- case ( " max-age " , let value) :
76
+ case ( " expires " , . some( let value) ) ? :
77
+ self . expires_timestamp = parseCookieTime ( value)
78
+ case ( " max-age " , let value) ? :
109
79
self . maxAge = value. flatMap ( Int . init)
110
- case ( " secure " , nil ) :
80
+ case ( " secure " , nil ) ? :
111
81
self . secure = true
112
- case ( " httponly " , nil ) :
82
+ case ( " httponly " , nil ) ? :
113
83
self . httpOnly = true
114
84
default :
115
85
continue
@@ -124,51 +94,111 @@ extension HTTPClient {
124
94
/// - value: The cookie's string value.
125
95
/// - path: The cookie's path.
126
96
/// - domain: The domain of the cookie, defaults to nil.
127
- /// - expires : The cookie's expiration date, defaults to nil.
97
+ /// - expires_timestamp : The cookie's expiration date, as a number of seconds since the Unix epoch. defaults to nil.
128
98
/// - maxAge: The cookie's age in seconds, defaults to nil.
129
99
/// - httpOnly: Whether this cookie should be used by HTTP servers only, defaults to false.
130
100
/// - secure: Whether this cookie should only be sent using secure channels, defaults to false.
131
- public init ( name: String , value: String , path: String = " / " , domain: String ? = nil , expires : Date ? = nil , maxAge: Int ? = nil , httpOnly: Bool = false , secure: Bool = false ) {
101
+ internal init ( name: String , value: String , path: String = " / " , domain: String ? = nil , expires_timestamp : Int64 ? = nil , maxAge: Int ? = nil , httpOnly: Bool = false , secure: Bool = false ) {
132
102
self . name = name
133
103
self . value = value
134
104
self . path = path
135
105
self . domain = domain
136
- self . expires = expires
106
+ self . expires_timestamp = expires_timestamp
137
107
self . maxAge = maxAge
138
108
self . httpOnly = httpOnly
139
109
self . secure = secure
140
110
}
111
+ }
112
+ }
141
113
142
- func parseComponent( _ component: String ) -> ( String ? , String ? ) {
143
- let nameAndValue = component. split ( separator: " = " , maxSplits: 1 ) . map {
144
- $0. trimmingCharacters ( in: . whitespaces)
145
- }
146
- if nameAndValue. count == 2 {
147
- return ( nameAndValue [ 0 ] . lowercased ( ) , nameAndValue [ 1 ] )
148
- } else if nameAndValue. count == 1 {
149
- return ( nameAndValue [ 0 ] . lowercased ( ) , nil )
150
- }
151
- return ( nil , nil )
152
- }
114
+ extension HTTPClient . Response {
115
+ /// List of HTTP cookies returned by the server.
116
+ public var cookies : [ HTTPClient . Cookie ] {
117
+ return self . headers [ " set-cookie " ] . compactMap { HTTPClient . Cookie ( header: $0, defaultDomain: self . host) }
153
118
}
154
119
}
155
120
156
- extension String {
157
- fileprivate func omittingQuotes( ) -> String {
158
- let dquote = " \" "
159
- if !hasPrefix( dquote) || !hasSuffix( dquote) {
160
- return self
121
+ extension String . UTF8View . SubSequence {
122
+ fileprivate func trimmingASCIISpaces( ) -> SubSequence {
123
+ guard let start = self . firstIndex ( where: { $0 != UInt8 ( ascii: " " ) } ) else {
124
+ return self [ self . endIndex..< self . endIndex]
125
+ }
126
+ let end = self . lastIndex ( where: { $0 != UInt8 ( ascii: " " ) } ) !
127
+ return self [ start... end]
128
+ }
129
+
130
+ /// If this collection begins and ends with an ASCII double-quote ("),
131
+ /// returns a version of self trimmed of those quotes. Otherwise, returns self.
132
+ fileprivate func trimmingPairedASCIIQuote( ) -> SubSequence {
133
+ let quoteChar = UInt8 ( ascii: " \" " )
134
+ var trimmed = self
135
+ if trimmed. popFirst ( ) == quoteChar && trimmed. popLast ( ) == quoteChar {
136
+ return trimmed
137
+ }
138
+ return self
139
+ }
140
+
141
+ /// Splits this collection in to a key and value at the first ASCII '=' character.
142
+ /// Both the key and value are trimmed of ASCII spaces.
143
+ fileprivate func parseKeyValuePair( ) -> ( key: SubSequence , value: SubSequence ) ? {
144
+ guard let keyValueSeparator = self . firstIndex ( of: UInt8 ( ascii: " = " ) ) else {
145
+ return nil
161
146
}
147
+ let trimmedName = self [ ..< keyValueSeparator] . trimmingASCIISpaces ( )
148
+ let trimmedValue = self [ self . index ( after: keyValueSeparator) ... ] . trimmingASCIISpaces ( )
149
+ return ( trimmedName, trimmedValue)
150
+ }
162
151
163
- let begin = index ( after: startIndex)
164
- let end = index ( before: endIndex)
165
- return String ( self [ begin..< end] )
152
+ /// Parses this collection as either a key-value pair, or a plain key.
153
+ /// The returned key is trimmed of ASCII spaces and normalized to lowercase.
154
+ /// The returned value is trimmed of ASCII spaces.
155
+ fileprivate func parseCookieComponent( ) -> ( key: String , value: String ? ) ? {
156
+ let ( trimmedName, trimmedValue) = self . parseKeyValuePair ( ) ?? ( self . trimmingASCIISpaces ( ) , nil )
157
+ guard !trimmedName. isEmpty else {
158
+ return nil
159
+ }
160
+ return ( String ( decoding: trimmedName, as: UTF8 . self) . lowercased ( ) , trimmedValue. map { String ( decoding: $0, as: UTF8 . self) } )
166
161
}
167
162
}
168
163
169
- extension HTTPClient . Response {
170
- /// List of HTTP cookies returned by the server.
171
- public var cookies : [ HTTPClient . Cookie ] {
172
- return self . headers [ " set-cookie " ] . compactMap { HTTPClient . Cookie ( header: $0, defaultDomain: self . host) }
164
+ private let posixLocale : UnsafeMutableRawPointer = {
165
+ // All POSIX systems must provide a "POSIX" locale, and its date/time formats are US English.
166
+ // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap07.html#tag_07_03_05
167
+ let _posixLocale = newlocale ( LC_TIME_MASK | LC_NUMERIC_MASK, " POSIX " , nil ) !
168
+ return UnsafeMutableRawPointer ( _posixLocale)
169
+ } ( )
170
+
171
+ private func parseTimestamp( _ string: String , format: String ) -> tm ? {
172
+ var timeComponents = tm ( )
173
+ guard swiftahc_cshims_strptime_l ( string, format, & timeComponents, posixLocale) else {
174
+ return nil
175
+ }
176
+ return timeComponents
177
+ }
178
+
179
+ private func parseCookieTime( _ string: String ) -> Int64 ? {
180
+ var string = string
181
+ if string. utf8. contains ( where: { $0 < 0x20 /* Control characters */ || $0 == 0x7F /* DEL */ } ) {
182
+ return nil
183
+ }
184
+ if string. hasSuffix ( " GMT " ) {
185
+ let timezoneStart = string. index ( string. endIndex, offsetBy: - 3 )
186
+ guard let trimmedTimestampLast = string [ ..< timezoneStart] . lastIndex ( where: { $0 != " " } ) else {
187
+ return nil
188
+ }
189
+ let trimmedTimestampEndIndex = string. index ( after: trimmedTimestampLast)
190
+ guard trimmedTimestampEndIndex != timezoneStart else {
191
+ return nil
192
+ }
193
+ string. removeSubrange ( trimmedTimestampEndIndex... )
194
+ }
195
+ guard
196
+ var timeComponents = parseTimestamp ( string, format: " %a, %d %b %Y %H:%M:%S " )
197
+ ?? parseTimestamp ( string, format: " %a, %d-%b-%y %H:%M:%S " )
198
+ ?? parseTimestamp ( string, format: " %a %b %d %H:%M:%S %Y " )
199
+ else {
200
+ return nil
173
201
}
202
+ let timestamp = Int64 ( timegm ( & timeComponents) )
203
+ return timestamp == - 1 && errno == EOVERFLOW ? nil : timestamp
174
204
}
0 commit comments