Skip to content

fix: DownloadTaskManager handle redirect authentication #8947

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

vsarunas
Copy link

Fix download of private artifact bundles on GitHub; which now involves a redirect to another domain to which authentication header should not be sent. Fixes #8946

Motivation:

GitHub API redirects private artifact bundle downloads to Azure Storage, but GitHub authentication headers are sent to Azure Storage domain, resulting in 403 errors:

error: failed downloading 'https://api.github.com/repos/ordo-one/model/releases/assets/273301740-linux-313.1.0.zip' which is required by binary target 'Ordo': badResponseStatusCode(403)

The issue occurs because Azure Storage rejects GitHub authentication that was set for api.github.com domain; there should be no header set as the redirect comes with a SAS token.

Modifications:

DataTaskManager has a redirect handler, but DownloadTaskManager does not. Pass authorizationProvider to DownloadTaskManager in order to set correct authorisation headers for cases when there is a redirect to a new domain.

Result:

Private GitHub artifact bundle downloads succeed by properly handling cross-domain redirects without leaking authentication headers.

@plemarquand
Copy link
Contributor

@swift-ci test


// Add new authorization header for a redirect if there is one, otherwise remove
var redirectRequest = request
if let redirectURL = request.url, let authorization = task.authorizationProvider?(redirectURL) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Can you add a test case to Tests/BasicsTests/URLSessionHTTPClientTests.swift?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a draft based on testAsyncDownloadDefaultAuthenticationSuccess(), the 302 request is sent out but the URLSessionHTTPClient is not being executed for me under Xcode.

I don't see how to add an assert or to view the intermediate headers in the test.

@plemarquand
Copy link
Contributor

Overall this looks good, thanks for the contribution! I'm a little concerned that GitHub changed this behaviour unintentionally, but even if they revert to the old behaviour I think this should still work.

Copy link
Contributor

@plemarquand plemarquand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry this took me a while to get back to. Its possible to update this test to exercise the redirect handling but requires an update to the MockURLProtocol.

@@ -805,6 +805,67 @@ final class URLSessionHTTPClientTest: XCTestCase {
}
}

func testAsyncDownloadAuthenticateWithRedirectdSuccess() async throws {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
func testAsyncDownloadAuthenticateWithRedirectdSuccess() async throws {
func testAsyncDownloadAuthenticateWithRedirectedSuccess() async throws {

Comment on lines +832 to +843
let request = HTTPClient.Request.download(
url: url,
options: options,
fileSystem: localFileSystem,
destination: destination
)

MockURLProtocol.onRequest(request) { request in
XCTAssertEqual(request.allHTTPHeaderFields?["Authorization"], testAuthHeader)
MockURLProtocol.sendResponse(statusCode: 302, headers: ["Location": redirectURL.absoluteString], for: request)
MockURLProtocol.sendCompletion(for: request)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll need to add a new method to MockURLProtocol to handle the redirecting, and then you can update this config to:

Suggested change
let request = HTTPClient.Request.download(
url: url,
options: options,
fileSystem: localFileSystem,
destination: destination
)
MockURLProtocol.onRequest(request) { request in
XCTAssertEqual(request.allHTTPHeaderFields?["Authorization"], testAuthHeader)
MockURLProtocol.sendResponse(statusCode: 302, headers: ["Location": redirectURL.absoluteString], for: request)
MockURLProtocol.sendCompletion(for: request)
}
let request = HTTPClient.Request.download(
url: url,
options: options,
fileSystem: localFileSystem,
destination: destination
)
let redirectRequest = HTTPClient.Request.download(
url: redirectURL,
options: options,
fileSystem: localFileSystem,
destination: destination
)
MockURLProtocol.onRequest(redirectRequest) { request in
XCTAssertEqual(request.allHTTPHeaderFields?["Authorization"], testAuthHeader)
MockURLProtocol.sendResponse(statusCode: 200, headers: ["Content-Length": "1024"], for: request)
MockURLProtocol.sendData(Data(repeating: 0xBE, count: 512), for: request)
MockURLProtocol.sendData(Data(repeating: 0xEF, count: 512), for: request)
MockURLProtocol.sendCompletion(for: request)
}
MockURLProtocol.onRequest(request) { request in
XCTAssertEqual(request.allHTTPHeaderFields?["Authorization"], testAuthHeader)
MockURLProtocol.sendResponse(statusCode: 302, headers: ["Location": redirectURL.absoluteString], for: request)
MockURLProtocol.sendRedirect(for: request, to: URLRequest(url: redirectURL))
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned above, this would go in URLSessionHTTPClientTests.swift to mock out url redirection:

    static func sendRedirect(for request: URLRequest, to newRequest: URLRequest) {
        let key = Key(request.httpMethod!, request.url!)
        self.sendRedirect(newRequest: newRequest, for: key)
    }

    private static func sendRedirect(newRequest: URLRequest, for key: Key) {
        guard let request = self.requests[key] else {
            return XCTFail("url did not start loading")
        }
        let response = HTTPURLResponse(url: key.url, statusCode: 302, httpVersion: "1.1", headerFields: nil)!
        request.client?.urlProtocol(request, wasRedirectedTo: newRequest, redirectResponse: response)
    }

// https://github.com/apple/swift-corelibs-foundation/pull/2593 tries to address the latter part
try XCTSkipIf(true, "test is only supported on macOS")
#endif
let netrcContent = "machine async-protected.downloader-tests.com login anonymous password qwerty"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let netrcContent = "machine async-protected.downloader-tests.com login anonymous password qwerty"
let netrcContent = """
machine async-protected.downloader-tests.com login anonymous password qwerty
machine cdn-async.downloader-tests.com login anonymous password qwerty
"""

Comment on lines +859 to +865
XCTAssertEqual(response.statusCode, 302)
//XCTAssertEqual(response.headers.get("Location"), redirectURL.absoluteString)

XCTAssertFileExists(destination)

//let bytes = ByteString(Array(repeating: 0xBE, count: 512) + Array(repeating: 0xEF, count: 512))
//XCTAssertEqual(try! localFileSystem.readFileContents(destination), bytes)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
XCTAssertEqual(response.statusCode, 302)
//XCTAssertEqual(response.headers.get("Location"), redirectURL.absoluteString)
XCTAssertFileExists(destination)
//let bytes = ByteString(Array(repeating: 0xBE, count: 512) + Array(repeating: 0xEF, count: 512))
//XCTAssertEqual(try! localFileSystem.readFileContents(destination), bytes)
XCTAssertEqual(response.statusCode, 200)
XCTAssertFileExists(destination)
let bytes = ByteString(Array(repeating: 0xBE, count: 512) + Array(repeating: 0xEF, count: 512))
XCTAssertEqual(try! localFileSystem.readFileContents(destination), bytes)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Private Artifact Bundle downloads sometimes fail with 403 error
3 participants