@@ -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+
112194extension URL {
113195 /// The base distribution endpoint URL
114196 var distributionEndpoint : URL { self . appendingPathComponent ( " /v2/ " ) }
0 commit comments