@@ -104,11 +104,96 @@ 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+ /// - Throws: If the registry name is invalid.
130+ /// - Throws: If a connection to the registry cannot be established.
131+ func urlSession(
132+ _ session: URLSession ,
133+ task: URLSessionTask ,
134+ willPerformHTTPRedirection response: HTTPURLResponse ,
135+ newRequest request: URLRequest ,
136+ completionHandler: @escaping ( URLRequest ? ) -> Swift . Void
137+ ) {
138+ // The Authorization header should be removed when following a redirect:
139+ //
140+ // https://fetch.spec.whatwg.org/#http-redirect-fetch
141+ //
142+ // URLSession on macOS does this, but on Linux the header is left in place.
143+ // This causes problems when pulling images from Docker Hub on Linux.
144+ //
145+ // Docker Hub redirects to AWS S3 via CloudFlare. Including the Authorization header
146+ // in the redirected request causes a 400 error to be returned with the XML message:
147+ //
148+ // InvalidRequest: Missing x-amz-content-sha256
149+ //
150+ // Removing the Authorization header makes the redirected request work.
151+ //
152+ // The spec also requires that if the redirected request is a POST, the method
153+ // should be changed to GET and the body should be deleted:
154+ //
155+ // https://datatracker.ietf.org/doc/html/rfc7231#section-6.4
156+ //
157+ // URLSession makes these changes before calling this delegate method:
158+ //
159+ // https://github.com/swiftlang/swift-corelibs-foundation/blob/265274a4be41b3d4d74fe4626d970898e4df330f/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift#L567C1-L572C1
160+ //
161+ // In the delegate:
162+ // - response.url is origin of the redirect response
163+ // - request.url is value of the redirect response's Location header
164+ //
165+ // URLSession also limits redirect loops:
166+ //
167+ // https://github.com/swiftlang/swift-corelibs-foundation/blob/265274a4be41b3d4d74fe4626d970898e4df330f/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift#L459C1-L460C38
168+
169+ var request = request
170+
171+
172+ guard let origin = response. url, let redirect = request. url else {
173+ // Reject the redirect if either URL is missing
174+ completionHandler ( nil )
175+ return
176+ }
177+
178+ // https://fetch.spec.whatwg.org/#http-redirect-fetch
179+ if !origin. hasSameOrigin ( as: redirect) {
180+ // Header names are case-insensitive
181+ if let index = request. allHTTPHeaderFields? . firstIndex ( where: { $0. key. lowercased ( ) == " authorization " } ) {
182+ request. allHTTPHeaderFields? . remove ( at: index)
183+ }
184+ }
185+
186+ completionHandler ( request)
187+ }
188+ }
189+
190+ extension URL {
191+ // https://html.spec.whatwg.org/multipage/browsers.html#same-origin
192+ func hasSameOrigin( as other: URL ) -> Bool {
193+ self . scheme == other. scheme && self . host == other. host && self . port == other. port
194+ }
195+ }
196+
112197extension URL {
113198 /// The base distribution endpoint URL
114199 var distributionEndpoint : URL { self . appendingPathComponent ( " /v2/ " ) }
0 commit comments