Skip to content

Commit 1ea1608

Browse files
authored
ContainerRegistry: Remove Authorization header when redirecting on Linux (#167)
Motivation ---------- In #164, @herzi reported a problem building container images based on `swift:slim` on Linux. The root cause of the problem is that Docker Hub now redirects pulls for `swift:slim` to an S3 bucked behind CloudFlare. URLSession on macOS follows the redirect correctly, but URLSession on Linux incorrectly includes an `Authorization` header which S3 rejects. Modifications ------------- Add a URLSession delegate which removes the `Authorization` header when following a redirect. Result ------ Pulling `swift:slim` from Docker Hub works again. Fixes: #166 Test Plan --------- * #164 extended the end-to-end tests to use `swift:slim` as well as `scratch`, demonstrating the problem. The extended tests now pass. * All existing tests continue to pass.
2 parents ebbd7bc + c06f14b commit 1ea1608

File tree

2 files changed

+86
-2
lines changed

2 files changed

+86
-2
lines changed

.github/workflows/endtoend_tests.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ jobs:
2121
- 5000:5000
2222
strategy:
2323
matrix:
24+
from:
25+
- scratch
26+
- swift:slim
2427
example:
2528
- Examples/HelloWorldVapor
2629
- Examples/HelloWorldHummingbird
@@ -52,7 +55,7 @@ jobs:
5255
--allow-network-connections all \
5356
build-container-image \
5457
--repository localhost:5000/example \
55-
--from scratch
58+
--from ${{ matrix.from }}
5659
5760
- name: Run the example
5861
run: |

Sources/ContainerRegistry/RegistryClient.swift

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,92 @@ 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+
request.allHTTPHeaderFields = request.allHTTPHeaderFields?
179+
.filter({ $0.key.lowercased() != "authorization" })
180+
}
181+
182+
completionHandler(request)
183+
}
184+
}
185+
186+
extension URL {
187+
// https://html.spec.whatwg.org/multipage/browsers.html#same-origin
188+
func hasSameOrigin(as other: URL) -> Bool {
189+
self.scheme == other.scheme && self.host == other.host && self.port == other.port
190+
}
191+
}
192+
112193
extension URL {
113194
/// The base distribution endpoint URL
114195
var distributionEndpoint: URL { self.appendingPathComponent("/v2/") }

0 commit comments

Comments
 (0)