Files
adaptive-multi-protocol-bro…/ampbrowser/transport_manager.py
T

259 lines
6.8 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
import json
from pathlib import Path
import os
import shutil
import socket
import subprocess
import time
from urllib.parse import urlparse
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
pid: int
state_dir: str
command: tuple[str, ...]
message: str
@property
def ready(self) -> bool:
return self.status in {"ready", "started"}
def ensure_transport_ready(
transport: str,
*,
config: AppConfig,
root: Path,
status: TransportStatus | None = None,
wait_seconds: float = 20.0,
) -> ManagedTransportResult:
adapter = adapter_for(transport)
if adapter is None:
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,
0,
_managed_state_dir(root, config, transport),
(),
f"adopted existing {transport} transport",
)
if transport != "tor":
return ManagedTransportResult(
transport,
"-",
"unsupported",
status.endpoint,
False,
0,
_managed_state_dir(root, config, transport),
(),
f"managed start for {transport} is not implemented yet",
)
return _start_tor(config=config, root=root, endpoint=status.endpoint, wait_seconds=wait_seconds)
def _start_tor(
*,
config: AppConfig,
root: Path,
endpoint: str,
wait_seconds: float,
) -> ManagedTransportResult:
provider = _tor_provider(config)
state_dir = _managed_state_dir(root, config, "tor")
if not provider:
return ManagedTransportResult(
"tor",
"-",
"missing-provider",
endpoint,
False,
0,
state_dir,
(),
"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)
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)
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",
str(data_dir),
"--ClientOnly",
"1",
"--Log",
f"notice file {state_path / 'tor.log'}",
)
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:
host, port = _endpoint_host_port(endpoint)
if _can_connect(host, port):
return True
time.sleep(0.2)
return False
def _can_connect(host: str, port: int) -> bool:
try:
with socket.create_connection((host, port), timeout=0.2):
return True
except OSError:
return False
def _endpoint_host_port(endpoint: str) -> tuple[str, int]:
parsed = urlparse(endpoint)
return parsed.hostname or "127.0.0.1", parsed.port or 9050