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/
|
||||||
python3 -m ampbrowser shell http://example.onion/ --yes
|
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
|
||||||
|
python3 -m ampbrowser open http://example.b32.i2p/ --yes --launch
|
||||||
python3 -m ampbrowser transport start tor
|
python3 -m ampbrowser transport start tor
|
||||||
|
python3 -m ampbrowser transport start i2p
|
||||||
python3 -m ampbrowser transport status tor
|
python3 -m ampbrowser transport status tor
|
||||||
|
python3 -m ampbrowser transport status i2p
|
||||||
python3 -m ampbrowser transport stop tor
|
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/ --config examples/config.toml
|
||||||
python3 -m ampbrowser open http://example.b32.i2p/ --platform android
|
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
|
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
|
binary: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class I2PProvider:
|
||||||
|
kind: str
|
||||||
|
binary: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ManagedTransportResult:
|
class ManagedTransportResult:
|
||||||
transport: str
|
transport: str
|
||||||
@@ -72,20 +78,22 @@ def ensure_transport_ready(
|
|||||||
f"adopted existing {transport} transport",
|
f"adopted existing {transport} transport",
|
||||||
)
|
)
|
||||||
|
|
||||||
if transport != "tor":
|
if transport == "tor":
|
||||||
return ManagedTransportResult(
|
return _start_tor(config=config, root=root, endpoint=status.endpoint, wait_seconds=wait_seconds)
|
||||||
transport,
|
if transport == "i2p":
|
||||||
"-",
|
return _start_i2p(config=config, root=root, endpoint=status.endpoint, wait_seconds=wait_seconds)
|
||||||
"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)
|
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(
|
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:
|
def _managed_state_dir(root: Path, config: AppConfig, transport: str) -> str:
|
||||||
return str(root / config.state_dir / "transports" / transport)
|
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
|
`AMPB_ARTI_BIN`, `AMPB_TOR_BIN`, configured `transports.tor.binary_path`, and finally a
|
||||||
system `tor` binary if one is present.
|
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
|
## Source Plan
|
||||||
|
|
||||||
Print the canonical source targets and paths:
|
Print the canonical source targets and paths:
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ socks_endpoint = "127.0.0.1:9050"
|
|||||||
[transports.i2p]
|
[transports.i2p]
|
||||||
enabled = true
|
enabled = true
|
||||||
mode = "adopt-or-prompt-manage"
|
mode = "adopt-or-prompt-manage"
|
||||||
|
# Optional. Used when AMPB should manage a local i2pd binary.
|
||||||
|
binary_path = ""
|
||||||
http_proxy = "127.0.0.1:4444"
|
http_proxy = "127.0.0.1:4444"
|
||||||
|
|
||||||
[transports.ipfs]
|
[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
|
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
|
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.
|
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
|
With `--launch --yes`, AMPB can start configured Tor and I2P providers with AMPB-owned
|
||||||
before launching. Transport policy comes from `.ampb/config.toml` or an explicit
|
state before launching. Transport policy comes from `.ampb/config.toml` or an explicit
|
||||||
`--config` path.
|
`--config` path.
|
||||||
|
|
||||||
`ampbrowser transport start|status|stop <name>` exposes the same ownership model without
|
`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
|
## Current Adapters
|
||||||
|
|
||||||
- Tor: detect SOCKS on `127.0.0.1:9050`; managed start supports a configured `tor` binary
|
- Tor: detect SOCKS on `127.0.0.1:9050`; managed start supports Arti, configured `tor`,
|
||||||
with AMPB-owned state; packaged Arti/Tor bundling remains a packaging target.
|
and system `tor` providers with AMPB-owned state.
|
||||||
- I2P: detect HTTP proxy on `127.0.0.1:4444`; managed start planned.
|
- 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.
|
- 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.
|
- 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.
|
- Reticulum: resilient/private route contract exists; adapter and readiness model are planned.
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ binary_path = ""
|
|||||||
[transports.i2p]
|
[transports.i2p]
|
||||||
enabled = true
|
enabled = true
|
||||||
mode = "adopt-or-prompt-manage"
|
mode = "adopt-or-prompt-manage"
|
||||||
|
# Optional. Used when AMPB should manage a local i2pd binary.
|
||||||
|
binary_path = ""
|
||||||
|
|
||||||
[transports.ipfs]
|
[transports.ipfs]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
|||||||
@@ -252,7 +252,26 @@ class TransportManagerTest(unittest.TestCase):
|
|||||||
self.assertFalse(result.ready)
|
self.assertFalse(result.ready)
|
||||||
popen.return_value.terminate.assert_called_once()
|
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(
|
status = TransportStatus(
|
||||||
transport="i2p",
|
transport="i2p",
|
||||||
installed=True,
|
installed=True,
|
||||||
@@ -262,9 +281,72 @@ class TransportManagerTest(unittest.TestCase):
|
|||||||
manage_supported=True,
|
manage_supported=True,
|
||||||
note="I2P HTTP proxy",
|
note="I2P HTTP proxy",
|
||||||
)
|
)
|
||||||
|
config = AppConfig(transport_modes={}, transport_binaries={"i2p": "/opt/ampb/i2pd"})
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
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.assertEqual("unsupported", result.status)
|
||||||
self.assertFalse(result.ready)
|
self.assertFalse(result.ready)
|
||||||
|
|||||||
Reference in New Issue
Block a user