mirror of
https://github.com/Such-Software/adaptive-multi-protocol-browser.git
synced 2026-10-17 14:31:46 +02:00
Add managed I2P provider start path
This commit is contained in:
@@ -44,9 +44,13 @@ python3 -m ampbrowser open http://example.onion/ --yes --launch
|
||||
python3 -m ampbrowser shell http://example.onion/
|
||||
python3 -m ampbrowser shell http://example.onion/ --yes
|
||||
python3 -m ampbrowser open http://example.b32.i2p/ --yes
|
||||
python3 -m ampbrowser open http://example.b32.i2p/ --yes --launch
|
||||
python3 -m ampbrowser transport start tor
|
||||
python3 -m ampbrowser transport start i2p
|
||||
python3 -m ampbrowser transport status tor
|
||||
python3 -m ampbrowser transport status i2p
|
||||
python3 -m ampbrowser transport stop tor
|
||||
python3 -m ampbrowser transport stop i2p
|
||||
python3 -m ampbrowser open http://example.b32.i2p/ --config examples/config.toml
|
||||
python3 -m ampbrowser open http://example.b32.i2p/ --platform android
|
||||
python3 -m ampbrowser fixture check ../adaptive-multi-protocol-gateway/dist/wownero/ampg-fixture-manifest.json
|
||||
|
||||
+174
-13
@@ -25,6 +25,12 @@ class TorProvider:
|
||||
binary: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class I2PProvider:
|
||||
kind: str
|
||||
binary: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ManagedTransportResult:
|
||||
transport: str
|
||||
@@ -72,20 +78,22 @@ def ensure_transport_ready(
|
||||
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",
|
||||
)
|
||||
if transport == "tor":
|
||||
return _start_tor(config=config, root=root, endpoint=status.endpoint, wait_seconds=wait_seconds)
|
||||
if transport == "i2p":
|
||||
return _start_i2p(config=config, root=root, endpoint=status.endpoint, wait_seconds=wait_seconds)
|
||||
|
||||
return _start_tor(config=config, root=root, endpoint=status.endpoint, wait_seconds=wait_seconds)
|
||||
return ManagedTransportResult(
|
||||
transport,
|
||||
"-",
|
||||
"unsupported",
|
||||
status.endpoint,
|
||||
False,
|
||||
0,
|
||||
_managed_state_dir(root, config, transport),
|
||||
(),
|
||||
f"managed start for {transport} is not implemented yet",
|
||||
)
|
||||
|
||||
|
||||
def transport_status(
|
||||
@@ -368,6 +376,159 @@ def _tor_command(provider: TorProvider, *, endpoint: str, state_path: Path) -> t
|
||||
)
|
||||
|
||||
|
||||
def _start_i2p(
|
||||
*,
|
||||
config: AppConfig,
|
||||
root: Path,
|
||||
endpoint: str,
|
||||
wait_seconds: float,
|
||||
) -> ManagedTransportResult:
|
||||
provider = _i2p_provider(config)
|
||||
state_dir = _managed_state_dir(root, config, "i2p")
|
||||
if not provider:
|
||||
return ManagedTransportResult(
|
||||
"i2p",
|
||||
"-",
|
||||
"missing-provider",
|
||||
endpoint,
|
||||
False,
|
||||
0,
|
||||
state_dir,
|
||||
(),
|
||||
"I2P provider not found; install i2pd or set AMPB_I2PD_BIN or transports.i2p.binary_path",
|
||||
)
|
||||
|
||||
state_path = Path(state_dir)
|
||||
state_path.mkdir(parents=True, exist_ok=True)
|
||||
_chmod_private(state_path)
|
||||
|
||||
command = _i2p_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(
|
||||
"i2p",
|
||||
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):
|
||||
result = ManagedTransportResult(
|
||||
"i2p",
|
||||
provider.kind,
|
||||
"started",
|
||||
endpoint,
|
||||
True,
|
||||
process.pid,
|
||||
state_dir,
|
||||
command,
|
||||
f"started managed {provider.kind} transport",
|
||||
)
|
||||
_write_owned_state(root, config, result)
|
||||
return result
|
||||
|
||||
process.terminate()
|
||||
return ManagedTransportResult(
|
||||
"i2p",
|
||||
provider.kind,
|
||||
"start-timeout",
|
||||
endpoint,
|
||||
True,
|
||||
process.pid,
|
||||
state_dir,
|
||||
command,
|
||||
f"managed {provider.kind} did not become ready at {endpoint}",
|
||||
)
|
||||
|
||||
|
||||
def _i2p_provider(config: AppConfig) -> I2PProvider | None:
|
||||
env_path = os.environ.get("AMPB_I2PD_BIN")
|
||||
if env_path:
|
||||
return I2PProvider("i2pd", env_path)
|
||||
|
||||
config_path = config.transport_binary("i2p")
|
||||
if config_path:
|
||||
return I2PProvider("i2pd", config_path)
|
||||
|
||||
system_i2pd = shutil.which("i2pd")
|
||||
if system_i2pd:
|
||||
return I2PProvider("i2pd", system_i2pd)
|
||||
return None
|
||||
|
||||
|
||||
def _i2p_command(provider: I2PProvider, *, endpoint: str, state_path: Path) -> tuple[str, ...]:
|
||||
host, port = _endpoint_host_port(endpoint)
|
||||
data_dir = state_path / "i2pd-data"
|
||||
data_dir.mkdir(parents=True, exist_ok=True)
|
||||
_chmod_private(data_dir)
|
||||
config_path = state_path / "i2pd.conf"
|
||||
tunnels_path = state_path / "tunnels.conf"
|
||||
tunnels_path.touch(exist_ok=True)
|
||||
config_path.write_text(_i2pd_config(host=host, port=port, state_path=state_path), encoding="utf-8")
|
||||
_chmod_private(config_path)
|
||||
_chmod_private(tunnels_path)
|
||||
return (
|
||||
provider.binary,
|
||||
"--conf",
|
||||
str(config_path),
|
||||
"--tunconf",
|
||||
str(tunnels_path),
|
||||
"--datadir",
|
||||
str(data_dir),
|
||||
"--daemon=false",
|
||||
"--service=false",
|
||||
)
|
||||
|
||||
|
||||
def _i2pd_config(*, host: str, port: int, state_path: Path) -> str:
|
||||
return "\n".join(
|
||||
(
|
||||
"daemon = false",
|
||||
"service = false",
|
||||
"notransit = true",
|
||||
"log = file",
|
||||
f"logfile = {state_path / 'i2pd-router.log'}",
|
||||
"loglevel = warn",
|
||||
"",
|
||||
"[http]",
|
||||
"enabled = false",
|
||||
"",
|
||||
"[httpproxy]",
|
||||
"enabled = true",
|
||||
f"address = {host}",
|
||||
f"port = {port}",
|
||||
"addresshelper = true",
|
||||
"senduseragent = false",
|
||||
"",
|
||||
"[socksproxy]",
|
||||
"enabled = false",
|
||||
"",
|
||||
"[sam]",
|
||||
"enabled = false",
|
||||
"",
|
||||
"[bob]",
|
||||
"enabled = false",
|
||||
"",
|
||||
"[i2cp]",
|
||||
"enabled = false",
|
||||
"",
|
||||
"[i2pcontrol]",
|
||||
"enabled = false",
|
||||
"",
|
||||
"[upnp]",
|
||||
"enabled = false",
|
||||
"",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _managed_state_dir(root: Path, config: AppConfig, transport: str) -> str:
|
||||
return str(root / config.state_dir / "transports" / transport)
|
||||
|
||||
|
||||
@@ -94,6 +94,14 @@ workspace:
|
||||
`AMPB_ARTI_BIN`, `AMPB_TOR_BIN`, configured `transports.tor.binary_path`, and finally a
|
||||
system `tor` binary if one is present.
|
||||
|
||||
## I2P Provider Probe
|
||||
|
||||
Managed I2P startup currently targets `i2pd`. AMPB adopts an existing HTTP proxy on
|
||||
`127.0.0.1:4444` when available; otherwise `ampbrowser open <i2p-url> --yes --launch`
|
||||
uses `AMPB_I2PD_BIN`, configured `transports.i2p.binary_path`, or a system `i2pd` binary.
|
||||
The AMPB-owned process writes config, router data, logs, and ownership metadata under
|
||||
`.ampb/transports/i2p`.
|
||||
|
||||
## Source Plan
|
||||
|
||||
Print the canonical source targets and paths:
|
||||
|
||||
@@ -42,6 +42,8 @@ socks_endpoint = "127.0.0.1:9050"
|
||||
[transports.i2p]
|
||||
enabled = true
|
||||
mode = "adopt-or-prompt-manage"
|
||||
# Optional. Used when AMPB should manage a local i2pd binary.
|
||||
binary_path = ""
|
||||
http_proxy = "127.0.0.1:4444"
|
||||
|
||||
[transports.ipfs]
|
||||
|
||||
@@ -9,8 +9,8 @@ transports only after the user opens a URL that needs them and approves setup.
|
||||
profile, proxy endpoint, browser runtime path, generated profile prefs path, required
|
||||
setup steps, and whether user consent is still needed. With `--launch`, AMPB creates the
|
||||
profile and starts the bundled browser only when the selected route is already ready.
|
||||
With `--launch --yes`, AMPB can start a configured Tor provider with AMPB-owned state
|
||||
before launching. Transport policy comes from `.ampb/config.toml` or an explicit
|
||||
With `--launch --yes`, AMPB can start configured Tor and I2P providers with AMPB-owned
|
||||
state before launching. Transport policy comes from `.ampb/config.toml` or an explicit
|
||||
`--config` path.
|
||||
|
||||
`ampbrowser transport start|status|stop <name>` exposes the same ownership model without
|
||||
@@ -39,9 +39,10 @@ AMPB records owned process metadata in `.ampb/transports/<name>/ampb-owned.json`
|
||||
|
||||
## Current Adapters
|
||||
|
||||
- Tor: detect SOCKS on `127.0.0.1:9050`; managed start supports a configured `tor` binary
|
||||
with AMPB-owned state; packaged Arti/Tor bundling remains a packaging target.
|
||||
- I2P: detect HTTP proxy on `127.0.0.1:4444`; managed start planned.
|
||||
- Tor: detect SOCKS on `127.0.0.1:9050`; managed start supports Arti, configured `tor`,
|
||||
and system `tor` providers with AMPB-owned state.
|
||||
- I2P: detect HTTP proxy on `127.0.0.1:4444`; managed start supports configured or system
|
||||
`i2pd` providers with AMPB-owned state.
|
||||
- IPFS: detect HTTP gateway on `127.0.0.1:8080`; managed gateway start planned.
|
||||
- Gemini: built-in route and render path; no daemon required for static browsing.
|
||||
- Reticulum: resilient/private route contract exists; adapter and readiness model are planned.
|
||||
|
||||
@@ -13,6 +13,8 @@ binary_path = ""
|
||||
[transports.i2p]
|
||||
enabled = true
|
||||
mode = "adopt-or-prompt-manage"
|
||||
# Optional. Used when AMPB should manage a local i2pd binary.
|
||||
binary_path = ""
|
||||
|
||||
[transports.ipfs]
|
||||
enabled = true
|
||||
|
||||
@@ -252,7 +252,26 @@ class TransportManagerTest(unittest.TestCase):
|
||||
self.assertFalse(result.ready)
|
||||
popen.return_value.terminate.assert_called_once()
|
||||
|
||||
def test_non_tor_managed_start_is_reported_as_unsupported(self) -> None:
|
||||
def test_reports_missing_i2p_provider(self) -> None:
|
||||
status = TransportStatus(
|
||||
transport="i2p",
|
||||
installed=False,
|
||||
running=False,
|
||||
endpoint="http://127.0.0.1:4444",
|
||||
adoptable=False,
|
||||
manage_supported=True,
|
||||
note="I2P HTTP proxy",
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
with patch("ampbrowser.transport_manager.shutil.which", return_value=None):
|
||||
result = ensure_transport_ready("i2p", config=AppConfig(transport_modes={}), root=Path(tmp), status=status)
|
||||
|
||||
self.assertEqual("missing-provider", result.status)
|
||||
self.assertFalse(result.ready)
|
||||
self.assertIn("I2P provider not found", result.message)
|
||||
|
||||
def test_starts_configured_i2pd_provider_with_ampb_state(self) -> None:
|
||||
status = TransportStatus(
|
||||
transport="i2p",
|
||||
installed=True,
|
||||
@@ -262,9 +281,72 @@ class TransportManagerTest(unittest.TestCase):
|
||||
manage_supported=True,
|
||||
note="I2P HTTP proxy",
|
||||
)
|
||||
config = AppConfig(transport_modes={}, transport_binaries={"i2p": "/opt/ampb/i2pd"})
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
result = ensure_transport_ready("i2p", config=AppConfig(transport_modes={}), root=Path(tmp), status=status)
|
||||
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("i2p", config=config, root=root, status=status)
|
||||
|
||||
self.assertEqual("started", result.status)
|
||||
self.assertEqual("i2pd", result.provider)
|
||||
self.assertTrue(result.owned)
|
||||
self.assertEqual(1234, result.pid)
|
||||
self.assertEqual(str(root / ".ampb/transports/i2p"), result.state_dir)
|
||||
self.assertEqual("/opt/ampb/i2pd", result.command[0])
|
||||
self.assertIn("--conf", result.command)
|
||||
self.assertIn(str(root / ".ampb/transports/i2p/i2pd.conf"), result.command)
|
||||
self.assertIn("--datadir", result.command)
|
||||
self.assertIn(str(root / ".ampb/transports/i2p/i2pd-data"), result.command)
|
||||
self.assertIn("--daemon=false", result.command)
|
||||
self.assertIn("--service=false", result.command)
|
||||
config_text = (root / ".ampb/transports/i2p/i2pd.conf").read_text(encoding="utf-8")
|
||||
self.assertIn("[httpproxy]", config_text)
|
||||
self.assertIn("address = 127.0.0.1", config_text)
|
||||
self.assertIn("port = 4444", config_text)
|
||||
self.assertIn("notransit = true", config_text)
|
||||
state_path = root / ".ampb/transports/i2p/ampb-owned.json"
|
||||
self.assertTrue(state_path.exists())
|
||||
self.assertIn('"provider": "i2pd"', state_path.read_text(encoding="utf-8"))
|
||||
popen.assert_called_once()
|
||||
|
||||
def test_terminates_owned_i2pd_process_on_timeout(self) -> None:
|
||||
status = TransportStatus(
|
||||
transport="i2p",
|
||||
installed=True,
|
||||
running=False,
|
||||
endpoint="http://127.0.0.1:4444",
|
||||
adoptable=False,
|
||||
manage_supported=True,
|
||||
note="I2P HTTP proxy",
|
||||
)
|
||||
config = AppConfig(transport_modes={}, transport_binaries={"i2p": "/opt/ampb/i2pd"})
|
||||
|
||||
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("i2p", config=config, root=Path(tmp), status=status)
|
||||
|
||||
self.assertEqual("start-timeout", result.status)
|
||||
self.assertFalse(result.ready)
|
||||
popen.return_value.terminate.assert_called_once()
|
||||
|
||||
def test_other_managed_start_is_reported_as_unsupported(self) -> None:
|
||||
status = TransportStatus(
|
||||
transport="ipfs",
|
||||
installed=True,
|
||||
running=False,
|
||||
endpoint="http://127.0.0.1:8080",
|
||||
adoptable=False,
|
||||
manage_supported=True,
|
||||
note="IPFS local gateway",
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
result = ensure_transport_ready("ipfs", config=AppConfig(transport_modes={}), root=Path(tmp), status=status)
|
||||
|
||||
self.assertEqual("unsupported", result.status)
|
||||
self.assertFalse(result.ready)
|
||||
|
||||
Reference in New Issue
Block a user