Skip to content

Commit b945527

Browse files
committed
ContainerRegistry: Remove Authorization header when redirecting on Linux
1 parent ebbd7bc commit b945527

File tree

1 file changed

+86
-1
lines changed

1 file changed

+86
-1
lines changed

Sources/ContainerRegistry/RegistryClient.swift

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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+
112197
extension URL {
113198
/// The base distribution endpoint URL
114199
var distributionEndpoint: URL { self.appendingPathComponent("/v2/") }

0 commit comments

Comments
 (0)