Skip to content

Commit 345cd61

Browse files
authored
ContainerRegistry: Switch to swift-http-types (#11)
### Motivation Swift HTTP Types provides currency types for HTTP clients and servers, which can be used with URLSession and AsyncHTTPClient. Generally speaking, we try to keep Swift Container Plugin's dependencies to a minimum because each dependency increases the initial build time and also increases the chance of a version conflict with the project which is trying to use the plugin. However `swift-http-types` is quite a small package and offers some nice ergonomic improvements over `URLRequest`/`URLResponse`, such as enums for HTTP status codes. ### Modifications Switch all uses of `URLRequest`/`URLResponse` to `HTTPRequest`/`HTTPResponse` ### Result No functional change. ### Test Plan The unit and integration tests continue to pass.
1 parent dd38050 commit 345cd61

File tree

8 files changed

+106
-120
lines changed

8 files changed

+106
-120
lines changed

Package.swift

+3
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,15 @@ let package = Package(
2626
dependencies: [
2727
.package(url: "https://github.com/apple/swift-crypto.git", "1.0.0"..<"4.0.0"),
2828
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"),
29+
.package(url: "https://github.com/apple/swift-http-types.git", from: "1.2.0"),
2930
],
3031
targets: [
3132
.target(
3233
name: "ContainerRegistry",
3334
dependencies: [
3435
.product(name: "Crypto", package: "swift-crypto", condition: .when(platforms: [.linux])),
36+
.product(name: "HTTPTypes", package: "swift-http-types"),
37+
.product(name: "HTTPTypesFoundation", package: "swift-http-types"),
3538
.target(
3639
name: "Basics" // AuthorizationProvider
3740
),

Sources/ContainerRegistry/AuthHandler.swift

+11-13
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,8 @@
1414

1515
import Basics
1616
import Foundation
17-
#if canImport(FoundationNetworking)
18-
import FoundationNetworking
19-
#endif
2017
import RegexBuilder
18+
import HTTPTypes
2119

2220
struct BearerTokenResponse: Codable {
2321
/// An opaque Bearer token that clients should supply to
@@ -130,7 +128,7 @@ public struct AuthHandler {
130128
}
131129

132130
/// Get locally-configured credentials, such as netrc or username/password, for a request
133-
func localCredentials(for request: URLRequest) -> String? {
131+
func localCredentials(for request: HTTPRequest) -> String? {
134132
guard let requestURL = request.url else { return nil }
135133

136134
if let netrcEntry = auth?.httpAuthorizationHeader(for: requestURL) { return netrcEntry }
@@ -149,7 +147,7 @@ public struct AuthHandler {
149147
/// In future it could provide cached responses from previous challenges.
150148
/// - Parameter request: The request to authorize.
151149
/// - Returns: The request, with an appropriate authorization header added, or nil if no credentials are available.
152-
public func auth(for request: URLRequest) -> URLRequest? { nil }
150+
public func auth(for request: HTTPRequest) -> HTTPRequest? { nil }
153151

154152
/// Add authorization to an HTTP rquest in response to a challenge from the server.
155153
/// - Parameters:
@@ -158,13 +156,13 @@ public struct AuthHandler {
158156
/// - client: An HTTP client, used to retrieve tokens if necessary.
159157
/// - Returns: The request, with an appropriate authorization header added, or nil if no credentials are available.
160158
/// - Throws: If an error occurs while retrieving a credential.
161-
public func auth(for request: URLRequest, withChallenge challenge: String, usingClient client: HTTPClient)
162-
async throws -> URLRequest?
159+
public func auth(for request: HTTPRequest, withChallenge challenge: String, usingClient client: HTTPClient)
160+
async throws -> HTTPRequest?
163161
{
164162
if challenge.lowercased().starts(with: "basic") {
165163
guard let authHeader = localCredentials(for: request) else { return nil }
166164
var request = request
167-
request.addValue(authHeader, forHTTPHeaderField: "Authorization")
165+
request.headerFields[.authorization] = authHeader
168166
return request
169167

170168
} else if challenge.lowercased().starts(with: "bearer") {
@@ -176,15 +174,15 @@ public struct AuthHandler {
176174
challenge.dropFirst("bearer".count).trimmingCharacters(in: .whitespacesAndNewlines)
177175
)
178176
guard let challengeURL = parsedChallenge.url else { return nil }
179-
var req = URLRequest(url: challengeURL)
180-
if let credentials = localCredentials(for: req) {
181-
req.addValue("\(credentials)", forHTTPHeaderField: "Authorization")
177+
var tokenRequest = HTTPRequest(url: challengeURL)
178+
if let credentials = localCredentials(for: tokenRequest) {
179+
tokenRequest.headerFields[.authorization] = credentials
182180
}
183181

184-
let (data, _) = try await client.executeRequestThrowing(req, expectingStatus: 200)
182+
let (data, _) = try await client.executeRequestThrowing(tokenRequest, expectingStatus: .ok)
185183
let tokenResponse = try JSONDecoder().decode(BearerTokenResponse.self, from: data)
186184
var request = request
187-
request.addValue("Bearer \(tokenResponse.token)", forHTTPHeaderField: "Authorization")
185+
request.headerFields[.authorization] = "Bearer \(tokenResponse.token)"
188186
return request
189187

190188
} else {

Sources/ContainerRegistry/Blobs.swift

+11-11
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,14 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import Foundation
16+
import HTTPTypes
1617

1718
#if canImport(CryptoKit)
1819
import CryptoKit
1920
#else
2021
import Crypto
2122
#endif
2223

23-
#if canImport(FoundationNetworking)
24-
import FoundationNetworking
25-
#endif
26-
2724
/// Calculates the digest of a blob of data.
2825
/// - Parameter data: Blob of data to digest.
2926
/// - Returns: The blob's digest, in the format expected by the distribution protocol.
@@ -45,17 +42,20 @@ extension RegistryClient {
4542
// Response will include a 'Location' header telling us where to PUT the blob data.
4643
let httpResponse = try await executeRequestThrowing(
4744
.post(registryURLForPath("/v2/\(repository)/blobs/uploads/")),
48-
expectingStatus: 202, // expected response code for a two-shot upload
49-
decodingErrors: [404]
45+
expectingStatus: .accepted, // expected response code for a "two-shot" upload
46+
decodingErrors: [.notFound]
5047
)
5148

52-
guard let location = httpResponse.response.value(forHTTPHeaderField: "Location") else {
49+
guard let location = httpResponse.response.headerFields[.location] else {
5350
throw HTTPClientError.missingResponseHeader("Location")
5451
}
5552
return URLComponents(string: location)
5653
}
5754
}
5855

56+
// The spec says that Docker- prefix headers are no longer to be used, but also specifies that the registry digest is returned in this header.
57+
extension HTTPField.Name { static let dockerContentDigest = Self("Docker-Content-Digest")! }
58+
5959
public extension RegistryClient {
6060
func blobExists(repository: String, digest: String) async throws -> Bool {
6161
precondition(repository.count > 0, "repository must not be an empty string")
@@ -67,7 +67,7 @@ public extension RegistryClient {
6767
decodingErrors: [404]
6868
)
6969
return true
70-
} catch HTTPClientError.unexpectedStatusCode(status: 404, _, _) { return false }
70+
} catch HTTPClientError.unexpectedStatusCode(status: .notFound, _, _) { return false }
7171
}
7272

7373
/// Fetches an unstructured blob of data from the registry.
@@ -141,14 +141,14 @@ public extension RegistryClient {
141141
// All blob uploads have Content-Type: application/octet-stream on the wire, even if mediatype is different
142142
.put(uploadURL, contentType: "application/octet-stream"),
143143
uploading: data,
144-
expectingStatus: 201,
145-
decodingErrors: [400, 404]
144+
expectingStatus: .created,
145+
decodingErrors: [.badRequest, .notFound]
146146
)
147147

148148
// The registry could compute a different digest and we should use its value
149149
// as the canonical digest for linking blobs. If the registry sends a digest we
150150
// should check that it matches our locally-calculated digest.
151-
if let serverDigest = httpResponse.response.value(forHTTPHeaderField: "Docker-Content-Digest") {
151+
if let serverDigest = httpResponse.response.headerFields[.dockerContentDigest] {
152152
assert(digest == serverDigest)
153153
}
154154
return .init(mediaType: mediaType, digest: digest, size: Int64(data.count))

Sources/ContainerRegistry/CheckAPI.swift

+6-3
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@ public extension RegistryClient {
2020
// The registry may require authentication on this endpoint.
2121
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#determining-support
2222
do {
23-
return try await executeRequestThrowing(.get(registryURLForPath("/v2/")), decodingErrors: [401, 404]).data
24-
== EmptyObject()
25-
} catch HTTPClientError.unexpectedStatusCode(status: 404, _, _) { return false }
23+
return try await executeRequestThrowing(
24+
.get(registryURLForPath("/v2/")),
25+
decodingErrors: [.unauthorized, .notFound]
26+
)
27+
.data == EmptyObject()
28+
} catch HTTPClientError.unexpectedStatusCode(status: .notFound, _, _) { return false }
2629
}
2730
}

0 commit comments

Comments
 (0)