mirror of
https://github.com/Such-Software/adaptive-multi-protocol-browser.git
synced 2026-10-17 14:31:46 +02:00
Add local Arti provider bootstrap
This commit is contained in:
+14
-2
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import socket
|
||||
|
||||
@@ -34,7 +36,7 @@ class TransportAdapter:
|
||||
manage_supported: bool = True
|
||||
|
||||
def inspect(self) -> TransportStatus:
|
||||
installed = self.builtin_running or any(shutil.which(command) for command in self.commands)
|
||||
installed = self.builtin_running or any(_command_available(command) for command in self.commands)
|
||||
running = self.builtin_running or (
|
||||
bool(self.connect_host) and bool(self.connect_port) and _can_connect(self.connect_host, self.connect_port)
|
||||
)
|
||||
@@ -84,7 +86,7 @@ ADAPTERS: dict[str, TransportAdapter] = {
|
||||
start_strategy="start managed Arti SOCKS proxy",
|
||||
stop_policy="stop only AMPB-owned Tor daemon",
|
||||
note="Tor via existing SOCKS proxy or managed Arti/Tor runtime",
|
||||
commands=("arti", "tor"),
|
||||
commands=("ampb-bundled-arti", "arti", "tor"),
|
||||
connect_host="127.0.0.1",
|
||||
connect_port=9050,
|
||||
),
|
||||
@@ -148,6 +150,16 @@ def adapter_for(name: str) -> TransportAdapter | None:
|
||||
return ADAPTERS.get(name)
|
||||
|
||||
|
||||
def _command_available(command: str) -> bool:
|
||||
if command == "ampb-bundled-arti":
|
||||
path = Path(os.environ.get("AMPB_BROWSER_BUILD_ROOT", "/tmp/ampb-browser-build")) / "providers/arti/bin/arti"
|
||||
return path.exists() and path.is_file()
|
||||
if "/" in command:
|
||||
path = Path(command)
|
||||
return path.exists() and path.is_file()
|
||||
return bool(shutil.which(command))
|
||||
|
||||
|
||||
def _can_connect(host: str, port: int) -> bool:
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=0.2):
|
||||
|
||||
@@ -153,6 +153,7 @@ def _cmd_open(
|
||||
status="ready" if transport_result.ready else transport_result.status,
|
||||
message=transport_result.message,
|
||||
transport_setup_status=transport_result.status,
|
||||
transport_setup_provider=transport_result.provider,
|
||||
transport_setup_message=transport_result.message,
|
||||
)
|
||||
open_plan = execute_open(open_plan, root=Path.cwd())
|
||||
@@ -181,6 +182,7 @@ def _cmd_open(
|
||||
f"runtime_path={runtime_path} "
|
||||
f"user_js_path={user_js_path} "
|
||||
f"transport_setup_status={open_plan.transport_setup_status} "
|
||||
f"transport_setup_provider={open_plan.transport_setup_provider} "
|
||||
f"transport_setup_message=\"{_safe(open_plan.transport_setup_message)}\" "
|
||||
f"launch_command=\"{_safe(launch_command)}\" "
|
||||
f"setup_steps=\"{_safe(setup_steps)}\" "
|
||||
|
||||
@@ -34,6 +34,7 @@ class OpenPlan:
|
||||
message: str
|
||||
launch_spec: BrowserLaunchSpec | None = None
|
||||
transport_setup_status: str = "-"
|
||||
transport_setup_provider: str = "-"
|
||||
transport_setup_message: str = "-"
|
||||
|
||||
|
||||
|
||||
+132
-60
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
import os
|
||||
import shutil
|
||||
@@ -13,9 +14,19 @@ from .adapters import TransportStatus, adapter_for
|
||||
from .config import AppConfig
|
||||
|
||||
|
||||
ARTI_RELATIVE_PATH = Path("providers/arti/bin/arti")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TorProvider:
|
||||
kind: str
|
||||
binary: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ManagedTransportResult:
|
||||
transport: str
|
||||
provider: str
|
||||
status: str
|
||||
endpoint: str
|
||||
owned: bool
|
||||
@@ -39,12 +50,13 @@ def ensure_transport_ready(
|
||||
) -> ManagedTransportResult:
|
||||
adapter = adapter_for(transport)
|
||||
if adapter is None:
|
||||
return ManagedTransportResult(transport, "unsupported", "-", False, 0, "-", (), "transport adapter not found")
|
||||
return ManagedTransportResult(transport, "-", "unsupported", "-", False, 0, "-", (), "transport adapter not found")
|
||||
|
||||
status = status or adapter.inspect()
|
||||
if status.adoptable:
|
||||
return ManagedTransportResult(
|
||||
transport,
|
||||
"-",
|
||||
"ready",
|
||||
status.endpoint,
|
||||
False,
|
||||
@@ -57,6 +69,7 @@ def ensure_transport_ready(
|
||||
if transport != "tor":
|
||||
return ManagedTransportResult(
|
||||
transport,
|
||||
"-",
|
||||
"unsupported",
|
||||
status.endpoint,
|
||||
False,
|
||||
@@ -76,28 +89,130 @@ def _start_tor(
|
||||
endpoint: str,
|
||||
wait_seconds: float,
|
||||
) -> ManagedTransportResult:
|
||||
binary = _transport_binary(config, "tor")
|
||||
provider = _tor_provider(config)
|
||||
state_dir = _managed_state_dir(root, config, "tor")
|
||||
if not binary:
|
||||
if not provider:
|
||||
return ManagedTransportResult(
|
||||
"tor",
|
||||
"-",
|
||||
"missing-provider",
|
||||
endpoint,
|
||||
False,
|
||||
0,
|
||||
state_dir,
|
||||
(),
|
||||
"Tor provider not found; set AMPB_TOR_BIN or transports.tor.binary_path",
|
||||
"Tor provider not found; run tools/browser-tor-provider-build.sh or set AMPB_ARTI_BIN, AMPB_TOR_BIN, or transports.tor.binary_path",
|
||||
)
|
||||
|
||||
state_path = Path(state_dir)
|
||||
data_dir = state_path / "data"
|
||||
log_path = state_path / "tor.log"
|
||||
data_dir.mkdir(parents=True, exist_ok=True)
|
||||
state_path.mkdir(parents=True, exist_ok=True)
|
||||
_chmod_private(state_path)
|
||||
|
||||
command = _tor_command(provider, endpoint=endpoint, state_path=state_path)
|
||||
try:
|
||||
with (state_path / f"{provider.kind}.log").open("ab") as log_file:
|
||||
process = subprocess.Popen(command, stdout=log_file, stderr=log_file, start_new_session=True) # noqa: S603
|
||||
except OSError as exc:
|
||||
return ManagedTransportResult(
|
||||
"tor",
|
||||
provider.kind,
|
||||
"start-failed",
|
||||
endpoint,
|
||||
False,
|
||||
0,
|
||||
state_dir,
|
||||
command,
|
||||
f"managed {provider.kind} could not start: {exc}",
|
||||
)
|
||||
|
||||
if _wait_for_endpoint(endpoint, timeout_seconds=wait_seconds):
|
||||
return ManagedTransportResult(
|
||||
"tor",
|
||||
provider.kind,
|
||||
"started",
|
||||
endpoint,
|
||||
True,
|
||||
process.pid,
|
||||
state_dir,
|
||||
command,
|
||||
f"started managed {provider.kind} transport",
|
||||
)
|
||||
|
||||
process.terminate()
|
||||
return ManagedTransportResult(
|
||||
"tor",
|
||||
provider.kind,
|
||||
"start-timeout",
|
||||
endpoint,
|
||||
True,
|
||||
process.pid,
|
||||
state_dir,
|
||||
command,
|
||||
f"managed {provider.kind} did not become ready at {endpoint}",
|
||||
)
|
||||
|
||||
|
||||
def _tor_provider(config: AppConfig) -> TorProvider | None:
|
||||
arti_path = os.environ.get("AMPB_ARTI_BIN") or _bundled_arti_path()
|
||||
if arti_path:
|
||||
return TorProvider("arti", arti_path)
|
||||
|
||||
tor_path = os.environ.get("AMPB_TOR_BIN")
|
||||
if tor_path:
|
||||
return TorProvider("tor", tor_path)
|
||||
|
||||
config_path = config.transport_binary("tor")
|
||||
if config_path:
|
||||
return TorProvider(_infer_provider_kind(config_path), config_path)
|
||||
|
||||
system_tor = shutil.which("tor")
|
||||
if system_tor:
|
||||
return TorProvider("tor", system_tor)
|
||||
return None
|
||||
|
||||
|
||||
def _bundled_arti_path() -> str:
|
||||
build_root = Path(os.environ.get("AMPB_BROWSER_BUILD_ROOT", "/tmp/ampb-browser-build"))
|
||||
candidate = build_root / ARTI_RELATIVE_PATH
|
||||
if candidate.exists() and candidate.is_file():
|
||||
return str(candidate)
|
||||
return ""
|
||||
|
||||
|
||||
def _infer_provider_kind(binary_path: str) -> str:
|
||||
name = Path(binary_path).name.lower()
|
||||
if name.startswith("arti"):
|
||||
return "arti"
|
||||
return "tor"
|
||||
|
||||
|
||||
def _tor_command(provider: TorProvider, *, endpoint: str, state_path: Path) -> tuple[str, ...]:
|
||||
host, port = _endpoint_host_port(endpoint)
|
||||
command = (
|
||||
binary,
|
||||
if provider.kind == "arti":
|
||||
state_dir = state_path / "arti-state"
|
||||
cache_dir = state_path / "arti-cache"
|
||||
state_dir.mkdir(parents=True, exist_ok=True)
|
||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
_chmod_private(state_dir)
|
||||
_chmod_private(cache_dir)
|
||||
return (
|
||||
provider.binary,
|
||||
"proxy",
|
||||
"-p",
|
||||
str(port),
|
||||
"-o",
|
||||
f"storage.state_dir={json.dumps(str(state_dir))}",
|
||||
"-o",
|
||||
f"storage.cache_dir={json.dumps(str(cache_dir))}",
|
||||
"-l",
|
||||
"warn",
|
||||
)
|
||||
|
||||
data_dir = state_path / "tor-data"
|
||||
data_dir.mkdir(parents=True, exist_ok=True)
|
||||
_chmod_private(data_dir)
|
||||
return (
|
||||
provider.binary,
|
||||
"--SocksPort",
|
||||
f"{host}:{port}",
|
||||
"--DataDirectory",
|
||||
@@ -105,64 +220,21 @@ def _start_tor(
|
||||
"--ClientOnly",
|
||||
"1",
|
||||
"--Log",
|
||||
f"notice file {log_path}",
|
||||
f"notice file {state_path / 'tor.log'}",
|
||||
)
|
||||
try:
|
||||
process = subprocess.Popen(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # noqa: S603
|
||||
except OSError as exc:
|
||||
return ManagedTransportResult(
|
||||
"tor",
|
||||
"start-failed",
|
||||
endpoint,
|
||||
False,
|
||||
0,
|
||||
state_dir,
|
||||
command,
|
||||
f"managed Tor could not start: {exc}",
|
||||
)
|
||||
|
||||
if _wait_for_endpoint(endpoint, timeout_seconds=wait_seconds):
|
||||
return ManagedTransportResult(
|
||||
"tor",
|
||||
"started",
|
||||
endpoint,
|
||||
True,
|
||||
process.pid,
|
||||
state_dir,
|
||||
command,
|
||||
"started managed Tor transport",
|
||||
)
|
||||
|
||||
process.terminate()
|
||||
return ManagedTransportResult(
|
||||
"tor",
|
||||
"start-timeout",
|
||||
endpoint,
|
||||
True,
|
||||
process.pid,
|
||||
state_dir,
|
||||
command,
|
||||
f"managed Tor did not become ready at {endpoint}",
|
||||
)
|
||||
|
||||
|
||||
def _transport_binary(config: AppConfig, transport: str) -> str:
|
||||
env_name = f"AMPB_{transport.upper()}_BIN"
|
||||
env_path = os.environ.get(env_name)
|
||||
if env_path:
|
||||
return env_path
|
||||
config_path = config.transport_binary(transport)
|
||||
if config_path:
|
||||
return config_path
|
||||
if transport == "tor":
|
||||
return shutil.which("tor") or ""
|
||||
return ""
|
||||
|
||||
|
||||
def _managed_state_dir(root: Path, config: AppConfig, transport: str) -> str:
|
||||
return str(root / config.state_dir / "transports" / transport)
|
||||
|
||||
|
||||
def _chmod_private(path: Path) -> None:
|
||||
try:
|
||||
path.chmod(0o700)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _wait_for_endpoint(endpoint: str, *, timeout_seconds: float) -> bool:
|
||||
deadline = time.monotonic() + timeout_seconds
|
||||
while time.monotonic() <= deadline:
|
||||
|
||||
@@ -76,6 +76,24 @@ A successful source build produces a bundled app at
|
||||
Configure and build modes may install Mozilla build Python helpers such as `zstandard`
|
||||
into mach virtualenvs under `/tmp/ampb-browser-build/cache/mozbuild`.
|
||||
|
||||
## Tor Provider Probe
|
||||
|
||||
Build a local Arti provider for managed Tor startup:
|
||||
|
||||
```sh
|
||||
sh tools/browser-tor-provider-build.sh
|
||||
```
|
||||
|
||||
The provider script keeps Cargo output and the installed Arti binary under the external
|
||||
workspace:
|
||||
|
||||
- `/tmp/ampb-browser-build/providers/arti/bin/arti`
|
||||
- `/tmp/ampb-browser-build/logs/arti-cargo-install.log`
|
||||
|
||||
`ampbrowser open <onion-url> --yes --launch` will prefer this local Arti provider, then
|
||||
`AMPB_ARTI_BIN`, `AMPB_TOR_BIN`, configured `transports.tor.binary_path`, and finally a
|
||||
system `tor` binary if one is present.
|
||||
|
||||
## Source Plan
|
||||
|
||||
Print the canonical source targets and paths:
|
||||
|
||||
@@ -26,6 +26,7 @@ class TransportManagerTest(unittest.TestCase):
|
||||
result = ensure_transport_ready("tor", config=AppConfig(transport_modes={}), root=Path(tmp), status=status)
|
||||
|
||||
self.assertEqual("ready", result.status)
|
||||
self.assertEqual("-", result.provider)
|
||||
self.assertFalse(result.owned)
|
||||
self.assertTrue(result.ready)
|
||||
self.assertEqual(0, result.pid)
|
||||
@@ -43,13 +44,46 @@ class TransportManagerTest(unittest.TestCase):
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
with patch("ampbrowser.transport_manager.shutil.which", return_value=None):
|
||||
result = ensure_transport_ready("tor", config=AppConfig(transport_modes={}), root=Path(tmp), status=status)
|
||||
with patch("ampbrowser.transport_manager._bundled_arti_path", return_value=""):
|
||||
result = ensure_transport_ready("tor", config=AppConfig(transport_modes={}), root=Path(tmp), status=status)
|
||||
|
||||
self.assertEqual("missing-provider", result.status)
|
||||
self.assertFalse(result.ready)
|
||||
self.assertIn("Tor provider not found", result.message)
|
||||
|
||||
def test_starts_configured_tor_provider_with_ampb_state(self) -> None:
|
||||
def test_starts_bundled_arti_provider_with_ampb_state(self) -> None:
|
||||
status = TransportStatus(
|
||||
transport="tor",
|
||||
installed=True,
|
||||
running=False,
|
||||
endpoint="socks5://127.0.0.1:9050",
|
||||
adoptable=False,
|
||||
manage_supported=True,
|
||||
note="Tor SOCKS proxy",
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
arti = root / "providers/arti/bin/arti"
|
||||
with patch("ampbrowser.transport_manager._wait_for_endpoint", return_value=True):
|
||||
with patch("ampbrowser.transport_manager.subprocess.Popen") as popen:
|
||||
with patch("ampbrowser.transport_manager._bundled_arti_path", return_value=str(arti)):
|
||||
popen.return_value.pid = 1234
|
||||
result = ensure_transport_ready("tor", config=AppConfig(transport_modes={}), root=root, status=status)
|
||||
|
||||
self.assertEqual("started", result.status)
|
||||
self.assertEqual("arti", result.provider)
|
||||
self.assertTrue(result.owned)
|
||||
self.assertEqual(1234, result.pid)
|
||||
self.assertEqual(str(root / ".ampb/transports/tor"), result.state_dir)
|
||||
self.assertEqual(str(arti), result.command[0])
|
||||
self.assertIn("proxy", result.command)
|
||||
self.assertIn("-p", result.command)
|
||||
self.assertIn('storage.state_dir="' + str(root / ".ampb/transports/tor/arti-state") + '"', result.command)
|
||||
self.assertIn('storage.cache_dir="' + str(root / ".ampb/transports/tor/arti-cache") + '"', result.command)
|
||||
popen.assert_called_once()
|
||||
|
||||
def test_starts_configured_classic_tor_provider_with_ampb_state(self) -> None:
|
||||
status = TransportStatus(
|
||||
transport="tor",
|
||||
installed=True,
|
||||
@@ -65,16 +99,18 @@ class TransportManagerTest(unittest.TestCase):
|
||||
root = Path(tmp)
|
||||
with patch("ampbrowser.transport_manager._wait_for_endpoint", return_value=True):
|
||||
with patch("ampbrowser.transport_manager.subprocess.Popen") as popen:
|
||||
popen.return_value.pid = 1234
|
||||
result = ensure_transport_ready("tor", config=config, root=root, status=status)
|
||||
with patch("ampbrowser.transport_manager._bundled_arti_path", return_value=""):
|
||||
popen.return_value.pid = 1234
|
||||
result = ensure_transport_ready("tor", config=config, root=root, status=status)
|
||||
|
||||
self.assertEqual("started", result.status)
|
||||
self.assertEqual("tor", result.provider)
|
||||
self.assertTrue(result.owned)
|
||||
self.assertEqual(1234, result.pid)
|
||||
self.assertEqual(str(root / ".ampb/transports/tor"), result.state_dir)
|
||||
self.assertEqual("/opt/ampb/tor", result.command[0])
|
||||
self.assertIn("--DataDirectory", result.command)
|
||||
self.assertIn(str(root / ".ampb/transports/tor/data"), result.command)
|
||||
self.assertIn(str(root / ".ampb/transports/tor/tor-data"), result.command)
|
||||
popen.assert_called_once()
|
||||
|
||||
def test_reports_tor_spawn_failure(self) -> None:
|
||||
@@ -90,10 +126,12 @@ class TransportManagerTest(unittest.TestCase):
|
||||
config = AppConfig(transport_modes={}, transport_binaries={"tor": "/opt/ampb/tor"})
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
with patch("ampbrowser.transport_manager.subprocess.Popen", side_effect=OSError("nope")):
|
||||
result = ensure_transport_ready("tor", config=config, root=Path(tmp), status=status)
|
||||
with patch("ampbrowser.transport_manager._bundled_arti_path", return_value=""):
|
||||
with patch("ampbrowser.transport_manager.subprocess.Popen", side_effect=OSError("nope")):
|
||||
result = ensure_transport_ready("tor", config=config, root=Path(tmp), status=status)
|
||||
|
||||
self.assertEqual("start-failed", result.status)
|
||||
self.assertEqual("tor", result.provider)
|
||||
self.assertFalse(result.ready)
|
||||
self.assertIn("could not start", result.message)
|
||||
|
||||
@@ -112,8 +150,9 @@ class TransportManagerTest(unittest.TestCase):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
with patch("ampbrowser.transport_manager._wait_for_endpoint", return_value=False):
|
||||
with patch("ampbrowser.transport_manager.subprocess.Popen") as popen:
|
||||
popen.return_value.pid = 1234
|
||||
result = ensure_transport_ready("tor", config=config, root=Path(tmp), status=status)
|
||||
with patch("ampbrowser.transport_manager._bundled_arti_path", return_value=""):
|
||||
popen.return_value.pid = 1234
|
||||
result = ensure_transport_ready("tor", config=config, root=Path(tmp), status=status)
|
||||
|
||||
self.assertEqual("start-timeout", result.status)
|
||||
self.assertFalse(result.ready)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
root="${AMPB_BROWSER_BUILD_ROOT:-/tmp/ampb-browser-build}"
|
||||
provider_root="$root/providers/arti"
|
||||
log="$root/logs/arti-cargo-install.log"
|
||||
|
||||
mkdir -p "$root/logs"
|
||||
mkdir -p "$root/providers"
|
||||
|
||||
cargo install arti --locked --root "$provider_root" >"$log" 2>&1
|
||||
|
||||
binary="$provider_root/bin/arti"
|
||||
if [ ! -x "$binary" ]; then
|
||||
printf 'missing arti binary after install: %s\n' "$binary" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
version="$("$binary" --version | head -n 1)"
|
||||
printf 'AMPB_TOR_PROVIDER status=ok kind=arti binary=%s log=%s version="%s"\n' "$binary" "$log" "$version"
|
||||
Reference in New Issue
Block a user