Skip to content

Commit 65be379

Browse files
committed
ContainerRegistry: Remove Authorization header when redirecting on Linux
1 parent 79027c5 commit 65be379

File tree

1 file changed

+83
-1
lines changed

1 file changed

+83
-1
lines changed

Sources/ContainerRegistry/RegistryClient.swift

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,93 @@ public struct RegistryClient {
104104
// URLSessionConfiguration.default allows request and credential caching, making testing confusing.
105105
// The SwiftPM sandbox also prevents URLSession from writing to the cache, which causes warnings.
106106
// .ephemeral has no caches.
107-
let urlsession = URLSession(configuration: .ephemeral)
107+
// A delegate is needed to remove the Authorization header when following HTTP redirects on Linux.
108+
let urlsession = URLSession(
109+
configuration: .ephemeral,
110+
delegate: RegistryURLSessionDelegate(),
111+
delegateQueue: nil
112+
)
108113
try await self.init(registry: registryURL, client: urlsession, auth: auth)
109114
}
110115
}
111116

117+
final class RegistryURLSessionDelegate: NSObject {}
118+
119+
extension RegistryURLSessionDelegate: URLSessionDelegate, URLSessionTaskDelegate {
120+
/// Called if the RegistryClient receives an HTTP redirect from the registry.
121+
/// - Parameters:
122+
/// - session: The session containing the task whose request resulted in a redirect.
123+
/// - task: The task whose request resulted in a redirect.
124+
/// - response: An object containing the server’s response to the original request.
125+
/// - request: A URL request object filled out with the new location.
126+
/// - completionHandler: A block that your handler should call with either the value
127+
/// of the request parameter, a modified URL request object, or NULL to refuse the
128+
/// redirect and return the body of the redirect response.
129+
func urlSession(
130+
_ session: URLSession,
131+
task: URLSessionTask,
132+
willPerformHTTPRedirection response: HTTPURLResponse,
133+
newRequest request: URLRequest,
134+
completionHandler: @escaping (URLRequest?) -> Swift.Void
135+
) {
136+
// The Authorization header should be removed when following a redirect:
137+
//
138+
// https://fetch.spec.whatwg.org/#http-redirect-fetch
139+
//
140+
// URLSession on macOS does this, but on Linux the header is left in place.
141+
// This causes problems when pulling images from Docker Hub on Linux.
142+
//
143+
// Docker Hub redirects to AWS S3 via CloudFlare. Including the Authorization header
144+
// in the redirected request causes a 400 error to be returned with the XML message:
145+
//
146+
// InvalidRequest: Missing x-amz-content-sha256
147+
//
148+
// Removing the Authorization header makes the redirected request work.
149+
//
150+
// The spec also requires that if the redirected request is a POST, the method
151+
// should be changed to GET and the body should be deleted:
152+
//
153+
// https://datatracker.ietf.org/doc/html/rfc7231#section-6.4
154+
//
155+
// URLSession makes these changes before calling this delegate method:
156+
//
157+
// https://github.com/swiftlang/swift-corelibs-foundation/blob/265274a4be41b3d4d74fe4626d970898e4df330f/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift#L567C1-L572C1
158+
//
159+
// In the delegate:
160+
// - response.url is origin of the redirect response
161+
// - request.url is value of the redirect response's Location header
162+
//
163+
// URLSession also limits redirect loops:
164+
//
165+
// https://github.com/swiftlang/swift-corelibs-foundation/blob/265274a4be41b3d4d74fe4626d970898e4df330f/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift#L459C1-L460C38
166+
167+
var request = request
168+
169+
guard let origin = response.url, let redirect = request.url else {
170+
// Reject the redirect if either URL is missing
171+
completionHandler(nil)
172+
return
173+
}
174+
175+
// https://fetch.spec.whatwg.org/#http-redirect-fetch
176+
if !origin.hasSameOrigin(as: redirect) {
177+
// Header names are case-insensitive
178+
if let index = request.allHTTPHeaderFields?.firstIndex(where: { $0.key.lowercased() == "authorization" }) {
179+
request.allHTTPHeaderFields?.remove(at: index)
180+
}
181+
}
182+
183+
completionHandler(request)
184+
}
185+
}
186+
187+
extension URL {
188+
// https://html.spec.whatwg.org/multipage/browsers.html#same-origin
189+
func hasSameOrigin(as other: URL) -> Bool {
190+
self.scheme == other.scheme && self.host == other.host && self.port == other.port
191+
}
192+
}
193+
112194
extension URL {
113195
/// The base distribution endpoint URL
114196
var distributionEndpoint: URL { self.appendingPathComponent("/v2/") }

0 commit comments

Comments
 (0)