Commit 854dd9d735

854dd9d73528dcea717a01775ad0d2be19b14079

parent: e519d100b6

Unsigned

cmc <hello@cleberg.net> · 2026-03-19 00:16 UTC

Guard authenticated text fetches to sr.ht hosts and add regression test

Layout: unified · split

Hutch.xcodeproj/project.pbxproj +130
@@ -13,12 +13,25 @@
1313 8B2F89682F69DEB900FC0253 /* SECURITY.md in Resources */ = {isa = PBXBuildFile; fileRef = 8B2F89652F69DEB900FC0253 /* SECURITY.md */; };
1414/* End PBXBuildFile section */
1515
16/* Begin PBXContainerItemProxy section */
17 8B8182BB2F6B73F3000AE049 /* PBXContainerItemProxy */ = {
18 isa = PBXContainerItemProxy;
19 containerPortal = 8B4B28C92F6704280045FA19 /* Project object */;
20 proxyType = 1;
21 remoteGlobalIDString = 8B4B28D02F6704280045FA19;
22 remoteInfo = Hutch;
23 };
24/* End PBXContainerItemProxy section */
25
1626/* Begin PBXFileReference section */
1727 8B2F89612F69DEA900FC0253 /* TODO.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = TODO.md; sourceTree = "<group>"; };
1828 8B2F89632F69DEB900FC0253 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
1929 8B2F89642F69DEB900FC0253 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
2030 8B2F89652F69DEB900FC0253 /* SECURITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SECURITY.md; sourceTree = "<group>"; };
2131 8B4B28D12F6704280045FA19 /* Hutch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Hutch.app; sourceTree = BUILT_PRODUCTS_DIR; };
32 8B8182B72F6B73F3000AE049 /* HutchTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HutchTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
33 8B8182C22F6B742B000AE049 /* HutchTests */ = {isa = PBXFileReference; lastKnownFileType = folder; path = HutchTests; sourceTree = "<group>"; };
34 8BDCA5272F6B76B20066AA29 /* HutchTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = HutchTests.xctestplan; sourceTree = "<group>"; };
2235/* End PBXFileReference section */
2336
2437/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -40,6 +53,11 @@
4053 path = Hutch;
4154 sourceTree = "<group>";
4255 };
56 8B8182B82F6B73F3000AE049 /* HutchTests */ = {
57 isa = PBXFileSystemSynchronizedRootGroup;
58 path = HutchTests;
59 sourceTree = "<group>";
60 };
4361/* End PBXFileSystemSynchronizedRootGroup section */
4462
4563/* Begin PBXFrameworksBuildPhase section */
@@ -50,17 +68,27 @@
5068 );
5169 runOnlyForDeploymentPostprocessing = 0;
5270 };
71 8B8182B42F6B73F3000AE049 /* Frameworks */ = {
72 isa = PBXFrameworksBuildPhase;
73 buildActionMask = 2147483647;
74 files = (
75 );
76 runOnlyForDeploymentPostprocessing = 0;
77 };
5378/* End PBXFrameworksBuildPhase section */
5479
5580/* Begin PBXGroup section */
5681 8B4B28C82F6704280045FA19 = {
5782 isa = PBXGroup;
5883 children = (
84 8BDCA5272F6B76B20066AA29 /* HutchTests.xctestplan */,
85 8B8182C22F6B742B000AE049 /* HutchTests */,
5986 8B2F89632F69DEB900FC0253 /* LICENSE */,
6087 8B2F89642F69DEB900FC0253 /* README.md */,
6188 8B2F89652F69DEB900FC0253 /* SECURITY.md */,
6289 8B2F89612F69DEA900FC0253 /* TODO.md */,
6390 8B4B28D32F6704280045FA19 /* Hutch */,
91 8B8182B82F6B73F3000AE049 /* HutchTests */,
6492 8B4B28D22F6704280045FA19 /* Products */,
6593 );
6694 sourceTree = "<group>";
@@ -69,6 +97,7 @@
6997 isa = PBXGroup;
7098 children = (
7199 8B4B28D12F6704280045FA19 /* Hutch.app */,
100 8B8182B72F6B73F3000AE049 /* HutchTests.xctest */,
72101 );
73102 name = Products;
74103 sourceTree = "<group>";
@@ -98,6 +127,29 @@
98127 productReference = 8B4B28D12F6704280045FA19 /* Hutch.app */;
99128 productType = "com.apple.product-type.application";
100129 };
130 8B8182B62F6B73F3000AE049 /* HutchTests */ = {
131 isa = PBXNativeTarget;
132 buildConfigurationList = 8B8182BF2F6B73F3000AE049 /* Build configuration list for PBXNativeTarget "HutchTests" */;
133 buildPhases = (
134 8B8182B32F6B73F3000AE049 /* Sources */,
135 8B8182B42F6B73F3000AE049 /* Frameworks */,
136 8B8182B52F6B73F3000AE049 /* Resources */,
137 );
138 buildRules = (
139 );
140 dependencies = (
141 8B8182BC2F6B73F3000AE049 /* PBXTargetDependency */,
142 );
143 fileSystemSynchronizedGroups = (
144 8B8182B82F6B73F3000AE049 /* HutchTests */,
145 );
146 name = HutchTests;
147 packageProductDependencies = (
148 );
149 productName = HutchTests;
150 productReference = 8B8182B72F6B73F3000AE049 /* HutchTests.xctest */;
151 productType = "com.apple.product-type.bundle.unit-test";
152 };
101153/* End PBXNativeTarget section */
102154
103155/* Begin PBXProject section */
@@ -111,6 +163,10 @@
111163 8B4B28D02F6704280045FA19 = {
112164 CreatedOnToolsVersion = 26.3;
113165 };
166 8B8182B62F6B73F3000AE049 = {
167 CreatedOnToolsVersion = 26.3;
168 TestTargetID = 8B4B28D02F6704280045FA19;
169 };
114170 };
115171 };
116172 buildConfigurationList = 8B4B28CC2F6704280045FA19 /* Build configuration list for PBXProject "Hutch" */;
@@ -128,6 +184,7 @@
128184 projectRoot = "";
129185 targets = (
130186 8B4B28D02F6704280045FA19 /* Hutch */,
187 8B8182B62F6B73F3000AE049 /* HutchTests */,
131188 );
132189 };
133190/* End PBXProject section */
@@ -144,6 +201,13 @@
144201 );
145202 runOnlyForDeploymentPostprocessing = 0;
146203 };
204 8B8182B52F6B73F3000AE049 /* Resources */ = {
205 isa = PBXResourcesBuildPhase;
206 buildActionMask = 2147483647;
207 files = (
208 );
209 runOnlyForDeploymentPostprocessing = 0;
210 };
147211/* End PBXResourcesBuildPhase section */
148212
149213/* Begin PBXSourcesBuildPhase section */
@@ -154,8 +218,23 @@
154218 );
155219 runOnlyForDeploymentPostprocessing = 0;
156220 };
221 8B8182B32F6B73F3000AE049 /* Sources */ = {
222 isa = PBXSourcesBuildPhase;
223 buildActionMask = 2147483647;
224 files = (
225 );
226 runOnlyForDeploymentPostprocessing = 0;
227 };
157228/* End PBXSourcesBuildPhase section */
158229
230/* Begin PBXTargetDependency section */
231 8B8182BC2F6B73F3000AE049 /* PBXTargetDependency */ = {
232 isa = PBXTargetDependency;
233 target = 8B4B28D02F6704280045FA19 /* Hutch */;
234 targetProxy = 8B8182BB2F6B73F3000AE049 /* PBXContainerItemProxy */;
235 };
236/* End PBXTargetDependency section */
237
159238/* Begin XCBuildConfiguration section */
160239 8B4B28DA2F6704290045FA19 /* Debug */ = {
161240 isa = XCBuildConfiguration;
@@ -350,6 +429,48 @@
350429 };
351430 name = Release;
352431 };
432 8B8182BD2F6B73F3000AE049 /* Debug */ = {
433 isa = XCBuildConfiguration;
434 buildSettings = {
435 BUNDLE_LOADER = "$(TEST_HOST)";
436 CODE_SIGN_STYLE = Automatic;
437 CURRENT_PROJECT_VERSION = 1;
438 DEVELOPMENT_TEAM = ZCNAX3VL9D;
439 GENERATE_INFOPLIST_FILE = YES;
440 MARKETING_VERSION = 1.0;
441 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.HutchTests;
442 PRODUCT_NAME = "$(TARGET_NAME)";
443 STRING_CATALOG_GENERATE_SYMBOLS = NO;
444 SWIFT_APPROACHABLE_CONCURRENCY = YES;
445 SWIFT_EMIT_LOC_STRINGS = NO;
446 SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
447 SWIFT_VERSION = 5.0;
448 TARGETED_DEVICE_FAMILY = "1,2";
449 TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hutch.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Hutch";
450 };
451 name = Debug;
452 };
453 8B8182BE2F6B73F3000AE049 /* Release */ = {
454 isa = XCBuildConfiguration;
455 buildSettings = {
456 BUNDLE_LOADER = "$(TEST_HOST)";
457 CODE_SIGN_STYLE = Automatic;
458 CURRENT_PROJECT_VERSION = 1;
459 DEVELOPMENT_TEAM = ZCNAX3VL9D;
460 GENERATE_INFOPLIST_FILE = YES;
461 MARKETING_VERSION = 1.0;
462 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.HutchTests;
463 PRODUCT_NAME = "$(TARGET_NAME)";
464 STRING_CATALOG_GENERATE_SYMBOLS = NO;
465 SWIFT_APPROACHABLE_CONCURRENCY = YES;
466 SWIFT_EMIT_LOC_STRINGS = NO;
467 SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
468 SWIFT_VERSION = 5.0;
469 TARGETED_DEVICE_FAMILY = "1,2";
470 TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hutch.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Hutch";
471 };
472 name = Release;
473 };
353474/* End XCBuildConfiguration section */
354475
355476/* Begin XCConfigurationList section */
@@ -371,6 +492,15 @@
371492 defaultConfigurationIsVisible = 0;
372493 defaultConfigurationName = Release;
373494 };
495 8B8182BF2F6B73F3000AE049 /* Build configuration list for PBXNativeTarget "HutchTests" */ = {
496 isa = XCConfigurationList;
497 buildConfigurations = (
498 8B8182BD2F6B73F3000AE049 /* Debug */,
499 8B8182BE2F6B73F3000AE049 /* Release */,
500 );
501 defaultConfigurationIsVisible = 0;
502 defaultConfigurationName = Release;
503 };
374504/* End XCConfigurationList section */
375505 };
376506 rootObject = 8B4B28C92F6704280045FA19 /* Project object */;
Hutch.xcodeproj/xcshareddata/xcschemes/Hutch.xcscheme added +96
@@ -0,0 +1,96 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<Scheme
3 LastUpgradeVersion = "2630"
4 version = "1.7">
5 <BuildAction
6 parallelizeBuildables = "YES"
7 buildImplicitDependencies = "YES"
8 buildArchitectures = "Automatic">
9 <BuildActionEntries>
10 <BuildActionEntry
11 buildForTesting = "YES"
12 buildForRunning = "YES"
13 buildForProfiling = "YES"
14 buildForArchiving = "YES"
15 buildForAnalyzing = "YES">
16 <BuildableReference
17 BuildableIdentifier = "primary"
18 BlueprintIdentifier = "8B4B28D02F6704280045FA19"
19 BuildableName = "Hutch.app"
20 BlueprintName = "Hutch"
21 ReferencedContainer = "container:Hutch.xcodeproj">
22 </BuildableReference>
23 </BuildActionEntry>
24 </BuildActionEntries>
25 </BuildAction>
26 <TestAction
27 buildConfiguration = "Debug"
28 selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
29 selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
30 shouldUseLaunchSchemeArgsEnv = "YES">
31 <TestPlans>
32 <TestPlanReference
33 reference = "container:HutchTests"
34 default = "YES">
35 </TestPlanReference>
36 </TestPlans>
37 <Testables>
38 <TestableReference
39 skipped = "NO"
40 parallelizable = "YES">
41 <BuildableReference
42 BuildableIdentifier = "primary"
43 BlueprintIdentifier = "8B8182B62F6B73F3000AE049"
44 BuildableName = "HutchTests.xctest"
45 BlueprintName = "HutchTests"
46 ReferencedContainer = "container:Hutch.xcodeproj">
47 </BuildableReference>
48 </TestableReference>
49 </Testables>
50 </TestAction>
51 <LaunchAction
52 buildConfiguration = "Debug"
53 selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
54 selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
55 launchStyle = "0"
56 useCustomWorkingDirectory = "NO"
57 ignoresPersistentStateOnLaunch = "NO"
58 debugDocumentVersioning = "YES"
59 debugServiceExtension = "internal"
60 allowLocationSimulation = "YES">
61 <BuildableProductRunnable
62 runnableDebuggingMode = "0">
63 <BuildableReference
64 BuildableIdentifier = "primary"
65 BlueprintIdentifier = "8B4B28D02F6704280045FA19"
66 BuildableName = "Hutch.app"
67 BlueprintName = "Hutch"
68 ReferencedContainer = "container:Hutch.xcodeproj">
69 </BuildableReference>
70 </BuildableProductRunnable>
71 </LaunchAction>
72 <ProfileAction
73 buildConfiguration = "Release"
74 shouldUseLaunchSchemeArgsEnv = "YES"
75 savedToolIdentifier = ""
76 useCustomWorkingDirectory = "NO"
77 debugDocumentVersioning = "YES">
78 <BuildableProductRunnable
79 runnableDebuggingMode = "0">
80 <BuildableReference
81 BuildableIdentifier = "primary"
82 BlueprintIdentifier = "8B4B28D02F6704280045FA19"
83 BuildableName = "Hutch.app"
84 BlueprintName = "Hutch"
85 ReferencedContainer = "container:Hutch.xcodeproj">
86 </BuildableReference>
87 </BuildableProductRunnable>
88 </ProfileAction>
89 <AnalyzeAction
90 buildConfiguration = "Debug">
91 </AnalyzeAction>
92 <ArchiveAction
93 buildConfiguration = "Release"
94 revealArchiveInOrganizer = "YES">
95 </ArchiveAction>
96</Scheme>
Hutch.xcodeproj/xcshareddata/xcschemes/HutchTests.xcscheme added +69
@@ -0,0 +1,69 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<Scheme
3 LastUpgradeVersion = "2630"
4 version = "1.7">
5 <BuildAction
6 parallelizeBuildables = "YES"
7 buildImplicitDependencies = "YES"
8 buildArchitectures = "Automatic">
9 </BuildAction>
10 <TestAction
11 buildConfiguration = "Debug"
12 selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
13 selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
14 shouldUseLaunchSchemeArgsEnv = "YES">
15 <TestPlans>
16 <TestPlanReference
17 reference = "container:HutchTests.xctestplan"
18 default = "YES">
19 </TestPlanReference>
20 </TestPlans>
21 <Testables>
22 <TestableReference
23 skipped = "NO"
24 parallelizable = "YES">
25 <BuildableReference
26 BuildableIdentifier = "primary"
27 BlueprintIdentifier = "8B8182B62F6B73F3000AE049"
28 BuildableName = "HutchTests.xctest"
29 BlueprintName = "HutchTests"
30 ReferencedContainer = "container:Hutch.xcodeproj">
31 </BuildableReference>
32 </TestableReference>
33 </Testables>
34 </TestAction>
35 <LaunchAction
36 buildConfiguration = "Debug"
37 selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
38 selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
39 launchStyle = "0"
40 useCustomWorkingDirectory = "NO"
41 ignoresPersistentStateOnLaunch = "NO"
42 debugDocumentVersioning = "YES"
43 debugServiceExtension = "internal"
44 allowLocationSimulation = "YES">
45 <MacroExpansion>
46 <BuildableReference
47 BuildableIdentifier = "primary"
48 BlueprintIdentifier = "8B4B28D02F6704280045FA19"
49 BuildableName = "Hutch.app"
50 BlueprintName = "Hutch"
51 ReferencedContainer = "container:Hutch.xcodeproj">
52 </BuildableReference>
53 </MacroExpansion>
54 </LaunchAction>
55 <ProfileAction
56 buildConfiguration = "Release"
57 shouldUseLaunchSchemeArgsEnv = "YES"
58 savedToolIdentifier = ""
59 useCustomWorkingDirectory = "NO"
60 debugDocumentVersioning = "YES">
61 </ProfileAction>
62 <AnalyzeAction
63 buildConfiguration = "Debug">
64 </AnalyzeAction>
65 <ArchiveAction
66 buildConfiguration = "Release"
67 revealArchiveInOrganizer = "YES">
68 </ArchiveAction>
69</Scheme>
Hutch/Networking/SRHTClient.swift +14
@@ -435,6 +435,9 @@ final class SRHTClient: Sendable {
435435 guard let token = _token.withLock({ $0 }), !token.isEmpty else {
436436 throw SRHTError.unauthorized
437437 }
438 guard Self.isTrustedAuthenticatedTextURL(url) else {
439 throw SRHTError.invalidAuthenticatedURL(url)
440 }
438441
439442 var request = URLRequest(url: url)
440443 request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
@@ -512,6 +515,17 @@ final class SRHTClient: Sendable {
512515
513516// MARK: - Data Helper
514517
518private extension SRHTClient {
519 static func isTrustedAuthenticatedTextURL(_ url: URL) -> Bool {
520 guard url.scheme?.localizedCaseInsensitiveCompare("https") == .orderedSame,
521 let host = url.host?.lowercased() else {
522 return false
523 }
524
525 return host.hasSuffix(".sr.ht")
526 }
527}
528
515529private extension Data {
516530 mutating func append(_ string: String) {
517531 if let data = string.data(using: .utf8) {
Hutch/Networking/SRHTError.swift +4
@@ -6,6 +6,8 @@ enum SRHTError: LocalizedError, Sendable {
66 case graphQLErrors([GraphQLError])
77 /// The HTTP response had a non-2xx status code.
88 case httpError(Int)
9 /// The client refused to send credentials to an unexpected URL.
10 case invalidAuthenticatedURL(URL)
911 /// The response data could not be decoded.
1012 case decodingError(any Error)
1113 /// A networking error from URLSession (timeout, DNS, connectivity, etc.).
@@ -20,6 +22,8 @@ enum SRHTError: LocalizedError, Sendable {
2022 return "GraphQL error: \(messages)"
2123 case .httpError(let code):
2224 return "Server returned HTTP \(code)."
25 case .invalidAuthenticatedURL(let url):
26 return "Refused to authenticate request to unexpected URL: \(url.absoluteString)"
2327 case .decodingError(let error):
2428 return "Failed to decode response: \(error.localizedDescription)"
2529 case .networkError(let error):
HutchTests.xctestplan added +30
@@ -0,0 +1,30 @@
1{
2 "configurations" : [
3 {
4 "id" : "06A589A5-80C7-41FE-911F-03518252CBDD",
5 "name" : "Test Scheme Action",
6 "options" : {
7
8 }
9 }
10 ],
11 "defaultOptions" : {
12 "performanceAntipatternCheckerEnabled" : true,
13 "targetForVariableExpansion" : {
14 "containerPath" : "container:Hutch.xcodeproj",
15 "identifier" : "8B4B28D02F6704280045FA19",
16 "name" : "Hutch"
17 }
18 },
19 "testTargets" : [
20 {
21 "parallelizable" : true,
22 "target" : {
23 "containerPath" : "container:Hutch.xcodeproj",
24 "identifier" : "8B8182B62F6B73F3000AE049",
25 "name" : "HutchTests"
26 }
27 }
28 ],
29 "version" : 1
30}
HutchTests/HutchTests.swift added +16
@@ -0,0 +1,16 @@
1//
2// HutchTests.swift
3// HutchTests
4//
5// Created by cmc on 2026-03-18.
6//
7
8import Testing
9
10struct HutchTests {
11
12 @Test func example() async throws {
13 // Write your test here and use APIs like `#expect(...)` to check expected conditions.
14 }
15
16}
HutchTests/SRHTClientTests.swift added +27
@@ -0,0 +1,27 @@
1import Foundation
2import Testing
3@testable import Hutch
4
5struct SRHTClientTests {
6
7 @Test
8 @MainActor
9 func fetchTextRejectsUnexpectedAuthenticatedURL() async throws {
10 let client = SRHTClient(token: "test-token")
11 let url = try #require(URL(string: "https://example.com/build-log"))
12
13 do {
14 _ = try await client.fetchText(url: url)
15 Issue.record("Expected fetchText(url:) to reject non-sr.ht URLs.")
16 } catch let error as SRHTError {
17 guard case .invalidAuthenticatedURL(let rejectedURL) = error else {
18 Issue.record("Expected invalidAuthenticatedURL error, got \(error).")
19 return
20 }
21
22 #expect(rejectedURL == url)
23 } catch {
24 Issue.record("Expected SRHTError.invalidAuthenticatedURL, got \(error).")
25 }
26 }
27}