Skip to content

Commit a1b4ad7

Browse files
authored
Add option to allow HTTP (not HTTPS) registries (#7204)
Addresses #7170: this adds an option to the `package-registry` `set` and `publish` commands to allow using an HTTP registry endpoint, bypassing the existing requirement that package registries use HTTPS. ### Motivation: In short, the motivation is to allow publishing to a registry that is hosted on a local machine. It is difficult to host a fully TLS-compliant HTTPS endpoint on developer machines at scale. See more detail in #7170 ### Modifications: * Adds a new `allowInsecureHTTP` flag to the Registry model. * Adds a new `--allow-insecure-http` flag to the `package-registry` `set` and `publish` commands. * Adds tests that validate the behavior of the model and command additions. * Adds tests that validate the requested constraints in #7170 regarding HTTP endpoints, such as not being able to publish to an HTTP endpoint if there is authentication configured for it, and `login` command requiring HTTPS. ### Result: We will be able to use the `swift` CLI to publish to our HTTP registries locally hosted on our development machines. As a result, we will be able to migrate to the official tooling instead of needing to continue using home-rolled publish tooling.
1 parent b704f85 commit a1b4ad7

File tree

5 files changed

+211
-11
lines changed

5 files changed

+211
-11
lines changed

Sources/PackageRegistry/RegistryConfiguration.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -405,9 +405,9 @@ extension PackageModel.Registry: Codable {
405405

406406
public init(from decoder: Decoder) throws {
407407
let container = try decoder.container(keyedBy: CodingKeys.self)
408-
self.init(
409-
url: try container.decode(URL.self, forKey: .url),
410-
supportsAvailability: try container.decodeIfPresent(Bool.self, forKey: .supportsAvailability) ?? false
408+
try self.init(
409+
url: container.decode(URL.self, forKey: .url),
410+
supportsAvailability: container.decodeIfPresent(Bool.self, forKey: .supportsAvailability) ?? false
411411
)
412412
}
413413

Sources/PackageRegistryTool/PackageRegistryTool+Publish.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ extension SwiftPackageRegistryTool {
8282
)
8383
var certificateChainPaths: [AbsolutePath] = []
8484

85+
@Flag(name: .customLong("allow-insecure-http"), help: "Allow using a non-HTTPS registry URL")
86+
var allowInsecureHTTP: Bool = false
87+
8588
@Flag(help: "Dry run only; prepare the archive and sign it but do not publish to the registry.")
8689
var dryRun: Bool = false
8790

@@ -106,7 +109,8 @@ extension SwiftPackageRegistryTool {
106109
throw ValidationError.unknownRegistry
107110
}
108111

109-
try registryURL.validateRegistryURL()
112+
let allowHTTP = try self.allowInsecureHTTP && (configuration.authentication(for: registryURL) == nil)
113+
try registryURL.validateRegistryURL(allowHTTP: allowHTTP)
110114

111115
// validate working directory path
112116
if let customWorkingDirectory {

Sources/PackageRegistryTool/PackageRegistryTool.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ public struct SwiftPackageRegistryTool: AsyncParsableCommand {
5454
@Option(help: "Associate the registry with a given scope")
5555
var scope: String?
5656

57+
@Flag(name: .customLong("allow-insecure-http"), help: "Allow using a non-HTTPS registry URL")
58+
var allowInsecureHTTP: Bool = false
59+
5760
@Argument(help: "The registry URL")
5861
var url: URL
5962

@@ -62,15 +65,16 @@ public struct SwiftPackageRegistryTool: AsyncParsableCommand {
6265
}
6366

6467
func run(_ swiftTool: SwiftTool) async throws {
65-
try self.registryURL.validateRegistryURL()
68+
try self.registryURL.validateRegistryURL(allowHTTP: self.allowInsecureHTTP)
6669

6770
let scope = try scope.map(PackageIdentity.Scope.init(validating:))
6871

6972
let set: (inout RegistryConfiguration) throws -> Void = { configuration in
73+
let registry = Registry(url: self.registryURL, supportsAvailability: false)
7074
if let scope {
71-
configuration.scopedRegistries[scope] = .init(url: self.registryURL, supportsAvailability: false)
75+
configuration.scopedRegistries[scope] = registry
7276
} else {
73-
configuration.defaultRegistry = .init(url: self.registryURL, supportsAvailability: false)
77+
configuration.defaultRegistry = registry
7478
}
7579
}
7680

@@ -161,8 +165,8 @@ public struct SwiftPackageRegistryTool: AsyncParsableCommand {
161165
}
162166

163167
extension URL {
164-
func validateRegistryURL() throws {
165-
guard self.scheme == "https" else {
168+
func validateRegistryURL(allowHTTP: Bool = false) throws {
169+
guard self.scheme == "https" || (self.scheme == "http" && allowHTTP) else {
166170
throw SwiftPackageRegistryTool.ValidationError.invalidURL(self)
167171
}
168172
}

Tests/CommandsTests/PackageRegistryToolTests.swift

Lines changed: 193 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import Commands
1515
import Foundation
1616
import PackageLoading
1717
import PackageModel
18+
import PackageRegistry
1819
@testable import PackageRegistryTool
1920
import PackageSigning
2021
import SPMTestSupport
@@ -117,6 +118,19 @@ final class PackageRegistryToolTests: CommandsTestCase {
117118
XCTAssertEqual(json["version"], .int(1))
118119
}
119120

121+
// Set default registry with allow-insecure-http option
122+
do {
123+
try execute(["set", "\(customRegistryBaseURL)", "--allow-insecure-http"], packagePath: packageRoot)
124+
125+
let json = try JSON(data: localFileSystem.readFileContents(configurationFilePath))
126+
XCTAssertEqual(json["registries"]?.dictionary?.count, 1)
127+
XCTAssertEqual(
128+
json["registries"]?.dictionary?["[default]"]?.dictionary?["url"]?.string,
129+
"\(customRegistryBaseURL)"
130+
)
131+
XCTAssertEqual(json["version"], .int(1))
132+
}
133+
120134
// Unset default registry
121135
do {
122136
try execute(["unset"], packagePath: packageRoot)
@@ -215,6 +229,40 @@ final class PackageRegistryToolTests: CommandsTestCase {
215229
}
216230
}
217231

232+
func testSetInsecureURL() throws {
233+
try fixture(name: "DependencyResolution/External/Simple") { fixturePath in
234+
let packageRoot = fixturePath.appending("Bar")
235+
let configurationFilePath = AbsolutePath(
236+
".swiftpm/configuration/registries.json",
237+
relativeTo: packageRoot
238+
)
239+
240+
XCTAssertFalse(localFileSystem.exists(configurationFilePath))
241+
242+
// Set default registry
243+
XCTAssertThrowsError(try execute(["set", "http://package.example.com"], packagePath: packageRoot))
244+
245+
XCTAssertFalse(localFileSystem.exists(configurationFilePath))
246+
}
247+
}
248+
249+
func testSetAllowedInsecureURL() throws {
250+
try fixture(name: "DependencyResolution/External/Simple") { fixturePath in
251+
let packageRoot = fixturePath.appending("Bar")
252+
let configurationFilePath = AbsolutePath(
253+
".swiftpm/configuration/registries.json",
254+
relativeTo: packageRoot
255+
)
256+
257+
XCTAssertFalse(localFileSystem.exists(configurationFilePath))
258+
259+
// Set default registry
260+
try execute(["set", "http://package.example.com", "--allow-insecure-http"], packagePath: packageRoot)
261+
262+
XCTAssertTrue(localFileSystem.exists(configurationFilePath))
263+
}
264+
}
265+
218266
func testSetInvalidScope() throws {
219267
try fixture(name: "DependencyResolution/External/Simple") { fixturePath in
220268
let packageRoot = fixturePath.appending("Bar")
@@ -402,6 +450,133 @@ final class PackageRegistryToolTests: CommandsTestCase {
402450
}
403451
}
404452

453+
func testPublishingToHTTPRegistry() throws {
454+
#if os(Linux)
455+
// needed for archiving
456+
guard SPM_posix_spawn_file_actions_addchdir_np_supported() else {
457+
throw XCTSkip("working directory not supported on this platform")
458+
}
459+
#endif
460+
461+
let packageIdentity = "test.my-package"
462+
let version = "0.1.0"
463+
let registryURL = "http://packages.example.com"
464+
465+
_ = try withTemporaryDirectory { temporaryDirectory in
466+
let packageDirectory = temporaryDirectory.appending("MyPackage")
467+
try localFileSystem.createDirectory(packageDirectory)
468+
469+
let initPackage = try InitPackage(
470+
name: "MyPackage",
471+
packageType: .executable,
472+
destinationPath: packageDirectory,
473+
fileSystem: localFileSystem
474+
)
475+
try initPackage.writePackageStructure()
476+
XCTAssertFileExists(packageDirectory.appending("Package.swift"))
477+
478+
let workingDirectory = temporaryDirectory.appending(component: UUID().uuidString)
479+
try localFileSystem.createDirectory(workingDirectory)
480+
481+
XCTAssertThrowsError(try SwiftPM.Registry.execute(
482+
[
483+
"publish",
484+
packageIdentity,
485+
version,
486+
"--url=\(registryURL)",
487+
"--scratch-directory=\(workingDirectory.pathString)",
488+
"--package-path=\(packageDirectory.pathString)",
489+
"--dry-run",
490+
]
491+
))
492+
}
493+
}
494+
495+
func testPublishingToAllowedHTTPRegistry() throws {
496+
#if os(Linux)
497+
// needed for archiving
498+
guard SPM_posix_spawn_file_actions_addchdir_np_supported() else {
499+
throw XCTSkip("working directory not supported on this platform")
500+
}
501+
#endif
502+
503+
let packageIdentity = "test.my-package"
504+
let version = "0.1.0"
505+
let registryURL = "http://packages.example.com"
506+
507+
// with no authentication configured for registry
508+
_ = try withTemporaryDirectory { temporaryDirectory in
509+
let packageDirectory = temporaryDirectory.appending("MyPackage")
510+
try localFileSystem.createDirectory(packageDirectory)
511+
512+
let initPackage = try InitPackage(
513+
name: "MyPackage",
514+
packageType: .executable,
515+
destinationPath: packageDirectory,
516+
fileSystem: localFileSystem
517+
)
518+
try initPackage.writePackageStructure()
519+
XCTAssertFileExists(packageDirectory.appending("Package.swift"))
520+
521+
let workingDirectory = temporaryDirectory.appending(component: UUID().uuidString)
522+
try localFileSystem.createDirectory(workingDirectory)
523+
524+
try SwiftPM.Registry.execute(
525+
[
526+
"publish",
527+
packageIdentity,
528+
version,
529+
"--url=\(registryURL)",
530+
"--scratch-directory=\(workingDirectory.pathString)",
531+
"--package-path=\(packageDirectory.pathString)",
532+
"--allow-insecure-http",
533+
"--dry-run",
534+
]
535+
)
536+
}
537+
538+
// with authentication configured for registry
539+
_ = try withTemporaryDirectory { temporaryDirectory in
540+
let packageDirectory = temporaryDirectory.appending("MyPackage")
541+
try localFileSystem.createDirectory(packageDirectory)
542+
543+
let initPackage = try InitPackage(
544+
name: "MyPackage",
545+
packageType: .executable,
546+
destinationPath: packageDirectory,
547+
fileSystem: localFileSystem
548+
)
549+
try initPackage.writePackageStructure()
550+
XCTAssertFileExists(packageDirectory.appending("Package.swift"))
551+
552+
let workingDirectory = temporaryDirectory.appending(component: UUID().uuidString)
553+
try localFileSystem.createDirectory(workingDirectory)
554+
555+
let configurationFilePath = AbsolutePath(
556+
".swiftpm/configuration/registries.json",
557+
relativeTo: packageDirectory
558+
)
559+
560+
try localFileSystem.createDirectory(configurationFilePath.parentDirectory, recursive: true)
561+
var configuration = RegistryConfiguration()
562+
try configuration.add(authentication: .init(type: .basic), for: URL(registryURL))
563+
try localFileSystem.writeFileContents(configurationFilePath, data: JSONEncoder().encode(configuration))
564+
565+
XCTAssertThrowsError(try SwiftPM.Registry.execute(
566+
[
567+
"publish",
568+
packageIdentity,
569+
version,
570+
"--url=\(registryURL)",
571+
"--scratch-directory=\(workingDirectory.pathString)",
572+
"--package-path=\(packageDirectory.pathString)",
573+
"--allow-insecure-http",
574+
"--dry-run",
575+
]
576+
))
577+
}
578+
}
579+
405580
func testPublishingUnsignedPackage() throws {
406581
#if os(Linux)
407582
// needed for archiving
@@ -903,13 +1078,18 @@ final class PackageRegistryToolTests: CommandsTestCase {
9031078
}
9041079
}
9051080

1081+
func testLoginRequiresHTTPS() {
1082+
let registryURL = URL(string: "http://packages.example.com")!
1083+
1084+
XCTAssertThrowsError(try SwiftPM.Registry.execute(["login", "--url", registryURL.absoluteString]))
1085+
}
1086+
9061087
func testCreateLoginURL() {
9071088
let registryURL = URL(string: "https://packages.example.com")!
9081089

9091090
XCTAssertEqual(try SwiftPackageRegistryTool.Login.loginURL(from: registryURL, loginAPIPath: nil).absoluteString, "https://packages.example.com/login")
9101091

9111092
XCTAssertEqual(try SwiftPackageRegistryTool.Login.loginURL(from: registryURL, loginAPIPath: "/secret-sign-in").absoluteString, "https://packages.example.com/secret-sign-in")
912-
9131093
}
9141094

9151095
func testCreateLoginURLMaintainsPort() {
@@ -920,6 +1100,18 @@ final class PackageRegistryToolTests: CommandsTestCase {
9201100
XCTAssertEqual(try SwiftPackageRegistryTool.Login.loginURL(from: registryURL, loginAPIPath: "/secret-sign-in").absoluteString, "https://packages.example.com:8081/secret-sign-in")
9211101
}
9221102

1103+
func testValidateRegistryURL() throws {
1104+
// Valid
1105+
try URL(string: "https://packages.example.com")!.validateRegistryURL()
1106+
try URL(string: "http://packages.example.com")!.validateRegistryURL(allowHTTP: true)
1107+
1108+
// Invalid
1109+
XCTAssertThrowsError(try URL(string: "http://packages.example.com")!.validateRegistryURL())
1110+
XCTAssertThrowsError(try URL(string: "http://packages.example.com")!.validateRegistryURL(allowHTTP: false))
1111+
XCTAssertThrowsError(try URL(string: "ssh://packages.example.com")!.validateRegistryURL())
1112+
XCTAssertThrowsError(try URL(string: "ftp://packages.example.com")!.validateRegistryURL(allowHTTP: true))
1113+
}
1114+
9231115
private func testRoots() throws -> [[UInt8]] {
9241116
try fixture(name: "Signing", createGitRepo: false) { fixturePath in
9251117
let rootCA = try localFileSystem

Tests/PackageRegistryTests/RegistryConfigurationTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ final class RegistryConfigurationTests: XCTestCase {
138138
},
139139
"bar": {
140140
"url": "\#(customRegistryBaseURL)"
141-
},
141+
}
142142
},
143143
"authentication": {
144144
"packages.example.com": {

0 commit comments

Comments
 (0)