@@ -227,15 +227,20 @@ public class HTTPClient {
227
227
channelEL: EventLoop ? = nil ,
228
228
deadline: NIODeadline ? = nil ) -> Task < Delegate . Response > {
229
229
let redirectHandler : RedirectHandler < Delegate . Response > ?
230
- if self . configuration. followRedirects {
230
+ switch self . configuration. redirectConfiguration. configuration {
231
+ case . follow( let max, let allowCycles) :
232
+ var request = request
233
+ if request. redirectState == nil {
234
+ request. redirectState = . init( count: max, visited: allowCycles ? nil : Set ( ) )
235
+ }
231
236
redirectHandler = RedirectHandler < Delegate . Response > ( request: request) { newRequest in
232
237
self . execute ( request: newRequest,
233
238
delegate: delegate,
234
239
eventLoop: delegateEL,
235
240
channelEL: channelEL,
236
241
deadline: deadline)
237
242
}
238
- } else {
243
+ case . disallow :
239
244
redirectHandler = nil
240
245
}
241
246
@@ -325,7 +330,7 @@ public class HTTPClient {
325
330
/// - `305: Use Proxy`
326
331
/// - `307: Temporary Redirect`
327
332
/// - `308: Permanent Redirect`
328
- public var followRedirects : Bool
333
+ public var redirectConfiguration : RedirectConfiguration
329
334
/// Default client timeout, defaults to no timeouts.
330
335
public var timeout : Timeout
331
336
/// Upstream proxy, defaults to no proxy.
@@ -336,27 +341,27 @@ public class HTTPClient {
336
341
public var ignoreUncleanSSLShutdown : Bool
337
342
338
343
public init ( tlsConfiguration: TLSConfiguration ? = nil ,
339
- followRedirects : Bool = false ,
344
+ redirectConfiguration : RedirectConfiguration ? = nil ,
340
345
timeout: Timeout = Timeout ( ) ,
341
346
proxy: Proxy ? = nil ,
342
347
ignoreUncleanSSLShutdown: Bool = false ,
343
348
decompression: Decompression = . disabled) {
344
349
self . tlsConfiguration = tlsConfiguration
345
- self . followRedirects = followRedirects
350
+ self . redirectConfiguration = redirectConfiguration ?? RedirectConfiguration ( )
346
351
self . timeout = timeout
347
352
self . proxy = proxy
348
353
self . ignoreUncleanSSLShutdown = ignoreUncleanSSLShutdown
349
354
self . decompression = decompression
350
355
}
351
356
352
357
public init ( certificateVerification: CertificateVerification ,
353
- followRedirects : Bool = false ,
358
+ redirectConfiguration : RedirectConfiguration ? = nil ,
354
359
timeout: Timeout = Timeout ( ) ,
355
360
proxy: Proxy ? = nil ,
356
361
ignoreUncleanSSLShutdown: Bool = false ,
357
362
decompression: Decompression = . disabled) {
358
363
self . tlsConfiguration = TLSConfiguration . forClient ( certificateVerification: certificateVerification)
359
- self . followRedirects = followRedirects
364
+ self . redirectConfiguration = redirectConfiguration ?? RedirectConfiguration ( )
360
365
self . timeout = timeout
361
366
self . proxy = proxy
362
367
self . ignoreUncleanSSLShutdown = ignoreUncleanSSLShutdown
@@ -439,6 +444,38 @@ extension HTTPClient.Configuration {
439
444
self . read = read
440
445
}
441
446
}
447
+
448
+ /// Specifies redirect processing settings.
449
+ public struct RedirectConfiguration {
450
+ enum Configuration {
451
+ /// Redirects are not followed.
452
+ case disallow
453
+ /// Redirects are followed with a specified limit.
454
+ case follow( max: Int , allowCycles: Bool )
455
+ }
456
+
457
+ var configuration : Configuration
458
+
459
+ init ( ) {
460
+ self . configuration = . follow( max: 5 , allowCycles: false )
461
+ }
462
+
463
+ init ( configuration: Configuration ) {
464
+ self . configuration = configuration
465
+ }
466
+
467
+ /// Redirects are not followed.
468
+ public static let disallow = RedirectConfiguration ( configuration: . disallow)
469
+
470
+ /// Redirects are followed with a specified limit.
471
+ ///
472
+ /// - parameters:
473
+ /// - max: The maximum number of allowed redirects.
474
+ /// - allowCycles: Whether cycles are allowed.
475
+ ///
476
+ /// - warning: Cycle detection will keep all visited URLs in memory which means a malicious server could use this as a denial-of-service vector.
477
+ public static func follow( max: Int , allowCycles: Bool ) -> RedirectConfiguration { return . init( configuration: . follow( max: max, allowCycles: allowCycles) ) }
478
+ }
442
479
}
443
480
444
481
private extension ChannelPipeline {
@@ -488,6 +525,8 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
488
525
case invalidProxyResponse
489
526
case contentLengthMissing
490
527
case proxyAuthenticationRequired
528
+ case redirectLimitReached
529
+ case redirectCycleDetected
491
530
}
492
531
493
532
private var code : Code
@@ -526,4 +565,8 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
526
565
public static let contentLengthMissing = HTTPClientError ( code: . contentLengthMissing)
527
566
/// Proxy Authentication Required.
528
567
public static let proxyAuthenticationRequired = HTTPClientError ( code: . proxyAuthenticationRequired)
568
+ /// Redirect Limit reached.
569
+ public static let redirectLimitReached = HTTPClientError ( code: . redirectLimitReached)
570
+ /// Redirect Cycle detected.
571
+ public static let redirectCycleDetected = HTTPClientError ( code: . redirectCycleDetected)
529
572
}
0 commit comments