12
12
//
13
13
//===----------------------------------------------------------------------===//
14
14
15
- import Foundation
16
15
import NIOHTTP1
17
16
18
17
extension HTTPClient {
@@ -26,8 +25,8 @@ extension HTTPClient {
26
25
public var path : String
27
26
/// The domain of the cookie.
28
27
public var domain : String ?
29
- /// The cookie's expiration date.
30
- public var expires : Date ?
28
+ /// The cookie's expiration date, as a number of seconds since the Unix epoch .
29
+ var expires_timestamp : Int64 ?
31
30
/// The cookie's age in seconds.
32
31
public var maxAge : Int ?
33
32
/// Whether the cookie should only be sent to HTTP servers.
@@ -42,38 +41,29 @@ extension HTTPClient {
42
41
/// - defaultDomain: Default domain to use if cookie was sent without one.
43
42
/// - returns: nil if the header is invalid.
44
43
public init ? ( header: String , defaultDomain: String ) {
45
- let components = header. components ( separatedBy: " ; " ) . map {
46
- $0. trimmingCharacters ( in: . whitespaces)
47
- }
44
+ let components = header. split ( separator: " ; " , omittingEmptySubsequences: false )
48
45
49
- if components. isEmpty {
46
+ guard let keyValuePair = components. first ? . trimmingWhitespace ( ) else {
50
47
return nil
51
48
}
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 {
49
+ guard let ( trimmedName, trimmedValue) = keyValuePair. parseKeyValuePair ( ) else {
58
50
return nil
59
51
}
60
-
61
- self . name = nameAndValue [ 0 ]
62
- self . value = nameAndValue [ 1 ] . omittingQuotes ( )
63
-
64
- guard !self . name. isEmpty else {
52
+ guard !trimmedName. isEmpty else {
65
53
return nil
66
54
}
67
55
56
+ self . name = String ( trimmedName)
57
+ self . value = String ( trimmedValue. omittingQuotes ( ) )
68
58
self . path = " / "
69
59
self . domain = defaultDomain
70
- self . expires = nil
60
+ self . expires_timestamp = nil
71
61
self . maxAge = nil
72
62
self . httpOnly = false
73
63
self . secure = false
74
64
75
65
for component in components [ 1 ... ] {
76
- switch self . parseComponent ( component ) {
66
+ switch component . parseCookieComponent ( ) {
77
67
case ( nil , nil ) :
78
68
continue
79
69
case ( " path " , . some( let value) ) :
@@ -84,27 +74,7 @@ extension HTTPClient {
84
74
guard let value = value else {
85
75
continue
86
76
}
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
- }
77
+ self . expires_timestamp = parseCookieTime ( value)
108
78
case ( " max-age " , let value) :
109
79
self . maxAge = value. flatMap ( Int . init)
110
80
case ( " secure " , nil ) :
@@ -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 {
121
+ extension StringProtocol {
122
+ fileprivate func omittingQuotes( ) -> SubSequence {
158
123
let dquote = " \" "
159
- if ! hasPrefix( dquote) || ! hasSuffix( dquote) {
160
- return self
124
+ guard hasPrefix ( dquote) , hasSuffix ( dquote) else {
125
+ return self [ startIndex ..< endIndex ]
161
126
}
127
+ return self . dropFirst ( ) . dropLast ( )
128
+ }
162
129
163
- let begin = index ( after: startIndex)
164
- let end = index ( before: endIndex)
165
- return String ( self [ begin..< end] )
130
+ fileprivate func trimmingWhitespace( ) -> SubSequence {
131
+ guard let start = self . firstIndex ( where: { !$0. isWhitespace } ) else {
132
+ return self [ self . endIndex..< self . endIndex]
133
+ }
134
+ let end = self . lastIndex ( where: { !$0. isWhitespace } ) !
135
+ return self [ start... end]
136
+ }
137
+
138
+ /// Splits this string at the first "=" sign, and trims whitespace from each side.
139
+ fileprivate func parseKeyValuePair( ) -> ( key: SubSequence , value: SubSequence ) ? {
140
+ guard let keyValueSeparator = self . firstIndex ( of: " = " ) else {
141
+ return nil
142
+ }
143
+ let trimmedName = self [ ..< keyValueSeparator] . trimmingWhitespace ( )
144
+ let trimmedValue = self [ self . index ( after: keyValueSeparator) ... ] . trimmingWhitespace ( )
145
+ return ( trimmedName, trimmedValue)
146
+ }
147
+
148
+ /// If this string is a key-value pair, splits it and normalizes the key.
149
+ /// Otherwise, return this string, trimmed of whitespace and normalized to lowercase.
150
+ fileprivate func parseCookieComponent( ) -> ( String ? , String ? ) {
151
+ guard let ( trimmedName, trimmedValue) = self . parseKeyValuePair ( ) else {
152
+ let trimmedName = self . trimmingWhitespace ( )
153
+ return ( trimmedName. isEmpty ? nil : trimmedName. lowercased ( ) , nil )
154
+ }
155
+ guard !trimmedName. isEmpty else {
156
+ return ( nil , nil )
157
+ }
158
+ return ( trimmedName. lowercased ( ) , trimmedValue. isEmpty ? nil : String ( trimmedValue) )
166
159
}
167
160
}
168
161
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) }
162
+ #if canImport(Darwin)
163
+ import Darwin
164
+ #elseif canImport(Glibc)
165
+ import Glibc
166
+ #endif
167
+ import CShims
168
+
169
+ private let posixLocale : locale_t = {
170
+ // All POSIX systems must provide a "POSIX" locale, and its date/time formats are US English.
171
+ // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap07.html#tag_07_03_05
172
+ newlocale ( LC_TIME_MASK | LC_NUMERIC_MASK, " POSIX " , nil ) !
173
+ } ( )
174
+
175
+ private func parseCookieTime( _ string: String ) -> Int64 ? {
176
+ var timeComps = tm ( )
177
+ var hasExplicitTimeZone = false
178
+ parse: do {
179
+ if swiftahc_cshims_strptime_l ( string, " %a, %d %b %Y %H:%M:%S %Z " , & timeComps, . init( posixLocale) ) != nil {
180
+ hasExplicitTimeZone = true
181
+ break parse
182
+ }
183
+ timeComps = tm ( )
184
+ if swiftahc_cshims_strptime_l ( string, " %a, %d-%b-%y %H:%M:%S %Z " , & timeComps, . init( posixLocale) ) != nil {
185
+ hasExplicitTimeZone = true
186
+ break parse
187
+ }
188
+ timeComps = tm ( )
189
+ if swiftahc_cshims_strptime_l ( string, " %a %b %d %H:%M:%S %Y " , & timeComps, . init( posixLocale) ) != nil {
190
+ break parse
191
+ }
192
+ return nil
193
+ }
194
+ #if canImport(Darwin)
195
+ // Darwin converts values with explicit time-zones to the local time-zone. Glibc appears not to.
196
+ // https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/strptime.3.html
197
+ if hasExplicitTimeZone {
198
+ let timestamp = Int64 ( mktime ( & timeComps) )
199
+ return timestamp == - 1 && errno == EOVERFLOW ? nil : timestamp
173
200
}
201
+ #endif
202
+ let timestamp = Int64 ( timegm ( & timeComps) )
203
+ return timestamp == - 1 && errno == EOVERFLOW ? nil : timestamp
174
204
}
0 commit comments