Skip to content

Commit 15855f8

Browse files
authored
Merge pull request #2756 from spevans/pr_max_http_redirects
URLSession: Add a limit on the number of HTTP Redirects that are followed
2 parents f819681 + c26fe15 commit 15855f8

File tree

2 files changed

+57
-3
lines changed

2 files changed

+57
-3
lines changed

Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ internal class _HTTPURLProtocol: _NativeProtocol {
2525
// and the 3xx response is returned to the client and the data is sent via the delegate notify
2626
// mechanism. `lastRedirectBody` holds the body of the redirect currently being processed.
2727
var lastRedirectBody: Data? = nil
28+
private var redirectCount = 0
2829

2930
public required init(task: URLSessionTask, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) {
3031
super.init(task: task, cachedResponse: cachedResponse, client: client)
@@ -440,13 +441,24 @@ internal class _HTTPURLProtocol: _NativeProtocol {
440441
}
441442

442443
override func redirectFor(request: URLRequest) {
443-
//TODO: Should keep track of the number of redirects that this
444-
// request has gone through and err out once it's too large, i.e.
445-
// call into `failWith(errorCode: )` with NSURLErrorHTTPTooManyRedirects
446444
guard case .transferCompleted(response: let response, bodyDataDrain: let bodyDataDrain) = self.internalState else {
447445
fatalError("Trying to redirect, but the transfer is not complete.")
448446
}
449447

448+
// Avoid a never ending redirect chain by having a hard limit on the number of redirects.
449+
// This value mirrors Darwin.
450+
redirectCount += 1
451+
if redirectCount > 16 {
452+
self.internalState = .transferFailed
453+
let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorHTTPTooManyRedirects,
454+
userInfo: [NSLocalizedDescriptionKey: "too many HTTP redirects"])
455+
guard let request = task?.currentRequest else {
456+
fatalError("In a redirect chain but no current task/request")
457+
}
458+
failWith(error: error, request: request)
459+
return
460+
}
461+
450462
guard let session = task?.session as? URLSession else { fatalError() }
451463
switch session.behaviour(for: task!) {
452464
case .taskDelegate(let delegate):

Tests/Foundation/Tests/TestURLSession.swift

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,47 @@ class TestURLSession: LoopbackServerTest {
798798
}
799799
}
800800

801+
func test_httpRedirectionExceededMaxRedirects() throws {
802+
let expectedMaxRedirects = 16
803+
let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/redirect/99"
804+
let url = try XCTUnwrap(URL(string: urlString))
805+
let exceededCountUrlString = "http://127.0.0.1:\(TestURLSession.serverPort)/redirect/\(99 - expectedMaxRedirects)"
806+
let exceededCountUrl = try XCTUnwrap(URL(string: exceededCountUrlString))
807+
808+
for method in httpMethods {
809+
var request = URLRequest(url: url)
810+
request.httpMethod = method
811+
let delegate = SessionDelegate(with: expectation(description: "\(method) \(urlString): with HTTP redirection"))
812+
813+
var redirectRequests: [(HTTPURLResponse, URLRequest)] = []
814+
delegate.redirectionHandler = { (response: HTTPURLResponse, request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) in
815+
redirectRequests.append((response, request))
816+
completionHandler(request)
817+
}
818+
delegate.run(with: request, timeoutInterval: 5)
819+
waitForExpectations(timeout: 20)
820+
821+
XCTAssertNil(delegate.response)
822+
XCTAssertNil(delegate.receivedData)
823+
824+
XCTAssertNotNil(delegate.error)
825+
let error = delegate.error as? URLError
826+
XCTAssertEqual(error?.code.rawValue, NSURLErrorHTTPTooManyRedirects)
827+
XCTAssertEqual(error?.localizedDescription, "too many HTTP redirects")
828+
let userInfo = error?.userInfo as? [String: Any]
829+
let errorURL = userInfo?[NSURLErrorFailingURLErrorKey] as? URL
830+
XCTAssertEqual(errorURL, exceededCountUrl)
831+
832+
// Check the last Redirection response/request received.
833+
XCTAssertEqual(redirectRequests.count, expectedMaxRedirects)
834+
let lastResponse = redirectRequests.last?.0
835+
let lastRequest = redirectRequests.last?.1
836+
837+
XCTAssertEqual(lastResponse?.statusCode, 302)
838+
XCTAssertEqual(lastRequest?.url, exceededCountUrl)
839+
}
840+
}
841+
801842
func test_httpNotFound() throws {
802843
let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/404"
803844
let url = try XCTUnwrap(URL(string: urlString))
@@ -1691,6 +1732,7 @@ class TestURLSession: LoopbackServerTest {
16911732
("test_httpRedirectionWithDefaultPort", test_httpRedirectionWithDefaultPort),
16921733
("test_httpRedirectionTimeout", test_httpRedirectionTimeout),
16931734
("test_httpRedirectionChainInheritsTimeoutInterval", test_httpRedirectionChainInheritsTimeoutInterval),
1735+
("test_httpRedirectionExceededMaxRedirects", test_httpRedirectionExceededMaxRedirects),
16941736
("test_httpNotFound", test_httpNotFound),
16951737
("test_http0_9SimpleResponses", test_http0_9SimpleResponses),
16961738
("test_outOfRangeButCorrectlyFormattedHTTPCode", test_outOfRangeButCorrectlyFormattedHTTPCode),

0 commit comments

Comments
 (0)