Merge pull request #206 from manaflow-ai/issue-180-http-nonsecure-hosts

Allow HTTP in built-in browser for local dev hosts
This commit is contained in:
Lawrence Chen 2026-02-20 15:39:22 -08:00 committed by GitHub
commit ea0593475c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 554 additions and 10 deletions

View file

@ -1425,6 +1425,39 @@ final class OmnibarSuggestionRankingTests: XCTestCase {
XCTAssertEqual(results.first?.completion, "https://gmail.com/")
}
func testNavigateSuggestionRanksAheadOfSwitchToTabForSameResolvedURL() throws {
let targetURL = try XCTUnwrap(URL(string: "http://http.badssl.com/"))
let results = buildOmnibarSuggestions(
query: targetURL.absoluteString,
engineName: "Google",
historyEntries: [],
openTabMatches: [
.init(
tabId: UUID(),
panelId: UUID(),
url: targetURL.absoluteString,
title: "http.badssl.com",
isKnownOpenTab: true
),
],
remoteQueries: [],
resolvedURL: targetURL,
limit: 8,
now: fixedNow
)
guard let first = results.first else {
XCTFail("Expected at least one suggestion")
return
}
guard case .navigate(let navigateURL) = first.kind else {
XCTFail("Expected first suggestion to be navigate, got \(first.kind)")
return
}
XCTAssertEqual(navigateURL, targetURL.absoluteString)
}
func testSuggestionSelectionPrefersAutocompletionCandidateAfterSuggestionsUpdate() {
let entries: [BrowserHistoryStore.Entry] = [
.init(

View file

@ -1,5 +1,7 @@
import XCTest
import Foundation
import AppKit
@testable import cmux_DEV
/// Regression test: ensures UpdatePill is never gated behind #if DEBUG in production code paths.
/// This prevents accidentally hiding the update UI in Release builds.
@ -64,3 +66,133 @@ final class UpdatePillReleaseVisibilityTests: XCTestCase {
return URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
}
}
/// Regression test: ensure WKWebView can load HTTP development URLs (e.g. *.localtest.me).
final class AppTransportSecurityTests: XCTestCase {
func testInfoPlistAllowsArbitraryLoadsInWebContent() throws {
let projectRoot = findProjectRoot()
let infoPlistURL = projectRoot.appendingPathComponent("Resources/Info.plist")
let data = try Data(contentsOf: infoPlistURL)
var format = PropertyListSerialization.PropertyListFormat.xml
let plist = try XCTUnwrap(
PropertyListSerialization.propertyList(from: data, options: [], format: &format) as? [String: Any]
)
let ats = try XCTUnwrap(plist["NSAppTransportSecurity"] as? [String: Any])
XCTAssertEqual(
ats["NSAllowsArbitraryLoadsInWebContent"] as? Bool,
true,
"Resources/Info.plist must allow HTTP loads in WKWebView for local dev hostnames."
)
}
private func findProjectRoot() -> URL {
var dir = URL(fileURLWithPath: #file).deletingLastPathComponent().deletingLastPathComponent()
for _ in 0..<10 {
let marker = dir.appendingPathComponent("GhosttyTabs.xcodeproj")
if FileManager.default.fileExists(atPath: marker.path) {
return dir
}
dir = dir.deletingLastPathComponent()
}
return URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
}
}
final class BrowserInsecureHTTPSettingsTests: XCTestCase {
func testDefaultAllowlistPatternsArePresent() {
XCTAssertEqual(
BrowserInsecureHTTPSettings.normalizedAllowlistPatterns(rawValue: nil),
["127.0.0.1", "localhost", "*.localtest.me"]
)
}
func testWildcardAndExactHostMatching() {
XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("localhost", rawAllowlist: nil))
XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("api.localtest.me", rawAllowlist: nil))
XCTAssertFalse(BrowserInsecureHTTPSettings.isHostAllowed("neverssl.com", rawAllowlist: nil))
}
func testCustomAllowlistNormalizesAndDeduplicatesEntries() {
let raw = """
localhost
*.example.com
127.0.0.1
https://dev.internal:8080/path
*.example.com
"""
XCTAssertEqual(
BrowserInsecureHTTPSettings.normalizedAllowlistPatterns(rawValue: raw),
["localhost", "*.example.com", "127.0.0.1", "dev.internal"]
)
XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("foo.example.com", rawAllowlist: raw))
XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("dev.internal", rawAllowlist: raw))
XCTAssertFalse(BrowserInsecureHTTPSettings.isHostAllowed("example.net", rawAllowlist: raw))
}
func testBlockDecisionUsesAllowlistAndSchemeRules() throws {
let localURL = try XCTUnwrap(URL(string: "http://foo.localtest.me:3000"))
XCTAssertFalse(browserShouldBlockInsecureHTTPURL(localURL, rawAllowlist: nil))
let insecureURL = try XCTUnwrap(URL(string: "http://neverssl.com"))
XCTAssertTrue(browserShouldBlockInsecureHTTPURL(insecureURL, rawAllowlist: nil))
let httpsURL = try XCTUnwrap(URL(string: "https://neverssl.com"))
XCTAssertFalse(browserShouldBlockInsecureHTTPURL(httpsURL, rawAllowlist: nil))
}
func testOneTimeBypassIsConsumedAfterFirstNavigation() throws {
let insecureURL = try XCTUnwrap(URL(string: "http://neverssl.com"))
var bypassHostOnce: String? = "neverssl.com"
XCTAssertTrue(browserShouldConsumeOneTimeInsecureHTTPBypass(
insecureURL,
bypassHostOnce: &bypassHostOnce
))
XCTAssertNil(bypassHostOnce)
// Subsequent visits should prompt again unless host was saved.
XCTAssertFalse(browserShouldConsumeOneTimeInsecureHTTPBypass(
insecureURL,
bypassHostOnce: &bypassHostOnce
))
XCTAssertTrue(browserShouldBlockInsecureHTTPURL(insecureURL, rawAllowlist: nil))
}
func testAddAllowedHostPersistsToDefaultsAndUnblocksHTTP() throws {
let suiteName = "BrowserInsecureHTTPSettingsTests.Persist.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {
XCTFail("Failed to create isolated UserDefaults suite")
return
}
defer { defaults.removePersistentDomain(forName: suiteName) }
let url = try XCTUnwrap(URL(string: "http://persist-me.test"))
XCTAssertTrue(browserShouldBlockInsecureHTTPURL(url, defaults: defaults))
BrowserInsecureHTTPSettings.addAllowedHost("persist-me.test", defaults: defaults)
let persisted = defaults.string(forKey: BrowserInsecureHTTPSettings.allowlistKey)
XCTAssertNotNil(persisted)
XCTAssertTrue(BrowserInsecureHTTPSettings.isHostAllowed("persist-me.test", defaults: defaults))
XCTAssertFalse(browserShouldBlockInsecureHTTPURL(url, defaults: defaults))
}
func testAllowlistSelectionPersistsForProceedAndOpenExternal() {
XCTAssertTrue(browserShouldPersistInsecureHTTPAllowlistSelection(
response: .alertFirstButtonReturn,
suppressionEnabled: true
))
XCTAssertTrue(browserShouldPersistInsecureHTTPAllowlistSelection(
response: .alertSecondButtonReturn,
suppressionEnabled: true
))
XCTAssertFalse(browserShouldPersistInsecureHTTPAllowlistSelection(
response: .alertThirdButtonReturn,
suppressionEnabled: true
))
XCTAssertFalse(browserShouldPersistInsecureHTTPAllowlistSelection(
response: .alertSecondButtonReturn,
suppressionEnabled: false
))
}
}