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 ( utf8Slice: trimmedName, in: header)
62
+ self . value = String ( utf8Slice: trimmedValue. trimmingPairedASCIIQuote ( ) , in: header)
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) ) :
80
- self . path = value
81
- case ( " domain " , . some( let value) ) :
82
- 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) :
109
- self . maxAge = value. flatMap ( Int . init)
110
- case ( " secure " , nil ) :
70
+ for component in components {
71
+ switch component. parseCookieComponent ( ) {
72
+ case ( " path " , . some( let value) ) ? :
73
+ self . path = String ( utf8Slice: value, in: header)
74
+ case ( " domain " , . some( let value) ) ? :
75
+ self . domain = String ( utf8Slice: value, in: header)
76
+ case ( " expires " , . some( let value) ) ? :
77
+ self . expires_timestamp = parseCookieTime ( value, in: header)
78
+ case ( " max-age " , . some( let value) ) ? :
79
+ self . maxAge = Int ( String ( utf8Slice: value, in: header) )
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,116 @@ 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
121
extension String {
157
- fileprivate func omittingQuotes( ) -> String {
158
- let dquote = " \" "
159
- if !hasPrefix( dquote) || !hasSuffix( dquote) {
160
- return self
122
+ fileprivate init ( utf8Slice: String . UTF8View . SubSequence , in base: String ) {
123
+ self . init ( base [ utf8Slice. startIndex..< utf8Slice. endIndex] )
124
+ }
125
+ }
126
+
127
+ extension String . UTF8View . SubSequence {
128
+ fileprivate func trimmingASCIISpaces( ) -> SubSequence {
129
+ guard let start = self . firstIndex ( where: { $0 != UInt8 ( ascii: " " ) } ) else {
130
+ return self [ self . endIndex..< self . endIndex]
161
131
}
132
+ let end = self . lastIndex ( where: { $0 != UInt8 ( ascii: " " ) } ) !
133
+ return self [ start... end]
134
+ }
162
135
163
- let begin = index ( after: startIndex)
164
- let end = index ( before: endIndex)
165
- return String ( self [ begin..< end] )
136
+ /// If this collection begins and ends with an ASCII double-quote ("),
137
+ /// returns a version of self trimmed of those quotes. Otherwise, returns self.
138
+ fileprivate func trimmingPairedASCIIQuote( ) -> SubSequence {
139
+ let quoteChar = UInt8 ( ascii: " \" " )
140
+ var trimmed = self
141
+ if trimmed. popFirst ( ) == quoteChar && trimmed. popLast ( ) == quoteChar {
142
+ return trimmed
143
+ }
144
+ return self
145
+ }
146
+
147
+ /// Splits this collection in to a key and value at the first ASCII '=' character.
148
+ /// Both the key and value are trimmed of ASCII spaces.
149
+ fileprivate func parseKeyValuePair( ) -> ( key: SubSequence , value: SubSequence ) ? {
150
+ guard let keyValueSeparator = self . firstIndex ( of: UInt8 ( ascii: " = " ) ) else {
151
+ return nil
152
+ }
153
+ let trimmedName = self [ ..< keyValueSeparator] . trimmingASCIISpaces ( )
154
+ let trimmedValue = self [ self . index ( after: keyValueSeparator) ... ] . trimmingASCIISpaces ( )
155
+ return ( trimmedName, trimmedValue)
156
+ }
157
+
158
+ /// Parses this collection as either a key-value pair, or a plain key.
159
+ /// The returned key is trimmed of ASCII spaces and normalized to lowercase.
160
+ /// The returned value is trimmed of ASCII spaces.
161
+ fileprivate func parseCookieComponent( ) -> ( key: String , value: SubSequence ? ) ? {
162
+ let ( trimmedName, trimmedValue) = self . parseKeyValuePair ( ) ?? ( self . trimmingASCIISpaces ( ) , nil )
163
+ guard !trimmedName. isEmpty else {
164
+ return nil
165
+ }
166
+ return ( String ( decoding: trimmedName, as: UTF8 . self) . lowercased ( ) , trimmedValue)
166
167
}
167
168
}
168
169
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) }
170
+ private let posixLocale : UnsafeMutableRawPointer = {
171
+ // All POSIX systems must provide a "POSIX" locale, and its date/time formats are US English.
172
+ // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap07.html#tag_07_03_05
173
+ let _posixLocale = newlocale ( LC_TIME_MASK | LC_NUMERIC_MASK, " POSIX " , nil ) !
174
+ return UnsafeMutableRawPointer ( _posixLocale)
175
+ } ( )
176
+
177
+ private func parseTimestamp( _ string: String , format: String ) -> tm ? {
178
+ var timeComponents = tm ( )
179
+ guard swiftahc_cshims_strptime_l ( string, format, & timeComponents, posixLocale) else {
180
+ return nil
181
+ }
182
+ return timeComponents
183
+ }
184
+
185
+ private func parseCookieTime( _ timestampUTF8: String . UTF8View . SubSequence , in header: String ) -> Int64 ? {
186
+ if timestampUTF8. contains ( where: { $0 < 0x20 /* Control characters */ || $0 == 0x7F /* DEL */ } ) {
187
+ return nil
188
+ }
189
+ let timestampString : String
190
+ if timestampUTF8. reversed ( ) . starts ( with: " TMG " . utf8) {
191
+ let timezoneStart = timestampUTF8. index ( timestampUTF8. endIndex, offsetBy: - 3 )
192
+ let trimmedTimestampUTF8 = timestampUTF8 [ ..< timezoneStart] . trimmingASCIISpaces ( )
193
+ guard trimmedTimestampUTF8. endIndex != timezoneStart else {
194
+ return nil
195
+ }
196
+ timestampString = String ( utf8Slice: trimmedTimestampUTF8, in: header)
197
+ } else {
198
+ timestampString = String ( utf8Slice: timestampUTF8, in: header)
199
+ }
200
+ guard
201
+ var timeComponents = parseTimestamp ( timestampString, format: " %a, %d %b %Y %H:%M:%S " )
202
+ ?? parseTimestamp ( timestampString, format: " %a, %d-%b-%y %H:%M:%S " )
203
+ ?? parseTimestamp ( timestampString, format: " %a %b %d %H:%M:%S %Y " )
204
+ else {
205
+ return nil
173
206
}
207
+ let timestamp = Int64 ( timegm ( & timeComponents) )
208
+ return timestamp == - 1 && errno == EOVERFLOW ? nil : timestamp
174
209
}
0 commit comments