Skip to content

Commit 54286a8

Browse files
authored
fix: DownloadTaskManager handle redirect authentication (#8947)
1 parent b409d13 commit 54286a8

File tree

2 files changed

+116
-2
lines changed

2 files changed

+116
-2
lines changed

Sources/Basics/HTTPClient/URLSessionHTTPClient.swift

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ final class URLSessionHTTPClient: Sendable {
7575
self.downloadTaskManager.register(
7676
task: downloadTask,
7777
urlRequest: urlRequest,
78+
authorizationProvider: request.options.authorizationProvider,
7879
// FIXME: always using synchronous filesystem, because `URLSessionDownloadDelegate`
7980
// needs temporary files to moved out of temporary locations synchronously in delegate callbacks.
8081
fileSystem: localFileSystem,
@@ -112,6 +113,7 @@ final class URLSessionHTTPClient: Sendable {
112113
self.downloadTaskManager.register(
113114
task: downloadTask,
114115
urlRequest: urlRequest,
116+
authorizationProvider: request.options.authorizationProvider,
115117
fileSystem: fileSystem,
116118
destination: destination,
117119
progress: progress,
@@ -255,6 +257,7 @@ private final class DownloadTaskManager: NSObject, URLSessionDownloadDelegate {
255257
func register(
256258
task: URLSessionDownloadTask,
257259
urlRequest: URLRequest,
260+
authorizationProvider: LegacyHTTPClientConfiguration.AuthorizationProvider?,
258261
fileSystem: FileSystem,
259262
destination: AbsolutePath,
260263
progress: LegacyHTTPClient.ProgressHandler?,
@@ -266,7 +269,8 @@ private final class DownloadTaskManager: NSObject, URLSessionDownloadDelegate {
266269
destination: destination,
267270
downloadTaskManager: self,
268271
progressHandler: progress,
269-
completionHandler: completion
272+
completionHandler: completion,
273+
authorizationProvider: authorizationProvider
270274
)
271275
}
272276

@@ -337,12 +341,35 @@ private final class DownloadTaskManager: NSObject, URLSessionDownloadDelegate {
337341
}
338342
}
339343

344+
public func urlSession(
345+
_ session: URLSession,
346+
task: URLSessionTask,
347+
willPerformHTTPRedirection response: HTTPURLResponse,
348+
newRequest request: URLRequest,
349+
completionHandler: @escaping (URLRequest?) -> Void
350+
) {
351+
guard let task = self.tasks[task.taskIdentifier] else {
352+
return
353+
}
354+
355+
// Add new authorization header for a redirect if there is one, otherwise remove
356+
var redirectRequest = request
357+
if let redirectURL = request.url, let authorization = task.authorizationProvider?(redirectURL) {
358+
redirectRequest.setValue(authorization, forHTTPHeaderField: "Authorization")
359+
} else {
360+
redirectRequest.setValue(nil, forHTTPHeaderField: "Authorization")
361+
}
362+
363+
completionHandler(redirectRequest)
364+
}
365+
340366
struct DownloadTask: Sendable {
341367
let task: URLSessionDownloadTask
342368
let fileSystem: FileSystem
343369
let destination: AbsolutePath
344370
let progressHandler: LegacyHTTPClient.ProgressHandler?
345371
let completionHandler: LegacyHTTPClient.CompletionHandler
372+
let authorizationProvider: LegacyHTTPClientConfiguration.AuthorizationProvider?
346373

347374
var moveFileError: Error?
348375

@@ -352,13 +379,15 @@ private final class DownloadTaskManager: NSObject, URLSessionDownloadDelegate {
352379
destination: AbsolutePath,
353380
downloadTaskManager: DownloadTaskManager,
354381
progressHandler: LegacyHTTPClient.ProgressHandler?,
355-
completionHandler: @escaping LegacyHTTPClient.CompletionHandler
382+
completionHandler: @escaping LegacyHTTPClient.CompletionHandler,
383+
authorizationProvider: LegacyHTTPClientConfiguration.AuthorizationProvider?
356384
) {
357385
self.task = task
358386
self.fileSystem = fileSystem
359387
self.destination = destination
360388
self.progressHandler = progressHandler
361389
self.completionHandler = completionHandler
390+
self.authorizationProvider = authorizationProvider
362391
}
363392
}
364393
}

Tests/BasicsTests/URLSessionHTTPClientTests.swift

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,78 @@ final class URLSessionHTTPClientTest: XCTestCase {
805805
}
806806
}
807807

808+
func testAsyncDownloadAuthenticateWithRedirectedSuccess() async throws {
809+
#if !os(macOS)
810+
// URLSession Download tests can only run on macOS
811+
// as re-libs-foundation's URLSessionTask implementation which expects the temporaryFileURL property to be on the request.
812+
// and there is no way to set it in a mock
813+
// https://github.com/apple/swift-corelibs-foundation/pull/2593 tries to address the latter part
814+
try XCTSkipIf(true, "test is only supported on macOS")
815+
#endif
816+
let netrcContent = "machine async-protected.downloader-tests.com login anonymous password qwerty"
817+
let netrc = try NetrcAuthorizationWrapper(underlying: NetrcParser.parse(netrcContent))
818+
let authData = Data("anonymous:qwerty".utf8)
819+
let testAuthHeader = "Basic \(authData.base64EncodedString())"
820+
821+
let configuration = URLSessionConfiguration.default
822+
configuration.protocolClasses = [MockURLProtocol.self]
823+
let urlSession = URLSessionHTTPClient(configuration: configuration)
824+
let httpClient = HTTPClient(implementation: urlSession.execute)
825+
826+
try await testWithTemporaryDirectory { temporaryDirectory in
827+
let url = URL("https://async-protected.downloader-tests.com/testBasics.zip")
828+
let redirectURL = URL("https://cdn-async.downloader-tests.com/testBasics.zip")
829+
let destination = temporaryDirectory.appending("download")
830+
var options = HTTPClientRequest.Options()
831+
options.authorizationProvider = netrc.httpAuthorizationHeader(for:)
832+
let request = HTTPClient.Request.download(
833+
url: url,
834+
options: options,
835+
fileSystem: localFileSystem,
836+
destination: destination
837+
)
838+
let redirectRequest = HTTPClient.Request.download(
839+
url: redirectURL,
840+
options: options,
841+
fileSystem: localFileSystem,
842+
destination: destination
843+
)
844+
845+
MockURLProtocol.onRequest(request) { request in
846+
XCTAssertEqual(request.allHTTPHeaderFields?["Authorization"], testAuthHeader)
847+
MockURLProtocol.sendResponse(statusCode: 302, headers: ["Location": redirectURL.absoluteString], for: request)
848+
MockURLProtocol.sendRedirect(for: request, to: URLRequest(url: redirectURL))
849+
}
850+
MockURLProtocol.onRequest(redirectRequest) { request in
851+
XCTAssertEqual(request.allHTTPHeaderFields?["Authorization"], nil)
852+
MockURLProtocol.sendResponse(statusCode: 200, headers: ["Content-Length": "1024"], for: request)
853+
MockURLProtocol.sendData(Data(repeating: 0xBE, count: 512), for: request)
854+
MockURLProtocol.sendData(Data(repeating: 0xEF, count: 512), for: request)
855+
MockURLProtocol.sendCompletion(for: request)
856+
}
857+
858+
let response = try await httpClient.execute(
859+
request,
860+
progress: { bytesDownloaded, totalBytesToDownload in
861+
switch (bytesDownloaded, totalBytesToDownload) {
862+
case (512, 1024):
863+
break
864+
case (1024, 1024):
865+
break
866+
default:
867+
XCTFail("unexpected progress")
868+
}
869+
}
870+
)
871+
872+
XCTAssertEqual(response.statusCode, 200)
873+
XCTAssertFileExists(destination)
874+
875+
let bytes = ByteString(Array(repeating: 0xBE, count: 512) + Array(repeating: 0xEF, count: 512))
876+
XCTAssertEqual(try! localFileSystem.readFileContents(destination), bytes)
877+
}
878+
}
879+
808880
func testAsyncDownloadDefaultAuthenticationSuccess() async throws {
809881
#if !os(macOS)
810882
// URLSession Download tests can only run on macOS
@@ -1055,6 +1127,19 @@ private class MockURLProtocol: URLProtocol {
10551127
request.client?.urlProtocol(request, didFailWithError: error)
10561128
}
10571129

1130+
static func sendRedirect(for request: URLRequest, to newRequest: URLRequest) {
1131+
let key = Key(request.httpMethod!, request.url!)
1132+
self.sendRedirect(newRequest: newRequest, for: key)
1133+
}
1134+
1135+
private static func sendRedirect(newRequest: URLRequest, for key: Key) {
1136+
guard let request = self.requests[key] else {
1137+
return XCTFail("url did not start loading")
1138+
}
1139+
let response = HTTPURLResponse(url: key.url, statusCode: 302, httpVersion: "1.1", headerFields: nil)!
1140+
request.client?.urlProtocol(request, wasRedirectedTo: newRequest, redirectResponse: response)
1141+
}
1142+
10581143
private struct Key: Hashable {
10591144
let method: String
10601145
let url: URL

0 commit comments

Comments
 (0)