-
Notifications
You must be signed in to change notification settings - Fork 125
add support for redirect limits #113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
939aa2b
fca5971
b437111
99096c5
b3ffde6
bbad11f
8b0e613
aa843de
7e2edf8
5209dc3
cf2d4d6
86cf7d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -226,15 +226,20 @@ public class HTTPClient { | |
channelEL: EventLoop? = nil, | ||
deadline: NIODeadline? = nil) -> Task<Delegate.Response> { | ||
let redirectHandler: RedirectHandler<Delegate.Response>? | ||
if self.configuration.followRedirects { | ||
switch self.configuration.redirects { | ||
case .allow(let max, let allowCycles): | ||
var request = request | ||
if request.redirectState == nil { | ||
request.redirectState = .init(count: max, visited: allowCycles ? nil : Set()) | ||
} | ||
redirectHandler = RedirectHandler<Delegate.Response>(request: request) { newRequest in | ||
self.execute(request: newRequest, | ||
delegate: delegate, | ||
eventLoop: delegateEL, | ||
channelEL: channelEL, | ||
deadline: deadline) | ||
} | ||
} else { | ||
case .disallow: | ||
redirectHandler = nil | ||
} | ||
|
||
|
@@ -317,33 +322,33 @@ public class HTTPClient { | |
/// - `305: Use Proxy` | ||
/// - `307: Temporary Redirect` | ||
/// - `308: Permanent Redirect` | ||
public var followRedirects: Bool | ||
public var redirects: RedirectPolicy | ||
/// Default client timeout, defaults to no timeouts. | ||
public var timeout: Timeout | ||
/// Upstream proxy, defaults to no proxy. | ||
public var proxy: Proxy? | ||
/// Ignore TLS unclean shutdown error, defaults to `false`. | ||
public var ignoreUncleanSSLShutdown: Bool | ||
|
||
public init(tlsConfiguration: TLSConfiguration? = nil, followRedirects: Bool = false, timeout: Timeout = Timeout(), proxy: Proxy? = nil) { | ||
self.init(tlsConfiguration: tlsConfiguration, followRedirects: followRedirects, timeout: timeout, proxy: proxy, ignoreUncleanSSLShutdown: false) | ||
public init(tlsConfiguration: TLSConfiguration? = nil, redirects: RedirectPolicy = .allow(max: 5, allowCycles: false), timeout: Timeout = Timeout(), proxy: Proxy? = nil) { | ||
self.init(tlsConfiguration: tlsConfiguration, redirects: redirects, timeout: timeout, proxy: proxy, ignoreUncleanSSLShutdown: false) | ||
} | ||
|
||
public init(tlsConfiguration: TLSConfiguration? = nil, followRedirects: Bool = false, timeout: Timeout = Timeout(), proxy: Proxy? = nil, ignoreUncleanSSLShutdown: Bool = false) { | ||
public init(tlsConfiguration: TLSConfiguration? = nil, redirects: RedirectPolicy = .allow(max: 5, allowCycles: false), timeout: Timeout = Timeout(), proxy: Proxy? = nil, ignoreUncleanSSLShutdown: Bool = false) { | ||
self.tlsConfiguration = tlsConfiguration | ||
self.followRedirects = followRedirects | ||
self.redirects = redirects | ||
self.timeout = timeout | ||
self.proxy = proxy | ||
self.ignoreUncleanSSLShutdown = ignoreUncleanSSLShutdown | ||
} | ||
|
||
public init(certificateVerification: CertificateVerification, followRedirects: Bool = false, timeout: Timeout = Timeout(), proxy: Proxy? = nil) { | ||
self.init(certificateVerification: certificateVerification, followRedirects: followRedirects, timeout: timeout, proxy: proxy, ignoreUncleanSSLShutdown: false) | ||
public init(certificateVerification: CertificateVerification, redirects: RedirectPolicy = .allow(max: 5, allowCycles: false), timeout: Timeout = Timeout(), proxy: Proxy? = nil) { | ||
self.init(certificateVerification: certificateVerification, redirects: redirects, timeout: timeout, proxy: proxy, ignoreUncleanSSLShutdown: false) | ||
} | ||
|
||
public init(certificateVerification: CertificateVerification, followRedirects: Bool = false, timeout: Timeout = Timeout(), proxy: Proxy? = nil, ignoreUncleanSSLShutdown: Bool = false) { | ||
public init(certificateVerification: CertificateVerification, redirects: RedirectPolicy = .allow(max: 5, allowCycles: false), timeout: Timeout = Timeout(), proxy: Proxy? = nil, ignoreUncleanSSLShutdown: Bool = false) { | ||
self.tlsConfiguration = TLSConfiguration.forClient(certificateVerification: certificateVerification) | ||
self.followRedirects = followRedirects | ||
self.redirects = redirects | ||
self.timeout = timeout | ||
self.proxy = proxy | ||
self.ignoreUncleanSSLShutdown = ignoreUncleanSSLShutdown | ||
|
@@ -423,6 +428,14 @@ extension HTTPClient.Configuration { | |
self.read = read | ||
} | ||
} | ||
|
||
/// Specifies redirect processing settings. | ||
public enum RedirectPolicy { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks! What do people think about calling this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 from me, @tomerd wdyt? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done!
artemredkin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// Redirects are not followed. | ||
case disallow | ||
/// Redirects are followed with a specified limit. Cycle detection reqiures that all visited URL's are kept in memory. | ||
artemredkin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
case allow(max: Int, allowCycles: Bool) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: "disable" and "enable" seem more natural than "disallow" and "allow"? since this is a feature flag not a security policy? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this struct was renamed to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the tests it reads as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like |
||
} | ||
} | ||
|
||
private extension ChannelPipeline { | ||
|
@@ -472,6 +485,8 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible { | |
case invalidProxyResponse | ||
case contentLengthMissing | ||
case proxyAuthenticationRequired | ||
case redirectLimitReached | ||
ianpartridge marked this conversation as resolved.
Show resolved
Hide resolved
|
||
case redirectCycleDetected | ||
} | ||
|
||
private var code: Code | ||
|
@@ -508,6 +523,10 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible { | |
public static let invalidProxyResponse = HTTPClientError(code: .invalidProxyResponse) | ||
/// Request does not contain `Content-Length` header. | ||
public static let contentLengthMissing = HTTPClientError(code: .contentLengthMissing) | ||
/// Proxy Authentication Required | ||
/// Proxy Authentication Required. | ||
public static let proxyAuthenticationRequired = HTTPClientError(code: .proxyAuthenticationRequired) | ||
/// Redirect Limit reached. | ||
public static let redirectLimitReached = HTTPClientError(code: .redirectLimitReached) | ||
/// Redirect Cycle detected. | ||
public static let redirectCycleDetected = HTTPClientError(code: .redirectCycleDetected) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -130,7 +130,7 @@ class HTTPClientTests: XCTestCase { | |
let httpBin = HTTPBin(ssl: false) | ||
let httpsBin = HTTPBin(ssl: true) | ||
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, | ||
configuration: HTTPClient.Configuration(certificateVerification: .none, followRedirects: true)) | ||
configuration: HTTPClient.Configuration(certificateVerification: .none, redirects: .allow(max: 10, allowCycles: true))) | ||
|
||
defer { | ||
XCTAssertNoThrow(try httpClient.syncShutdown()) | ||
|
@@ -148,7 +148,7 @@ class HTTPClientTests: XCTestCase { | |
func testHttpHostRedirect() throws { | ||
let httpBin = HTTPBin(ssl: false) | ||
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, | ||
configuration: HTTPClient.Configuration(certificateVerification: .none, followRedirects: true)) | ||
configuration: HTTPClient.Configuration(certificateVerification: .none, redirects: .allow(max: 10, allowCycles: true))) | ||
|
||
defer { | ||
XCTAssertNoThrow(try httpClient.syncShutdown()) | ||
|
@@ -525,7 +525,7 @@ class HTTPClientTests: XCTestCase { | |
let httpBin = HTTPBin() | ||
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 5) | ||
let httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoopGroup), | ||
configuration: HTTPClient.Configuration(followRedirects: true)) | ||
configuration: HTTPClient.Configuration(redirects: .allow(max: 10, allowCycles: true))) | ||
defer { | ||
XCTAssertNoThrow(try httpClient.syncShutdown()) | ||
XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) | ||
|
@@ -563,4 +563,32 @@ class HTTPClientTests: XCTestCase { | |
response = try httpClient.execute(request: request, delegate: delegate, eventLoop: .delegate(on: eventLoop)).wait() | ||
XCTAssertEqual(true, response) | ||
} | ||
|
||
func testLoopDetectionRedirectLimit() throws { | ||
let httpBin = HTTPBin(ssl: true) | ||
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, | ||
configuration: HTTPClient.Configuration(certificateVerification: .none, redirects: .allow(max: 5, allowCycles: false))) | ||
defer { | ||
XCTAssertNoThrow(try httpClient.syncShutdown()) | ||
XCTAssertNoThrow(try httpBin.shutdown()) | ||
} | ||
|
||
XCTAssertThrowsError(try httpClient.get(url: "https://localhost:\(httpBin.port)/redirect/infinite1").wait(), "Should fail with redirect limit") { error in | ||
XCTAssertEqual(error as! HTTPClientError, HTTPClientError.redirectCycleDetected) | ||
} | ||
} | ||
|
||
func testCountRedirectLimit() throws { | ||
let httpBin = HTTPBin(ssl: true) | ||
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew, | ||
configuration: HTTPClient.Configuration(certificateVerification: .none, redirects: .allow(max: 5, allowCycles: true))) | ||
defer { | ||
XCTAssertNoThrow(try httpClient.syncShutdown()) | ||
XCTAssertNoThrow(try httpBin.shutdown()) | ||
} | ||
|
||
XCTAssertThrowsError(try httpClient.get(url: "https://localhost:\(httpBin.port)/redirect/infinite1").wait(), "Should fail with redirect limit") { error in | ||
XCTAssertEqual(error as! HTTPClientError, HTTPClientError.redirectLimitReached) | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we want to test case insensitive loops? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do you mean when schema and path are capitalised differently? |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's quite a lot of code duplication here, with each initializer having the same hard-coded
.allow(max: 5, allowCycles: false)
default value. We should pull that out into a separatethen this can be:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
then
defaultRedirectPolicy
would also have to be public, what if it will be optional instead?