mirror of
https://github.com/Such-Software/adaptive-multi-protocol-browser.git
synced 2026-10-19 07:18:32 +02:00
Keep transports in one browser window
This commit is contained in:
@@ -1,41 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from .config import AppConfig
|
||||
from .launch import OpenPlan, prepare_open
|
||||
from .open_runner import launch_open_plan
|
||||
from .routing import route_url
|
||||
|
||||
|
||||
BROKER_TRANSPORTS = {"tor", "i2p"}
|
||||
|
||||
|
||||
class BrokerRouteError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def open_brokered_url(
|
||||
raw_url: str,
|
||||
*,
|
||||
expected_transport: str,
|
||||
consent: bool,
|
||||
config: AppConfig,
|
||||
root: Path,
|
||||
) -> OpenPlan:
|
||||
route = route_url(raw_url)
|
||||
if route.transport != expected_transport:
|
||||
raise BrokerRouteError(
|
||||
f"route transport {route.transport} does not match requested transport {expected_transport}"
|
||||
)
|
||||
if route.transport not in BROKER_TRANSPORTS:
|
||||
raise BrokerRouteError(f"{route.transport} is not supported by the desktop broker yet")
|
||||
|
||||
open_plan = prepare_open(
|
||||
route.normalized,
|
||||
consent=consent,
|
||||
dry_run=False,
|
||||
config=config,
|
||||
platform="desktop",
|
||||
)
|
||||
return launch_open_plan(open_plan, config=config, root=root)
|
||||
+4
-4
@@ -45,7 +45,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"--route-aware",
|
||||
dest="broker",
|
||||
action="store_true",
|
||||
help="Launch a clearnet broker that hands Tor and I2P routes to isolated profiles.",
|
||||
help="Launch one window with isolated clearnet, Tor, and I2P container tabs.",
|
||||
)
|
||||
open_mode = open_parser.add_mutually_exclusive_group()
|
||||
open_mode.add_argument(
|
||||
@@ -57,7 +57,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
open_mode.add_argument(
|
||||
"--launch",
|
||||
action="store_true",
|
||||
help="Create the isolated profile and launch the bundled browser when the route is ready.",
|
||||
help="Create AMPB-owned browser state and launch the bundled browser when the route is ready.",
|
||||
)
|
||||
|
||||
shell_parser = subcommands.add_parser("shell", help="Launch the desktop shell flow.")
|
||||
@@ -354,8 +354,8 @@ def _cmd_fixture(args) -> int:
|
||||
f"url={check.url} "
|
||||
f"expected_transport={check.expected_transport} "
|
||||
f"actual_transport={check.actual_transport} "
|
||||
f"expected_profile={check.expected_profile} "
|
||||
f"actual_profile={check.actual_profile} "
|
||||
f"expected_context={check.expected_context} "
|
||||
f"actual_context={check.actual_context} "
|
||||
f"expected_isolation={check.expected_isolation} "
|
||||
f"actual_isolation={check.actual_isolation} "
|
||||
f"tier={check.tier} "
|
||||
|
||||
+14
-10
@@ -9,7 +9,7 @@ from .interaction_policy import interaction_failures
|
||||
from .routing import route_url
|
||||
|
||||
|
||||
SUPPORTED_SCHEMAS = {"ampg.fixture-manifest.v1"}
|
||||
SUPPORTED_SCHEMAS = {"ampg.fixture-manifest.v1", "ampg.fixture-manifest.v2"}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -21,8 +21,8 @@ class FixtureCheck:
|
||||
url: str
|
||||
expected_transport: str
|
||||
actual_transport: str
|
||||
expected_profile: str
|
||||
actual_profile: str
|
||||
expected_context: str
|
||||
actual_context: str
|
||||
expected_isolation: str
|
||||
actual_isolation: str
|
||||
tier: str
|
||||
@@ -79,18 +79,22 @@ def _check_fixture(site_id: str, fixture: Any, path: Path) -> FixtureCheck:
|
||||
if not isinstance(checks, dict):
|
||||
raise ValueError(f"{path}: {protocol}: missing checks table")
|
||||
expected_transport = _string(checks, "transport")
|
||||
expected_profile = _string(checks, "profile")
|
||||
expected_isolation = _optional_string(checks, "isolation", "transport-profile")
|
||||
expected_context = (
|
||||
_string(checks, "context")
|
||||
if "context" in checks
|
||||
else _string(checks, "profile")
|
||||
)
|
||||
expected_isolation = _optional_string(checks, "isolation", "transport-context")
|
||||
interaction = _interaction_policy(fixture)
|
||||
route_metadata = _route_metadata(fixture)
|
||||
|
||||
route = route_url(url)
|
||||
actual_isolation = "transport-profile" if route.profile == route.transport else "shared-profile"
|
||||
actual_isolation = "transport-context"
|
||||
failures: list[str] = []
|
||||
if route.transport != expected_transport:
|
||||
failures.append(f"transport expected {expected_transport} got {route.transport}")
|
||||
if route.profile != expected_profile:
|
||||
failures.append(f"profile expected {expected_profile} got {route.profile}")
|
||||
if route.profile != expected_context:
|
||||
failures.append(f"context expected {expected_context} got {route.profile}")
|
||||
if actual_isolation != expected_isolation:
|
||||
failures.append(f"isolation expected {expected_isolation} got {actual_isolation}")
|
||||
failures.extend(
|
||||
@@ -112,8 +116,8 @@ def _check_fixture(site_id: str, fixture: Any, path: Path) -> FixtureCheck:
|
||||
url=route.normalized,
|
||||
expected_transport=expected_transport,
|
||||
actual_transport=route.transport,
|
||||
expected_profile=expected_profile,
|
||||
actual_profile=route.profile,
|
||||
expected_context=expected_context,
|
||||
actual_context=route.profile,
|
||||
expected_isolation=expected_isolation,
|
||||
actual_isolation=actual_isolation,
|
||||
tier=interaction["tier"],
|
||||
|
||||
+222
-147
@@ -107,7 +107,7 @@ def prepare_open(
|
||||
profile_path=config.profile_path(BROKER_PROFILE),
|
||||
proxy="-",
|
||||
setup_steps=(),
|
||||
message="broker entry URLs must use clearnet; alternate routes open in their isolated profile",
|
||||
message="broker entry URLs must use clearnet; alternate routes continue in isolated container tabs",
|
||||
broker=True,
|
||||
)
|
||||
profile_name = (
|
||||
@@ -178,7 +178,7 @@ def prepare_open(
|
||||
profile_path=profile_path,
|
||||
proxy=proxy,
|
||||
setup_steps=(),
|
||||
message="ready to open with isolated profile",
|
||||
message="ready to open in the one-window broker" if broker else "ready to open with isolated profile",
|
||||
broker=broker,
|
||||
launch_spec=launch_spec,
|
||||
)
|
||||
@@ -415,6 +415,14 @@ def _prefs_for(browse_plan: BrowsePlan, *, broker: bool = False) -> tuple[str, .
|
||||
prefs.extend(
|
||||
[
|
||||
("network.proxy.type", 0),
|
||||
("network.http.speculative-parallel-limit", 0),
|
||||
("network.prefetch-next", False),
|
||||
("network.dns.disablePrefetchFromHTTPS", True),
|
||||
("browser.urlbar.speculativeConnect.enabled", False),
|
||||
("media.peerconnection.enabled", False),
|
||||
("network.http.http3.enable", False),
|
||||
("network.trr.mode", 5),
|
||||
("privacy.userContext.enabled", True),
|
||||
("extensions.autoDisableScopes", 0),
|
||||
("extensions.enabledScopes", 5),
|
||||
("xpinstall.signatures.required", False),
|
||||
@@ -461,8 +469,6 @@ def _route_helper_extension_assets(extension_path: str) -> tuple[ProfileAsset, .
|
||||
ProfileAsset(str(base / "setup.html"), _route_helper_setup_html()),
|
||||
ProfileAsset(str(base / "setup.js"), _route_helper_setup_js()),
|
||||
ProfileAsset(str(base / "setup.css"), _route_helper_setup_css()),
|
||||
ProfileAsset(str(base / "handoff.html"), _route_helper_handoff_html()),
|
||||
ProfileAsset(str(base / "handoff.js"), _route_helper_handoff_js()),
|
||||
)
|
||||
|
||||
|
||||
@@ -470,22 +476,20 @@ def _route_helper_manifest() -> str:
|
||||
return json.dumps(
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "AMPB Route Helper",
|
||||
"name": "AMPB Transport Contexts",
|
||||
"version": "0.1.0",
|
||||
"description": "Hands alternate-network links to isolated AMPB transport profiles.",
|
||||
"applications": {"gecko": {"id": ROUTE_HELPER_EXTENSION_ID}},
|
||||
"description": "Keeps AMPB transports in isolated, fail-closed container tabs.",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {"id": ROUTE_HELPER_EXTENSION_ID, "strict_min_version": "128.0"}
|
||||
},
|
||||
"permissions": [
|
||||
"tabs",
|
||||
"cookies",
|
||||
"contextualIdentities",
|
||||
"proxy",
|
||||
"webRequest",
|
||||
"webRequestBlocking",
|
||||
"http://*.onion/*",
|
||||
"https://*.onion/*",
|
||||
"ws://*.onion/*",
|
||||
"wss://*.onion/*",
|
||||
"http://*.i2p/*",
|
||||
"https://*.i2p/*",
|
||||
"ws://*.i2p/*",
|
||||
"wss://*.i2p/*",
|
||||
"<all_urls>",
|
||||
"http://127.0.0.1/*",
|
||||
],
|
||||
"background": {"scripts": ["background.js"]},
|
||||
@@ -496,86 +500,197 @@ def _route_helper_manifest() -> str:
|
||||
|
||||
|
||||
def _route_helper_background_js() -> str:
|
||||
return "\n".join(
|
||||
(
|
||||
'"use strict";',
|
||||
"",
|
||||
f'const HELPER_URL = "{HELPER_URL_PLACEHOLDER}";',
|
||||
f'const HELPER_TOKEN = "{HELPER_TOKEN_PLACEHOLDER}";',
|
||||
"const pendingByTab = new Map();",
|
||||
"",
|
||||
"function transportForUrl(rawUrl) {",
|
||||
" try {",
|
||||
" const parsed = new URL(rawUrl);",
|
||||
" const host = parsed.hostname.toLowerCase().replace(/\\.$/, '');",
|
||||
" if (host.endsWith('.onion')) return 'tor';",
|
||||
" if (host.endsWith('.i2p')) return 'i2p';",
|
||||
" } catch (_error) {}",
|
||||
" return '';",
|
||||
"}",
|
||||
"",
|
||||
"async function helper(action, transport, url, consent = false) {",
|
||||
" const response = await fetch(HELPER_URL, {",
|
||||
" method: 'POST',",
|
||||
" headers: {",
|
||||
" 'Content-Type': 'application/json',",
|
||||
" 'X-AMPB-Token': HELPER_TOKEN,",
|
||||
" },",
|
||||
" body: JSON.stringify({ action, transport, url, consent }),",
|
||||
" });",
|
||||
" return response.json();",
|
||||
"}",
|
||||
"",
|
||||
"function internalUrl(page, transport, url, result) {",
|
||||
" const installCommand = Array.isArray(result.install_command) ? result.install_command.join(' ') : '';",
|
||||
" const params = new URLSearchParams({",
|
||||
" transport,",
|
||||
" url,",
|
||||
" message: result.message || '',",
|
||||
" hint: result.setup_hint || '',",
|
||||
" install: installCommand,",
|
||||
" });",
|
||||
" return browser.runtime.getURL(`${page}.html?${params.toString()}`);",
|
||||
"}",
|
||||
"",
|
||||
"async function handoff(details, transport) {",
|
||||
" let result;",
|
||||
" try {",
|
||||
" result = await helper('open', transport, details.url, false);",
|
||||
" } catch (error) {",
|
||||
" result = { ok: false, launched: false, message: String(error) };",
|
||||
" }",
|
||||
" const page = result.launched ? 'handoff' : 'setup';",
|
||||
" try {",
|
||||
" await browser.tabs.update(details.tabId, { url: internalUrl(page, transport, details.url, result) });",
|
||||
" } finally {",
|
||||
" pendingByTab.delete(details.tabId);",
|
||||
" }",
|
||||
"}",
|
||||
"",
|
||||
"function interceptRequest(details) {",
|
||||
" const transport = transportForUrl(details.url);",
|
||||
" if (!transport) return {};",
|
||||
" if (details.type !== 'main_frame' || details.tabId < 0) return { cancel: true };",
|
||||
" if (pendingByTab.get(details.tabId) === details.url) return { cancel: true };",
|
||||
" pendingByTab.set(details.tabId, details.url);",
|
||||
" void handoff(details, transport);",
|
||||
" return { cancel: true };",
|
||||
"}",
|
||||
"",
|
||||
"browser.webRequest.onBeforeRequest.addListener(",
|
||||
" interceptRequest,",
|
||||
" { urls: ['http://*.onion/*', 'https://*.onion/*', 'ws://*.onion/*', 'wss://*.onion/*', 'http://*.i2p/*', 'https://*.i2p/*', 'ws://*.i2p/*', 'wss://*.i2p/*'] },",
|
||||
" ['blocking']",
|
||||
");",
|
||||
"",
|
||||
"browser.runtime.onMessage.addListener((message) => {",
|
||||
" if (!message || message.type !== 'ampb-open') return false;",
|
||||
" return helper('open', message.transport, message.url, message.consent === true);",
|
||||
"});",
|
||||
"",
|
||||
)
|
||||
tor_adapter = adapter_for("tor")
|
||||
i2p_adapter = adapter_for("i2p")
|
||||
tor_host, tor_port = _endpoint_host_port(
|
||||
tor_adapter.endpoint if tor_adapter else "socks5://127.0.0.1:9050"
|
||||
)
|
||||
i2p_host, i2p_port = _endpoint_host_port(
|
||||
i2p_adapter.endpoint if i2p_adapter else "http://127.0.0.1:4444"
|
||||
)
|
||||
lines = (
|
||||
'"use strict";',
|
||||
"",
|
||||
f'const HELPER_URL = "{HELPER_URL_PLACEHOLDER}";',
|
||||
f'const HELPER_TOKEN = "{HELPER_TOKEN_PLACEHOLDER}";',
|
||||
f'const TOR_PROXY = {{ host: "{tor_host}", port: {tor_port} }};',
|
||||
f'const I2P_PROXY = {{ host: "{i2p_host}", port: {i2p_port} }};',
|
||||
"const CONTAINERS = {",
|
||||
" clearnet: { name: 'AMPB Clearnet', color: 'blue', icon: 'circle' },",
|
||||
" tor: { name: 'AMPB Tor', color: 'purple', icon: 'fingerprint' },",
|
||||
" i2p: { name: 'AMPB I2P', color: 'orange', icon: 'circle' },",
|
||||
"};",
|
||||
"const storesByTransport = new Map();",
|
||||
"const transportsByStore = new Map();",
|
||||
"const pendingByTab = new Map();",
|
||||
"const containersReady = initializeContainers();",
|
||||
"",
|
||||
"function transportForUrl(rawUrl) {",
|
||||
" try {",
|
||||
" const parsed = new URL(rawUrl);",
|
||||
" const host = parsed.hostname.toLowerCase().replace(/\\.$/, '');",
|
||||
" if (host.endsWith('.onion')) return 'tor';",
|
||||
" if (host.endsWith('.i2p')) return 'i2p';",
|
||||
" if (['http:', 'https:', 'ws:', 'wss:'].includes(parsed.protocol)) return 'clearnet';",
|
||||
" } catch (_error) {}",
|
||||
" return '';",
|
||||
"}",
|
||||
"",
|
||||
"function hostname(rawUrl) {",
|
||||
" try { return new URL(rawUrl).hostname.toLowerCase().replace(/\\.$/, ''); } catch (_error) { return ''; }",
|
||||
"}",
|
||||
"",
|
||||
"function transportForStore(cookieStoreId) {",
|
||||
" return transportsByStore.get(cookieStoreId) || '';",
|
||||
"}",
|
||||
"",
|
||||
"function isHelperRequest(details) {",
|
||||
" const origin = details.originUrl || details.documentUrl || '';",
|
||||
" return details.url.startsWith(HELPER_URL) && details.tabId < 0 && origin.startsWith(browser.runtime.getURL(''));",
|
||||
"}",
|
||||
"",
|
||||
"async function initializeContainers() {",
|
||||
" const identities = await browser.contextualIdentities.query({});",
|
||||
" for (const [transport, metadata] of Object.entries(CONTAINERS)) {",
|
||||
" let identity = identities.find((candidate) => candidate.name === metadata.name);",
|
||||
" if (!identity) identity = await browser.contextualIdentities.create(metadata);",
|
||||
" storesByTransport.set(transport, identity.cookieStoreId);",
|
||||
" transportsByStore.set(identity.cookieStoreId, transport);",
|
||||
" }",
|
||||
"}",
|
||||
"",
|
||||
"async function helper(action, transport, url) {",
|
||||
" const response = await fetch(HELPER_URL, {",
|
||||
" method: 'POST',",
|
||||
" headers: { 'Content-Type': 'application/json', 'X-AMPB-Token': HELPER_TOKEN },",
|
||||
" body: JSON.stringify({ action, transport, url }),",
|
||||
" });",
|
||||
" return response.json();",
|
||||
"}",
|
||||
"",
|
||||
"function setupUrl(mode, transport, url, result) {",
|
||||
" const installCommand = Array.isArray(result.install_command) ? result.install_command.join(' ') : '';",
|
||||
" const params = new URLSearchParams({",
|
||||
" mode, transport, url, message: result.message || '', hint: result.setup_hint || '', install: installCommand,",
|
||||
" });",
|
||||
" return browser.runtime.getURL(`setup.html?${params.toString()}`);",
|
||||
"}",
|
||||
"",
|
||||
"function isolationToken(details) {",
|
||||
" const store = details.cookieStoreId || 'unknown';",
|
||||
" const firstParty = hostname(details.documentUrl || details.originUrl || details.url) || 'unknown';",
|
||||
" return `ampb:${store}:${firstParty}`.slice(0, 220);",
|
||||
"}",
|
||||
"",
|
||||
"function torProxy(details) {",
|
||||
" const token = isolationToken(details);",
|
||||
" return {",
|
||||
" type: 'socks', host: TOR_PROXY.host, port: TOR_PROXY.port, proxyDNS: true,",
|
||||
" username: '<torS0X>0', password: token, connectionIsolationKey: token, failoverTimeout: 1,",
|
||||
" };",
|
||||
"}",
|
||||
"",
|
||||
"function i2pProxy() {",
|
||||
" return { type: 'http', host: I2P_PROXY.host, port: I2P_PROXY.port, failoverTimeout: 1 };",
|
||||
"}",
|
||||
"",
|
||||
"function blockedProxy() {",
|
||||
" return { type: 'http', host: '127.0.0.1', port: 9, failoverTimeout: 1 };",
|
||||
"}",
|
||||
"",
|
||||
"function proxyForRequest(details) {",
|
||||
" if (isHelperRequest(details)) return { type: 'direct' };",
|
||||
" if (details.url.startsWith(HELPER_URL)) return blockedProxy();",
|
||||
" const target = transportForUrl(details.url);",
|
||||
" const current = transportForStore(details.cookieStoreId);",
|
||||
" if (target === 'tor' && current !== 'tor') return torProxy(details);",
|
||||
" if (target === 'i2p' && current !== 'i2p') return i2pProxy();",
|
||||
" if (current === 'tor') return torProxy(details);",
|
||||
" if (current === 'i2p') return target === 'i2p' ? i2pProxy() : blockedProxy();",
|
||||
" return { type: 'direct' };",
|
||||
"}",
|
||||
"",
|
||||
"async function replaceTab(tabId, transport, url) {",
|
||||
" await containersReady;",
|
||||
" const oldTab = await browser.tabs.get(tabId);",
|
||||
" const cookieStoreId = storesByTransport.get(transport);",
|
||||
" if (!cookieStoreId) throw new Error(`No container for ${transport}`);",
|
||||
" const newTab = await browser.tabs.create({",
|
||||
" windowId: oldTab.windowId, index: oldTab.index, active: oldTab.active,",
|
||||
" pinned: oldTab.pinned, cookieStoreId, url,",
|
||||
" });",
|
||||
" if (oldTab.mutedInfo && oldTab.mutedInfo.muted) await browser.tabs.update(newTab.id, { muted: true });",
|
||||
" await browser.tabs.remove(tabId);",
|
||||
"}",
|
||||
"",
|
||||
"async function routeTopLevel(details, transport, current) {",
|
||||
" try {",
|
||||
" await containersReady;",
|
||||
" if (transport === 'clearnet') {",
|
||||
" if (!current) await replaceTab(details.tabId, transport, details.url);",
|
||||
" else await browser.tabs.update(details.tabId, {",
|
||||
" url: setupUrl('switch', transport, details.url, { message: 'Switching to clearnet can reveal your network address.' }),",
|
||||
" });",
|
||||
" return;",
|
||||
" }",
|
||||
" const result = await helper('status', transport, details.url);",
|
||||
" if (result.ready) await replaceTab(details.tabId, transport, details.url);",
|
||||
" else await browser.tabs.update(details.tabId, { url: setupUrl('setup', transport, details.url, result) });",
|
||||
" } catch (error) {",
|
||||
" await browser.tabs.update(details.tabId, {",
|
||||
" url: setupUrl('error', transport, details.url, { message: String(error) }),",
|
||||
" });",
|
||||
" } finally {",
|
||||
" pendingByTab.delete(details.tabId);",
|
||||
" }",
|
||||
"}",
|
||||
"",
|
||||
"function interceptRequest(details) {",
|
||||
" if (isHelperRequest(details)) return {};",
|
||||
" if (details.url.startsWith(HELPER_URL)) return { cancel: true };",
|
||||
" const target = transportForUrl(details.url);",
|
||||
" if (!target) return {};",
|
||||
" const current = transportForStore(details.cookieStoreId);",
|
||||
" if (current === target) return {};",
|
||||
" if (current === 'tor' && target === 'clearnet') return {};",
|
||||
" if (details.type !== 'main_frame' || details.tabId < 0) return { cancel: true };",
|
||||
" if (pendingByTab.get(details.tabId) === details.url) return { cancel: true };",
|
||||
" pendingByTab.set(details.tabId, details.url);",
|
||||
" void routeTopLevel(details, target, current);",
|
||||
" return { cancel: true };",
|
||||
"}",
|
||||
"",
|
||||
"browser.proxy.onRequest.addListener(proxyForRequest, { urls: ['<all_urls>'] });",
|
||||
"browser.webRequest.onBeforeRequest.addListener(",
|
||||
" interceptRequest, { urls: ['<all_urls>'] }, ['blocking']",
|
||||
");",
|
||||
"",
|
||||
"browser.runtime.onMessage.addListener(async (message, sender) => {",
|
||||
" if (!message || !sender.tab || sender.tab.id < 0) return false;",
|
||||
" if (message.type === 'ampb-confirm-switch') {",
|
||||
" setTimeout(() => void replaceTab(sender.tab.id, message.transport, message.url), 0);",
|
||||
" return { ok: true, switching: true };",
|
||||
" }",
|
||||
" if (message.type !== 'ampb-ensure-and-switch') return false;",
|
||||
" const result = await helper('ensure', message.transport, message.url);",
|
||||
" if (result.ready) setTimeout(() => void replaceTab(sender.tab.id, message.transport, message.url), 0);",
|
||||
" return { ...result, switching: result.ready };",
|
||||
"});",
|
||||
"browser.tabs.onRemoved.addListener((tabId) => pendingByTab.delete(tabId));",
|
||||
"",
|
||||
"async function normalizeOpenTabs() {",
|
||||
" await containersReady;",
|
||||
" const tabs = await browser.tabs.query({});",
|
||||
" for (const tab of tabs) {",
|
||||
" if (tab.id < 0 || transportForStore(tab.cookieStoreId)) continue;",
|
||||
" const transport = transportForUrl(tab.url || '');",
|
||||
" if (transport === 'clearnet') await replaceTab(tab.id, transport, tab.url);",
|
||||
" }",
|
||||
"}",
|
||||
"void normalizeOpenTabs();",
|
||||
"",
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _route_helper_setup_html() -> str:
|
||||
@@ -591,7 +706,7 @@ def _route_helper_setup_html() -> str:
|
||||
"</head>",
|
||||
"<body>",
|
||||
' <main class="panel">',
|
||||
' <p class="eyebrow">AMPB Route Helper</p>',
|
||||
' <p class="eyebrow">AMPB</p>',
|
||||
' <h1 id="title">Set Up Transport</h1>',
|
||||
' <p id="body">AMPB needs to start a local transport before opening this route.</p>',
|
||||
' <pre id="url"></pre>',
|
||||
@@ -614,6 +729,7 @@ def _route_helper_setup_js() -> str:
|
||||
'"use strict";',
|
||||
"",
|
||||
"const params = new URLSearchParams(window.location.search);",
|
||||
"const mode = params.get('mode') || 'setup';",
|
||||
"const transport = params.get('transport') || '';",
|
||||
"const url = params.get('url') || '';",
|
||||
"const message = params.get('message') || '';",
|
||||
@@ -632,21 +748,23 @@ def _route_helper_setup_js() -> str:
|
||||
" installCommand.textContent = value;",
|
||||
"}",
|
||||
"",
|
||||
"document.getElementById('title').textContent = `Set up ${label}?`;",
|
||||
"document.getElementById('body').textContent = `AMPB can start a managed ${label} session and keep its state isolated.`;",
|
||||
"const switching = mode === 'switch';",
|
||||
"document.getElementById('title').textContent = switching ? `Switch to ${label}?` : `Set up ${label}?`;",
|
||||
"document.getElementById('body').textContent = switching",
|
||||
" ? `This tab will continue in the isolated ${label} container.`",
|
||||
" : `AMPB can start a managed ${label} session and continue in an isolated ${label} container tab.`;",
|
||||
"document.getElementById('url').textContent = url;",
|
||||
"document.getElementById('message').textContent = hint || message;",
|
||||
"document.getElementById('approve').textContent = `Start ${label} and open`;",
|
||||
"document.getElementById('approve').textContent = switching ? `Switch to ${label}` : `Start ${label} and continue`;",
|
||||
"showInstallCommand(install);",
|
||||
"",
|
||||
"document.getElementById('approve').addEventListener('click', async () => {",
|
||||
" const status = document.getElementById('status');",
|
||||
" status.textContent = `Starting ${label}...`;",
|
||||
" const result = await browser.runtime.sendMessage({ type: 'ampb-open', transport, url, consent: true });",
|
||||
" if (result && result.launched) {",
|
||||
" status.textContent = `${label} opened in its isolated browser profile.`;",
|
||||
" const params = new URLSearchParams({ transport, url });",
|
||||
" window.location.href = browser.runtime.getURL(`handoff.html?${params.toString()}`);",
|
||||
" status.textContent = switching ? `Switching to ${label}...` : `Starting ${label}...`;",
|
||||
" const type = switching ? 'ampb-confirm-switch' : 'ampb-ensure-and-switch';",
|
||||
" const result = await browser.runtime.sendMessage({ type, transport, url });",
|
||||
" if (result && result.switching) {",
|
||||
" status.textContent = `Continuing in the ${label} tab...`;",
|
||||
" return;",
|
||||
" }",
|
||||
" if (result && Array.isArray(result.install_command)) {",
|
||||
@@ -676,49 +794,6 @@ def _route_helper_setup_css() -> str:
|
||||
)
|
||||
|
||||
|
||||
def _route_helper_handoff_html() -> str:
|
||||
return "\n".join(
|
||||
(
|
||||
"<!doctype html>",
|
||||
'<html lang="en">',
|
||||
"<head>",
|
||||
' <meta charset="utf-8">',
|
||||
' <meta name="viewport" content="width=device-width, initial-scale=1">',
|
||||
" <title>Route Opened</title>",
|
||||
' <link rel="stylesheet" href="setup.css">',
|
||||
"</head>",
|
||||
"<body>",
|
||||
' <main class="panel">',
|
||||
' <p class="eyebrow">AMPB Transport Broker</p>',
|
||||
' <h1 id="title">Route opened</h1>',
|
||||
' <p id="body">The destination is open in its isolated transport profile.</p>',
|
||||
' <pre id="url"></pre>',
|
||||
" </main>",
|
||||
' <script src="handoff.js"></script>',
|
||||
"</body>",
|
||||
"</html>",
|
||||
"",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _route_helper_handoff_js() -> str:
|
||||
return "\n".join(
|
||||
(
|
||||
'"use strict";',
|
||||
"",
|
||||
"const params = new URLSearchParams(window.location.search);",
|
||||
"const transport = params.get('transport') || '';",
|
||||
"const url = params.get('url') || '';",
|
||||
"const label = transport === 'tor' ? 'Tor' : transport === 'i2p' ? 'I2P' : transport;",
|
||||
"document.getElementById('title').textContent = `${label} route opened`;",
|
||||
"document.getElementById('body').textContent = `This destination opened in the isolated ${label} profile. This clearnet profile did not load it.`;",
|
||||
"document.getElementById('url').textContent = url;",
|
||||
"",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _endpoint_host_port(endpoint: str) -> tuple[str, int]:
|
||||
parsed = urlparse(endpoint)
|
||||
return parsed.hostname or "127.0.0.1", parsed.port or 0
|
||||
|
||||
@@ -103,7 +103,7 @@ BROWSER_BACKENDS = (
|
||||
"primary desktop runtime",
|
||||
"planned",
|
||||
"desktop",
|
||||
"bundled Firefox/Gecko-lineage app with AMPB-owned profiles and transports",
|
||||
"bundled Firefox/Gecko-lineage app with one window and AMPB-owned transport contexts",
|
||||
"no system browser dependency; track Firefox ESR, Tor Browser, and Mullvad hardening choices",
|
||||
"Default desktop runtime; users should not need Firefox installed.",
|
||||
),
|
||||
@@ -113,7 +113,7 @@ BROWSER_BACKENDS = (
|
||||
"planned",
|
||||
"android",
|
||||
"bundled GeckoView/Fenix/Tor Browser Android-lineage app with foreground transports",
|
||||
"no system browser dependency; app owns profile, proxy policy, and transport lifecycle",
|
||||
"no system browser dependency; app owns context, proxy policy, and transport lifecycle",
|
||||
"Default Android runtime for real mobile AMPB.",
|
||||
),
|
||||
BrowserBackend(
|
||||
|
||||
@@ -24,8 +24,8 @@ class PlatformCapability:
|
||||
|
||||
|
||||
PLATFORM_CAPABILITIES = (
|
||||
PlatformCapability("clearnet", "desktop", "ready", "ready", "unsupported", "unsupported", "Use bundled AMPB Gecko desktop runtime with isolated profile."),
|
||||
PlatformCapability("clearnet", "android", "ready", "ready", "unsupported", "unsupported", "Use bundled AMPB GeckoView Android runtime with isolated profile."),
|
||||
PlatformCapability("clearnet", "desktop", "ready", "ready", "unsupported", "unsupported", "Use one bundled AMPB Gecko window with isolated transport contexts."),
|
||||
PlatformCapability("clearnet", "android", "ready", "ready", "unsupported", "unsupported", "Use one bundled GeckoView app with isolated transport sessions."),
|
||||
PlatformCapability("clearnet", "ios", "ready", "ready", "unsupported", "unsupported", "Use bundled AMPB iOS shell with platform WebKit constraints."),
|
||||
PlatformCapability("tor", "desktop", "ready", "ready", "planned", "planned", "Adopt local Tor or prompt before managed Arti SOCKS setup."),
|
||||
PlatformCapability("tor", "android", "planned", "planned", "planned", "planned", "Use an app-owned foreground service; prefer Arti/Tor runtime integration where available."),
|
||||
|
||||
@@ -8,9 +8,7 @@ import threading
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from .broker import BrokerRouteError, open_brokered_url
|
||||
from .config import AppConfig, load_config
|
||||
from .launch import OpenPlan
|
||||
from .routing import route_url
|
||||
from .transport_manager import ManagedTransportResult, ensure_transport_ready, transport_status
|
||||
|
||||
@@ -35,24 +33,6 @@ def handle_helper_message(
|
||||
return _result_response(transport_status(transport, config=config, root=root))
|
||||
if action == "ensure":
|
||||
return _result_response(ensure_transport_ready(transport, config=config, root=root))
|
||||
if action == "open":
|
||||
raw_url = _string(message.get("url"))
|
||||
if not raw_url:
|
||||
return _error_response("missing-url", "broker open requires a URL")
|
||||
try:
|
||||
open_plan = open_brokered_url(
|
||||
raw_url,
|
||||
expected_transport=transport,
|
||||
consent=message.get("consent") is True,
|
||||
config=config,
|
||||
root=root,
|
||||
)
|
||||
except BrokerRouteError as exc:
|
||||
return _error_response("route-mismatch", str(exc))
|
||||
transport_result = None
|
||||
if open_plan.status not in {"launched", "opened-existing"}:
|
||||
transport_result = transport_status(transport, config=config, root=root)
|
||||
return _open_response(open_plan, transport_result)
|
||||
return _error_response("unsupported-action", f"unsupported helper action: {action or '-'}")
|
||||
|
||||
|
||||
@@ -151,32 +131,6 @@ def _result_response(result: ManagedTransportResult) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _open_response(
|
||||
open_plan: OpenPlan,
|
||||
transport_result: ManagedTransportResult | None,
|
||||
) -> dict[str, Any]:
|
||||
route = open_plan.browse_plan.route
|
||||
opened = open_plan.status in {"launched", "opened-existing"}
|
||||
return {
|
||||
"ok": True,
|
||||
"transport": route.transport,
|
||||
"profile": route.profile,
|
||||
"status": open_plan.status,
|
||||
"ready": opened,
|
||||
"launched": opened,
|
||||
"requires_consent": open_plan.status == "consent-required",
|
||||
"consent_granted": open_plan.consent_granted,
|
||||
"browser_pid": open_plan.browser_pid,
|
||||
"profile_path": open_plan.profile_path,
|
||||
"message": open_plan.message,
|
||||
"setup_prompt_title": open_plan.setup_prompt_title,
|
||||
"setup_prompt_body": open_plan.setup_prompt_body,
|
||||
"setup_prompt_approve_label": open_plan.setup_prompt_approve_label,
|
||||
"setup_hint": transport_result.setup_hint if transport_result else "-",
|
||||
"install_command": list(transport_result.install_command) if transport_result else [],
|
||||
}
|
||||
|
||||
|
||||
def _error_response(status: str, message: str) -> dict[str, Any]:
|
||||
return {
|
||||
"ok": False,
|
||||
|
||||
Reference in New Issue
Block a user