@@ -105,13 +105,209 @@ extension _HTTPURLProtocol._HTTPMessage {
105
105
struct _Version : RawRepresentable {
106
106
let rawValue : String
107
107
}
108
+ /// An authentication challenge parsed from `WWW-Authenticate` header field.
109
+ ///
110
+ /// Only parts necessary for Basic auth scheme are implemented at the moment.
111
+ /// - SeeAlso: https://tools.ietf.org/html/rfc7235#section-4.1
112
+ struct _Challenge {
113
+ static let AuthSchemeBasic = " basic "
114
+ static let AuthSchemeDigest = " digest "
115
+ /// A single auth challenge parameter
116
+ struct _AuthParameter {
117
+ let name : String
118
+ let value : String
119
+ }
120
+ let authScheme : String
121
+ let authParameters : [ _AuthParameter ]
122
+ }
108
123
}
109
124
extension _HTTPURLProtocol . _HTTPMessage . _Version {
110
125
init ? ( versionString: String ) {
111
126
rawValue = versionString
112
127
}
113
128
}
114
-
129
+ extension _HTTPURLProtocol . _HTTPMessage . _Challenge {
130
+ /// Case-insensitively searches for auth parameter with specified name
131
+ func parameter( withName name: String ) -> _AuthParameter ? {
132
+ return authParameters. first { $0. name. caseInsensitiveCompare ( name) == . orderedSame }
133
+ }
134
+ }
135
+ extension _HTTPURLProtocol . _HTTPMessage . _Challenge {
136
+ /// Creates authentication challenges from provided `HTTPURLResponse`.
137
+ ///
138
+ /// The value of `WWW-Authenticate` field is used for parsing authentication challenges
139
+ /// of supported type.
140
+ ///
141
+ /// - note: `Basic` is the only supported scheme at the moment.
142
+ /// - parameter response: A response to get header value from.
143
+ /// - returns: An array of supported challenges found in response.
144
+ /// # Reference
145
+ /// - [RFC 7235 - Hypertext Transfer Protocol (HTTP/1.1): Authentication](https://tools.ietf.org/html/rfc7235)
146
+ /// - [RFC 7617 - The 'Basic' HTTP Authentication Scheme](https://tools.ietf.org/html/rfc7617)
147
+ static func challenges( from response: HTTPURLResponse ) -> [ _HTTPURLProtocol . _HTTPMessage . _Challenge ] {
148
+ guard let authenticateValue = response. value ( forHTTPHeaderField: " WWW-Authenticate " ) else {
149
+ return [ ]
150
+ }
151
+ return challenges ( from: authenticateValue)
152
+ }
153
+ /// Creates authentication challenges from provided field value.
154
+ ///
155
+ /// Field value is expected to conform [RFC 7235 Section 4.1](https://tools.ietf.org/html/rfc7235#section-4.1)
156
+ /// as much as needed to define supported authorization schemes.
157
+ ///
158
+ /// - note: `Basic` is the only supported scheme at the moment.
159
+ /// - parameter authenticateFieldValue: A value of `WWW-Authenticate` field
160
+ /// - returns: array of supported challenges found.
161
+ /// # Reference
162
+ /// - [RFC 7235 - Hypertext Transfer Protocol (HTTP/1.1): Authentication](https://tools.ietf.org/html/rfc7235)
163
+ /// - [RFC 7617 - The 'Basic' HTTP Authentication Scheme](https://tools.ietf.org/html/rfc7617)
164
+ static func challenges( from authenticateFieldValue: String ) -> [ _HTTPURLProtocol . _HTTPMessage . _Challenge ] {
165
+ var challenges = [ _HTTPURLProtocol. _HTTPMessage. _Challenge] ( )
166
+
167
+ // Typical WWW-Authenticate header is something like
168
+ // WWWW-Authenticate: Digest realm="test", domain="/HTTP/Digest", nonce="e3d002b9b2080453fdacea2d89f2d102"
169
+ //
170
+ // https://tools.ietf.org/html/rfc7235#section-4.1
171
+ // WWW-Authenticate = 1#challenge
172
+ //
173
+ // https://tools.ietf.org/html/rfc7235#section-2.1
174
+ // challenge = auth-scheme [ 1*SP ( token68 / #auth-param ) ]
175
+ // auth-scheme = token
176
+ // auth-param = token BWS "=" BWS ( token / quoted-string )
177
+ // token68 = 1*( ALPHA / DIGIT /
178
+ // "-" / "." / "_" / "~" / "+" / "/" ) *"="
179
+ //
180
+ // https://tools.ietf.org/html/rfc7230#section-3.2.3
181
+ // OWS = *( SP / HTAB ) ; optional whitespace
182
+ // BWS = OWS ; "bad" whitespace
183
+ //
184
+ // https://tools.ietf.org/html/rfc7230#section-3.2.6
185
+ // token = 1*tchar
186
+ // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
187
+ // / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
188
+ // / DIGIT / ALPHA
189
+ // ; any VCHAR, except delimiters
190
+ // quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
191
+ // qdtext = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
192
+ // obs-text = %x80-FF
193
+ // quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
194
+ //
195
+ // https://tools.ietf.org/html/rfc5234#appendix-B.1
196
+ // ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
197
+ // SP = %x20
198
+ // HTAB = %x09 ; horizontal tab
199
+ // VCHAR = %x21-7E ; visible (printing) characters
200
+ // DQUOTE = %x22
201
+ // DIGIT = %x30-39 ; 0-9
202
+
203
+ var authenticateView = authenticateFieldValue. unicodeScalars [ ... ]
204
+ // Do an "eager" search of supported auth schemes. Same as it implemented in CURL.
205
+ // This means we will look after every comma on every step, no matter what was
206
+ // (or wasn't) parsed on previous step.
207
+ //
208
+ // WWW-Authenticate field could contain some sort of ambiguity, because, in general,
209
+ // it is a comma-separated list of comma-separated lists. As mentioned
210
+ // in https://tools.ietf.org/html/rfc7235#section-4.1, user agents are advised to
211
+ // take special care of parsing all challenges completely.
212
+ while !authenticateView. isEmpty {
213
+ guard let authSchemeRange = authenticateView. rangeOfTokenPrefix else {
214
+ break
215
+ }
216
+ let authScheme = String ( authenticateView [ authSchemeRange] )
217
+ if authScheme. caseInsensitiveCompare ( AuthSchemeBasic) == . orderedSame {
218
+ let authDataView = authenticateView [ authSchemeRange. upperBound... ]
219
+ let authParameters = _HTTPURLProtocol. _HTTPMessage. _Challenge. _AuthParameter. parameters ( from: authDataView)
220
+ let challenge = _HTTPURLProtocol. _HTTPMessage. _Challenge ( authScheme: authScheme, authParameters: authParameters)
221
+ // "realm" is the only mandatory parameter for Basic auth scheme. Otherwise consider parsed data invalid.
222
+ if challenge. parameter ( withName: " realm " ) != nil {
223
+ challenges. append ( challenge)
224
+ }
225
+ }
226
+ // read up to the next comma
227
+ guard let commaIndex = authenticateView. firstIndex ( of: _Delimiters. Comma) else {
228
+ break
229
+ }
230
+ // skip comma
231
+ authenticateView = authenticateView [ authenticateView. index ( after: commaIndex) ... ]
232
+ // consume spaces
233
+ authenticateView = authenticateView. trimSPPrefix
234
+ }
235
+ return challenges
236
+ }
237
+ }
238
+ private extension _HTTPURLProtocol . _HTTPMessage . _Challenge . _AuthParameter {
239
+ /// Reads authorization challenge parameters from provided Unicode Scalar view
240
+ static func parameters( from parametersView: String . UnicodeScalarView . SubSequence ) -> [ _HTTPURLProtocol . _HTTPMessage . _Challenge . _AuthParameter ] {
241
+ var parametersView = parametersView
242
+ var parameters = [ _HTTPURLProtocol. _HTTPMessage. _Challenge. _AuthParameter] ( )
243
+ while true {
244
+ parametersView = parametersView. trimSPPrefix
245
+ guard let parameter = parameter ( from: & parametersView) else {
246
+ break
247
+ }
248
+ parameters. append ( parameter)
249
+ // trim spaces and expect comma
250
+ parametersView = parametersView. trimSPPrefix
251
+ guard parametersView. first == _Delimiters. Comma else {
252
+ break
253
+ }
254
+ // drop comma
255
+ parametersView = parametersView. dropFirst ( )
256
+ }
257
+ return parameters
258
+ }
259
+ /// Reads a single challenge parameter from provided Unicode Scalar view
260
+ private static func parameter( from parametersView: inout String . UnicodeScalarView . SubSequence ) -> _HTTPURLProtocol . _HTTPMessage . _Challenge . _AuthParameter ? {
261
+ // Read parameter name. Return nil if name is not readable.
262
+ guard let parameterName = parameterName ( from: & parametersView) else {
263
+ return nil
264
+ }
265
+ // Trim BWS, expect '='
266
+ parametersView = parametersView. trimSPHTPrefix ?? parametersView
267
+ guard parametersView. first == _Delimiters. Equals else {
268
+ return nil
269
+ }
270
+ // Drop '='
271
+ parametersView = parametersView. dropFirst ( )
272
+ // Read parameter value. Return nil if parameter is not readable.
273
+ guard let parameterValue = parameterValue ( from: & parametersView) else {
274
+ return nil
275
+ }
276
+ return _HTTPURLProtocol. _HTTPMessage. _Challenge. _AuthParameter ( name: parameterName, value: parameterValue)
277
+ }
278
+ /// Reads a challenge parameter name from provided Unicode Scalar view
279
+ private static func parameterName( from nameView: inout String . UnicodeScalarView . SubSequence ) -> String ? {
280
+ guard let nameRange = nameView. rangeOfTokenPrefix else {
281
+ return nil
282
+ }
283
+
284
+ let name = String ( nameView [ nameRange] )
285
+ nameView = nameView [ nameRange. upperBound... ]
286
+ return name
287
+ }
288
+ /// Reads a challenge parameter value from provided Unicode Scalar view
289
+ private static func parameterValue( from valueView: inout String . UnicodeScalarView . SubSequence ) -> String ? {
290
+ // Trim BWS
291
+ valueView = valueView. trimSPHTPrefix ?? valueView
292
+ if valueView. first == _Delimiters. DoubleQuote {
293
+ // quoted-string
294
+ if let valueRange = valueView. rangeOfQuotedStringPrefix {
295
+ let value = valueView [ valueRange] . dequotedString ( )
296
+ valueView = valueView [ valueRange. upperBound... ]
297
+ return value
298
+ }
299
+ }
300
+ else {
301
+ // token
302
+ if let valueRange = valueView. rangeOfTokenPrefix {
303
+ let value = String ( valueView [ valueRange] )
304
+ valueView = valueView [ valueRange. upperBound... ]
305
+ return value
306
+ }
307
+ }
308
+ return nil
309
+ }
310
+ }
115
311
private extension _HTTPURLProtocol . _HTTPMessage . _StartLine {
116
312
init ? ( line: String ) {
117
313
guard let r = line. splitRequestLine ( ) else { return nil }
@@ -236,6 +432,7 @@ private extension Collection {
236
432
private extension String . UnicodeScalarView . SubSequence {
237
433
/// The range of *Token* characters as specified by RFC 2616.
238
434
var rangeOfTokenPrefix : Range < Index > ? {
435
+ guard !isEmpty else { return nil }
239
436
var end = startIndex
240
437
while self [ end] . isValidMessageToken {
241
438
end = self . index ( after: end)
@@ -269,6 +466,101 @@ private extension String.UnicodeScalarView.SubSequence {
269
466
}
270
467
return nil
271
468
}
469
+ var trimSPPrefix : SubSequence {
470
+ var idx = startIndex
471
+ while idx < endIndex {
472
+ if self [ idx] == _Delimiters. Space {
473
+ idx = self . index ( after: idx)
474
+ } else {
475
+ return self [ idx..< endIndex]
476
+ }
477
+ }
478
+ return self
479
+ }
480
+ /// Returns range of **quoted-string** starting from first index of sequence.
481
+ ///
482
+ /// - returns: range of **quoted-string** or `nil` if value can not be parsed.
483
+ var rangeOfQuotedStringPrefix : Range < Index > ? {
484
+ // quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
485
+ // qdtext = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
486
+ // obs-text = %x80-FF
487
+ // quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
488
+ guard !isEmpty else {
489
+ return nil
490
+ }
491
+ var idx = startIndex
492
+ // Expect and consume dquote
493
+ guard self [ idx] == _Delimiters. DoubleQuote else {
494
+ return nil
495
+ }
496
+ idx = self . index ( after: idx)
497
+ var isQuotedPair = false
498
+ while idx < endIndex {
499
+ let currentScalar = self [ idx]
500
+ if currentScalar == _Delimiters. Backslash && !isQuotedPair {
501
+ isQuotedPair = true
502
+ } else if isQuotedPair {
503
+ guard currentScalar. isQuotedPairEscapee else {
504
+ return nil
505
+ }
506
+ isQuotedPair = false
507
+ } else if currentScalar == _Delimiters. DoubleQuote {
508
+ break
509
+ } else {
510
+ guard currentScalar. isQdtext else {
511
+ return nil
512
+ }
513
+ }
514
+ idx = self . index ( after: idx)
515
+ }
516
+ // Expect stop on dquote
517
+ guard idx < endIndex, self [ idx] == _Delimiters. DoubleQuote else {
518
+ return nil
519
+ }
520
+ return startIndex..< self . index ( after: idx)
521
+ }
522
+ /// Returns dequoted string if receiver contains **quoted-string**
523
+ ///
524
+ /// - returns: dequoted string or `nil` if receiver does not contain valid quoted string
525
+ func dequotedString( ) -> String ? {
526
+ guard !isEmpty else {
527
+ return nil
528
+ }
529
+ var resultView = String . UnicodeScalarView ( )
530
+ resultView. reserveCapacity ( self . count)
531
+ var idx = startIndex
532
+ // Expect and consume dquote
533
+ guard self [ idx] == _Delimiters. DoubleQuote else {
534
+ return nil
535
+ }
536
+ idx = self . index ( after: idx)
537
+ var isQuotedPair = false
538
+ while idx < endIndex {
539
+ let currentScalar = self [ idx]
540
+ if currentScalar == _Delimiters. Backslash && !isQuotedPair {
541
+ isQuotedPair = true
542
+ } else if isQuotedPair {
543
+ guard currentScalar. isQuotedPairEscapee else {
544
+ return nil
545
+ }
546
+ isQuotedPair = false
547
+ resultView. append ( currentScalar)
548
+ } else if currentScalar == _Delimiters. DoubleQuote {
549
+ break
550
+ } else {
551
+ guard currentScalar. isQdtext else {
552
+ return nil
553
+ }
554
+ resultView. append ( currentScalar)
555
+ }
556
+ idx = self . index ( after: idx)
557
+ }
558
+ // Expect stop on dquote
559
+ guard idx < endIndex, self [ idx] == _Delimiters. DoubleQuote else {
560
+ return nil
561
+ }
562
+ return String ( resultView)
563
+ }
272
564
}
273
565
private extension UnicodeScalar {
274
566
/// Is this a valid **token** as defined by RFC 2616 ?
@@ -278,4 +570,32 @@ private extension UnicodeScalar {
278
570
guard UnicodeScalar ( 32 ) <= self && self <= UnicodeScalar ( 126 ) else { return false }
279
571
return !_Delimiters. Separators. characterIsMember ( UInt16 ( self . value) )
280
572
}
573
+ /// Is this a valid **qdtext** character
574
+ ///
575
+ /// - SeeAlso: https://tools.ietf.org/html/rfc7230#section-3.2.6
576
+ var isQdtext : Bool {
577
+ // qdtext = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
578
+ // obs-text = %x80-FF
579
+ let value = self . value
580
+ return self == _Delimiters. HorizontalTab
581
+ || self == _Delimiters. Space
582
+ || value == 0x21
583
+ || 0x23 <= value && value <= 0x5B
584
+ || 0x5D <= value && value <= 0x7E
585
+ || 0x80 <= value && value <= 0xFF
586
+
587
+ }
588
+ /// Is this a valid second octet of **quoted-pair**
589
+ ///
590
+ /// - SeeAlso: https://tools.ietf.org/html/rfc7230#section-3.2.6
591
+ /// - SeeAlso: https://tools.ietf.org/html/rfc5234#appendix-B.1
592
+ var isQuotedPairEscapee : Bool {
593
+ // quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
594
+ // obs-text = %x80-FF
595
+ let value = self . value
596
+ return self == _Delimiters. HorizontalTab
597
+ || self == _Delimiters. Space
598
+ || 0x21 <= value && value <= 0x7E
599
+ || 0x80 <= value && value <= 0xFF
600
+ }
281
601
}
0 commit comments