summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Hutch.xcodeproj/project.pbxproj130
-rw-r--r--Hutch.xcodeproj/xcshareddata/xcschemes/Hutch.xcscheme96
-rw-r--r--Hutch.xcodeproj/xcshareddata/xcschemes/HutchTests.xcscheme69
-rw-r--r--Hutch/Networking/SRHTClient.swift14
-rw-r--r--Hutch/Networking/SRHTError.swift4
-rw-r--r--HutchTests.xctestplan30
-rw-r--r--HutchTests/HutchTests.swift16
-rw-r--r--HutchTests/SRHTClientTests.swift27
8 files changed, 386 insertions, 0 deletions
diff --git a/Hutch.xcodeproj/project.pbxproj b/Hutch.xcodeproj/project.pbxproj
index 8aab8bb..3cd0646 100644
--- a/Hutch.xcodeproj/project.pbxproj
+++ b/Hutch.xcodeproj/project.pbxproj
@@ -13,12 +13,25 @@
8B2F89682F69DEB900FC0253 /* SECURITY.md in Resources */ = {isa = PBXBuildFile; fileRef = 8B2F89652F69DEB900FC0253 /* SECURITY.md */; };
/* End PBXBuildFile section */
+/* Begin PBXContainerItemProxy section */
+ 8B8182BB2F6B73F3000AE049 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 8B4B28C92F6704280045FA19 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 8B4B28D02F6704280045FA19;
+ remoteInfo = Hutch;
+ };
+/* End PBXContainerItemProxy section */
+
/* Begin PBXFileReference section */
8B2F89612F69DEA900FC0253 /* TODO.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = TODO.md; sourceTree = "<group>"; };
8B2F89632F69DEB900FC0253 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
8B2F89642F69DEB900FC0253 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
8B2F89652F69DEB900FC0253 /* SECURITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SECURITY.md; sourceTree = "<group>"; };
8B4B28D12F6704280045FA19 /* Hutch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Hutch.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 8B8182B72F6B73F3000AE049 /* HutchTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HutchTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 8B8182C22F6B742B000AE049 /* HutchTests */ = {isa = PBXFileReference; lastKnownFileType = folder; path = HutchTests; sourceTree = "<group>"; };
+ 8BDCA5272F6B76B20066AA29 /* HutchTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = HutchTests.xctestplan; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -40,6 +53,11 @@
path = Hutch;
sourceTree = "<group>";
};
+ 8B8182B82F6B73F3000AE049 /* HutchTests */ = {
+ isa = PBXFileSystemSynchronizedRootGroup;
+ path = HutchTests;
+ sourceTree = "<group>";
+ };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
@@ -50,17 +68,27 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 8B8182B42F6B73F3000AE049 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
8B4B28C82F6704280045FA19 = {
isa = PBXGroup;
children = (
+ 8BDCA5272F6B76B20066AA29 /* HutchTests.xctestplan */,
+ 8B8182C22F6B742B000AE049 /* HutchTests */,
8B2F89632F69DEB900FC0253 /* LICENSE */,
8B2F89642F69DEB900FC0253 /* README.md */,
8B2F89652F69DEB900FC0253 /* SECURITY.md */,
8B2F89612F69DEA900FC0253 /* TODO.md */,
8B4B28D32F6704280045FA19 /* Hutch */,
+ 8B8182B82F6B73F3000AE049 /* HutchTests */,
8B4B28D22F6704280045FA19 /* Products */,
);
sourceTree = "<group>";
@@ -69,6 +97,7 @@
isa = PBXGroup;
children = (
8B4B28D12F6704280045FA19 /* Hutch.app */,
+ 8B8182B72F6B73F3000AE049 /* HutchTests.xctest */,
);
name = Products;
sourceTree = "<group>";
@@ -98,6 +127,29 @@
productReference = 8B4B28D12F6704280045FA19 /* Hutch.app */;
productType = "com.apple.product-type.application";
};
+ 8B8182B62F6B73F3000AE049 /* HutchTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 8B8182BF2F6B73F3000AE049 /* Build configuration list for PBXNativeTarget "HutchTests" */;
+ buildPhases = (
+ 8B8182B32F6B73F3000AE049 /* Sources */,
+ 8B8182B42F6B73F3000AE049 /* Frameworks */,
+ 8B8182B52F6B73F3000AE049 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 8B8182BC2F6B73F3000AE049 /* PBXTargetDependency */,
+ );
+ fileSystemSynchronizedGroups = (
+ 8B8182B82F6B73F3000AE049 /* HutchTests */,
+ );
+ name = HutchTests;
+ packageProductDependencies = (
+ );
+ productName = HutchTests;
+ productReference = 8B8182B72F6B73F3000AE049 /* HutchTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -111,6 +163,10 @@
8B4B28D02F6704280045FA19 = {
CreatedOnToolsVersion = 26.3;
};
+ 8B8182B62F6B73F3000AE049 = {
+ CreatedOnToolsVersion = 26.3;
+ TestTargetID = 8B4B28D02F6704280045FA19;
+ };
};
};
buildConfigurationList = 8B4B28CC2F6704280045FA19 /* Build configuration list for PBXProject "Hutch" */;
@@ -128,6 +184,7 @@
projectRoot = "";
targets = (
8B4B28D02F6704280045FA19 /* Hutch */,
+ 8B8182B62F6B73F3000AE049 /* HutchTests */,
);
};
/* End PBXProject section */
@@ -144,6 +201,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 8B8182B52F6B73F3000AE049 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -154,8 +218,23 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 8B8182B32F6B73F3000AE049 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXSourcesBuildPhase section */
+/* Begin PBXTargetDependency section */
+ 8B8182BC2F6B73F3000AE049 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 8B4B28D02F6704280045FA19 /* Hutch */;
+ targetProxy = 8B8182BB2F6B73F3000AE049 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
/* Begin XCBuildConfiguration section */
8B4B28DA2F6704290045FA19 /* Debug */ = {
isa = XCBuildConfiguration;
@@ -350,6 +429,48 @@
};
name = Release;
};
+ 8B8182BD2F6B73F3000AE049 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = ZCNAX3VL9D;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.HutchTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ STRING_CATALOG_GENERATE_SYMBOLS = NO;
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hutch.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Hutch";
+ };
+ name = Debug;
+ };
+ 8B8182BE2F6B73F3000AE049 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = ZCNAX3VL9D;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.HutchTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ STRING_CATALOG_GENERATE_SYMBOLS = NO;
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hutch.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Hutch";
+ };
+ name = Release;
+ };
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -371,6 +492,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
+ 8B8182BF2F6B73F3000AE049 /* Build configuration list for PBXNativeTarget "HutchTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 8B8182BD2F6B73F3000AE049 /* Debug */,
+ 8B8182BE2F6B73F3000AE049 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
/* End XCConfigurationList section */
};
rootObject = 8B4B28C92F6704280045FA19 /* Project object */;
diff --git a/Hutch.xcodeproj/xcshareddata/xcschemes/Hutch.xcscheme b/Hutch.xcodeproj/xcshareddata/xcschemes/Hutch.xcscheme
new file mode 100644
index 0000000..4bb83be
--- /dev/null
+++ b/Hutch.xcodeproj/xcshareddata/xcschemes/Hutch.xcscheme
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "2630"
+ version = "1.7">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES"
+ buildArchitectures = "Automatic">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "8B4B28D02F6704280045FA19"
+ BuildableName = "Hutch.app"
+ BlueprintName = "Hutch"
+ ReferencedContainer = "container:Hutch.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <TestPlans>
+ <TestPlanReference
+ reference = "container:HutchTests"
+ default = "YES">
+ </TestPlanReference>
+ </TestPlans>
+ <Testables>
+ <TestableReference
+ skipped = "NO"
+ parallelizable = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "8B8182B62F6B73F3000AE049"
+ BuildableName = "HutchTests.xctest"
+ BlueprintName = "HutchTests"
+ ReferencedContainer = "container:Hutch.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "8B4B28D02F6704280045FA19"
+ BuildableName = "Hutch.app"
+ BlueprintName = "Hutch"
+ ReferencedContainer = "container:Hutch.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "8B4B28D02F6704280045FA19"
+ BuildableName = "Hutch.app"
+ BlueprintName = "Hutch"
+ ReferencedContainer = "container:Hutch.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Hutch.xcodeproj/xcshareddata/xcschemes/HutchTests.xcscheme b/Hutch.xcodeproj/xcshareddata/xcschemes/HutchTests.xcscheme
new file mode 100644
index 0000000..909ebd2
--- /dev/null
+++ b/Hutch.xcodeproj/xcshareddata/xcschemes/HutchTests.xcscheme
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "2630"
+ version = "1.7">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES"
+ buildArchitectures = "Automatic">
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <TestPlans>
+ <TestPlanReference
+ reference = "container:HutchTests.xctestplan"
+ default = "YES">
+ </TestPlanReference>
+ </TestPlans>
+ <Testables>
+ <TestableReference
+ skipped = "NO"
+ parallelizable = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "8B8182B62F6B73F3000AE049"
+ BuildableName = "HutchTests.xctest"
+ BlueprintName = "HutchTests"
+ ReferencedContainer = "container:Hutch.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "8B4B28D02F6704280045FA19"
+ BuildableName = "Hutch.app"
+ BlueprintName = "Hutch"
+ ReferencedContainer = "container:Hutch.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Hutch/Networking/SRHTClient.swift b/Hutch/Networking/SRHTClient.swift
index a24fe24..6032cab 100644
--- a/Hutch/Networking/SRHTClient.swift
+++ b/Hutch/Networking/SRHTClient.swift
@@ -435,6 +435,9 @@ final class SRHTClient: Sendable {
guard let token = _token.withLock({ $0 }), !token.isEmpty else {
throw SRHTError.unauthorized
}
+ guard Self.isTrustedAuthenticatedTextURL(url) else {
+ throw SRHTError.invalidAuthenticatedURL(url)
+ }
var request = URLRequest(url: url)
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
@@ -512,6 +515,17 @@ final class SRHTClient: Sendable {
// MARK: - Data Helper
+private extension SRHTClient {
+ static func isTrustedAuthenticatedTextURL(_ url: URL) -> Bool {
+ guard url.scheme?.localizedCaseInsensitiveCompare("https") == .orderedSame,
+ let host = url.host?.lowercased() else {
+ return false
+ }
+
+ return host.hasSuffix(".sr.ht")
+ }
+}
+
private extension Data {
mutating func append(_ string: String) {
if let data = string.data(using: .utf8) {
diff --git a/Hutch/Networking/SRHTError.swift b/Hutch/Networking/SRHTError.swift
index f148c00..f2da408 100644
--- a/Hutch/Networking/SRHTError.swift
+++ b/Hutch/Networking/SRHTError.swift
@@ -6,6 +6,8 @@ enum SRHTError: LocalizedError, Sendable {
case graphQLErrors([GraphQLError])
/// The HTTP response had a non-2xx status code.
case httpError(Int)
+ /// The client refused to send credentials to an unexpected URL.
+ case invalidAuthenticatedURL(URL)
/// The response data could not be decoded.
case decodingError(any Error)
/// A networking error from URLSession (timeout, DNS, connectivity, etc.).
@@ -20,6 +22,8 @@ enum SRHTError: LocalizedError, Sendable {
return "GraphQL error: \(messages)"
case .httpError(let code):
return "Server returned HTTP \(code)."
+ case .invalidAuthenticatedURL(let url):
+ return "Refused to authenticate request to unexpected URL: \(url.absoluteString)"
case .decodingError(let error):
return "Failed to decode response: \(error.localizedDescription)"
case .networkError(let error):
diff --git a/HutchTests.xctestplan b/HutchTests.xctestplan
new file mode 100644
index 0000000..8af3d1b
--- /dev/null
+++ b/HutchTests.xctestplan
@@ -0,0 +1,30 @@
+{
+ "configurations" : [
+ {
+ "id" : "06A589A5-80C7-41FE-911F-03518252CBDD",
+ "name" : "Test Scheme Action",
+ "options" : {
+
+ }
+ }
+ ],
+ "defaultOptions" : {
+ "performanceAntipatternCheckerEnabled" : true,
+ "targetForVariableExpansion" : {
+ "containerPath" : "container:Hutch.xcodeproj",
+ "identifier" : "8B4B28D02F6704280045FA19",
+ "name" : "Hutch"
+ }
+ },
+ "testTargets" : [
+ {
+ "parallelizable" : true,
+ "target" : {
+ "containerPath" : "container:Hutch.xcodeproj",
+ "identifier" : "8B8182B62F6B73F3000AE049",
+ "name" : "HutchTests"
+ }
+ }
+ ],
+ "version" : 1
+}
diff --git a/HutchTests/HutchTests.swift b/HutchTests/HutchTests.swift
new file mode 100644
index 0000000..c681efa
--- /dev/null
+++ b/HutchTests/HutchTests.swift
@@ -0,0 +1,16 @@
+//
+// HutchTests.swift
+// HutchTests
+//
+// Created by cmc on 2026-03-18.
+//
+
+import Testing
+
+struct HutchTests {
+
+ @Test func example() async throws {
+ // Write your test here and use APIs like `#expect(...)` to check expected conditions.
+ }
+
+}
diff --git a/HutchTests/SRHTClientTests.swift b/HutchTests/SRHTClientTests.swift
new file mode 100644
index 0000000..2ce2529
--- /dev/null
+++ b/HutchTests/SRHTClientTests.swift
@@ -0,0 +1,27 @@
+import Foundation
+import Testing
+@testable import Hutch
+
+struct SRHTClientTests {
+
+ @Test
+ @MainActor
+ func fetchTextRejectsUnexpectedAuthenticatedURL() async throws {
+ let client = SRHTClient(token: "test-token")
+ let url = try #require(URL(string: "https://example.com/build-log"))
+
+ do {
+ _ = try await client.fetchText(url: url)
+ Issue.record("Expected fetchText(url:) to reject non-sr.ht URLs.")
+ } catch let error as SRHTError {
+ guard case .invalidAuthenticatedURL(let rejectedURL) = error else {
+ Issue.record("Expected invalidAuthenticatedURL error, got \(error).")
+ return
+ }
+
+ #expect(rejectedURL == url)
+ } catch {
+ Issue.record("Expected SRHTError.invalidAuthenticatedURL, got \(error).")
+ }
+ }
+}