Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions OpenVision/Services/AIBackend/HermesService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,13 +184,19 @@ final class HermesService: ObservableObject {
}

/// The address with a scheme, for one typed without (`myhost.ts.net`, `100.88.1.2:8642`):
/// http for a local or Tailscale IP (servers there rarely have a certificate), https otherwise.
/// http for a local host or Tailscale IP (servers there rarely have a certificate), for a
/// dot-less name (`nuc`, almost always a LAN machine), and for a `*.ts.net` name with a port
/// (`tailscale serve` answers https on 443, so a port means the server itself); https
/// otherwise. Only a guess: a dot-less name could also resolve through a search domain, so
/// it still gets the plain-http warning and can't sign in over http.
nonisolated static func withScheme(_ address: String) -> String {
let text = address.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty, !text.contains("://") else { return text }
let hostAndPort = text.split(separator: "/", maxSplits: 1).first.map(String.init) ?? text
let host = hostAndPort.split(separator: ":").first.map(String.init) ?? hostAndPort
return (isLocalHost(host) ? "http://" : "https://") + text
let tailnetPort = host.lowercased().hasSuffix(".ts.net") && hostAndPort.contains(":")
let singleLabel = !host.contains(".") && !host.contains(":")
return (isLocalHost(host) || singleLabel || tailnetPort ? "http://" : "https://") + text
}

private static func authorize(_ request: inout URLRequest, key: String, sessionKey: String?) {
Expand Down
4 changes: 2 additions & 2 deletions OpenVision/Views/Settings/HermesSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ struct HermesSettingsView: View {
@ViewBuilder
private var apiKeySections: some View {
Section {
TextField("https://hermes.example.com", text: $serverURL)
TextField("hermes.example.com", text: $serverURL)
.autocapitalization(.none)
.autocorrectionDisabled()
.keyboardType(.URL)
Expand Down Expand Up @@ -137,7 +137,7 @@ struct HermesSettingsView: View {
@ViewBuilder
private var passwordSections: some View {
Section {
TextField("https://hermes.example.com", text: $dashboardURL)
TextField("hermes.example.com", text: $dashboardURL)
.autocapitalization(.none)
.autocorrectionDisabled()
.keyboardType(.URL)
Expand Down
11 changes: 11 additions & 0 deletions OpenVisionTests/HermesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ final class HermesTests: XCTestCase {
XCTAssertEqual(HermesService.withScheme("http://8.8.8.8"), "http://8.8.8.8", "a typed scheme is kept")
}

func testDotlessAndTailnetPortAddressesUsePlainHTTP() {
XCTAssertEqual(HermesService.apiBase(from: "nuc:8642")?.absoluteString, "http://nuc:8642/v1")
XCTAssertEqual(HermesService.apiBase(from: "myhost.tailnet.ts.net:8642")?.absoluteString,
"http://myhost.tailnet.ts.net:8642/v1", "a port means not tailscale serve")
// The guess isn't trust: a dot-less name may resolve through a search domain.
XCTAssertTrue(HermesService.isUnencryptedRemote("nuc:8642"), "still warns before sending the API key")
XCTAssertNil(HermesDashboard.base(from: "hermes-box"), "no plain-http sign-in on a dot-less name")
XCTAssertFalse(HermesService.isUnencryptedRemote("http://localhost"))
XCTAssertTrue(HermesService.isUnencryptedRemote("http://hermes.example.com"), "public hosts still warn")
}

func testAPIBaseRejectsNonHTTP() {
XCTAssertNil(HermesService.apiBase(from: ""))
XCTAssertNil(HermesService.apiBase(from: "ftp://hermes.example.com"))
Expand Down
Loading