From c0b2cbe1c8256852adf20f3c4feccf69c66ba7b5 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 6 Feb 2026 09:30:35 -0700 Subject: [PATCH 01/17] docs: update preferred external port design in TODO --- agents/TODO.md | 107 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/agents/TODO.md b/agents/TODO.md index 70124aa74..2ee9bbe2b 100644 --- a/agents/TODO.md +++ b/agents/TODO.md @@ -6,4 +6,111 @@ Pending tasks for AI agents. Remove items when completed. - [ ] Architecture - Web (`/web`) - @MattDHill +## Features +- [ ] Support preferred external ports besides 443 - @dr-bonez + + **Problem**: Currently, port 443 is the only preferred external port that is actually honored. When a + service requests `preferred_external_port: 8443` (or any non-443 value) for SSL, the system ignores + the preference and assigns a dynamic-range port (49152-65535). The `preferred_external_port` is only + used as a label for Tor mappings and as a trigger for the port-443 special case in `update()`. + + **Goal**: Honor `preferred_external_port` for both SSL and non-SSL binds when the requested port is + available, with proper conflict resolution and fallback to dynamic-range allocation. + + ### Design + + **Key distinction**: There are two separate concepts for SSL port usage: + + 1. **Port ownership** (`assigned_ssl_port`) — A port exclusively owned by a binding, allocated from + `AvailablePorts`. Used for server hostnames (`.local`, mDNS, etc.) and iptables forwards. + 2. **Domain SSL port** — The port used for domain-based vhost entries. A binding does NOT need to own + a port to have a domain vhost on it. The VHostController already supports multiple hostnames on the + same port via SNI. Any binding can create a domain vhost entry on any SSL port that the + VHostController has a listener for, regardless of who "owns" that port. + + For example: the OS owns port 443 as its `assigned_ssl_port`. A service with + `preferred_external_port: 443` won't get 443 as its `assigned_ssl_port` (it's taken), but it CAN + still have domain vhost entries on port 443 — SNI routes by hostname. + + #### 1. Preferred Port Allocation for Ownership (`forward.rs`, `binding.rs`) + + Expand `AvailablePorts` to support trying a preferred port before falling back to the dynamic range: + + - Add `try_alloc(port) -> Option`: Attempts to exclusively allocate a specific port. Returns + `None` if the port is already allocated or restricted. + - Enforce the restricted port list (currently noted in `vhost.rs:89`: `<=1024, >=32768, 5355, 5432, + 9050, 6010, 9051, 5353`) — skip the preferred port if restricted, except for ports the OS itself + uses (80, 443). + - No SSL-vs-non-SSL distinction or refcounting needed at this layer — ownership is always exclusive. + SSL port sharing for domains is handled entirely by the VHostController via SNI. + + Modify `BindInfo::new()` and `BindInfo::update()` to attempt the preferred port first: + + ``` + assigned_ssl_port = try_alloc(ssl.preferred_external_port) + .unwrap_or(dynamic_pool.alloc()) + assigned_port = try_alloc(options.preferred_external_port) + .unwrap_or(dynamic_pool.alloc()) + ``` + + After this change, `assigned_ssl_port` may match the preferred port if it was available, or fall back + to the dynamic range as before. + + #### 2. Eliminate the Port 5443 Hack: Source-IP-Based Public/Private Gating (`vhost.rs`, `net_controller.rs`) + + **Current problem**: The `if ssl.preferred_external_port == 443` branch (line 341 of + `net_controller.rs`) creates a bespoke dual-vhost setup: port 5443 for private-only access and port + 443 for public (or public+private). This exists because both public and private traffic arrive on the + same port 443 listener, and the current `InterfaceFilter`/`PublicFilter` model distinguishes + public/private by which *network interface* the connection arrived on — which doesn't work when both + traffic types share a listener. + + **Solution**: Determine public vs private based on **source IP** at the vhost level. Traffic arriving + from the gateway IP should be treated as public (the gateway may MASQUERADE/NAT internet traffic, so + anything from the gateway is potentially public). Traffic from LAN IPs is private. + + This applies to **all** vhost targets, not just port 443: + + - **Add a `public` field to `ProxyTarget`** (or an enum: `Public`, `Private`, `Both`) indicating + what traffic this target accepts. + - **Modify `VHostTarget::filter()`** (`vhost.rs:342`): Instead of (or in addition to) checking the + network interface via `GatewayInfo`, check the source IP of the TCP connection against known gateway + IPs. If the source IP matches a gateway or IP outside the subnet, the connection is public; + otherwise it's private. Use this to gate against the target's `public` field. + - **Eliminate the 5443 port entirely**: A single vhost entry on port 443 (or any shared SSL port) can + serve both public and private traffic, with per-target source-IP gating determining which backend + handles which connections. + + #### 3. Simplify `update()` Domain Vhost Logic (`net_controller.rs`) + + With source-IP gating in the vhost controller: + + - **Remove the `== 443` special case** and the 5443 secondary vhost. + - For **server hostnames** (`.local`, mDNS, embassy, startos, localhost): use `assigned_ssl_port` + (the port the binding owns). + - For **domain-based vhost entries**: attempt to use `preferred_external_port` as the vhost port. + This succeeds if the port is either unused or already has an SSL listener (SNI handles sharing). + It fails only if the port is already in use by a non-SSL binding, or is a restricted port. On + failure, fall back to `assigned_ssl_port`. + - Each domain vhost entry declares whether it's public, private, or both — the vhost controller uses + source IP to enforce this. + - Hostname info must exactly match the actual vhost port used: for server hostnames, report + `ssl_port: assigned_ssl_port`. For domains, report `ssl_port: preferred_external_port` if it was + successfully used for the domain vhost, otherwise report `ssl_port: assigned_ssl_port`. + + #### 4. No SDK or Frontend Changes Needed + + - SDK: `preferredExternalPort` is already exposed in `BindOptions` and `AddSslOptions`. + - Frontend: Already reads `assigned_port`/`assigned_ssl_port` from `NetInfo`. + + ### Key Files + + | File | Role | + |------|------| + | `core/src/net/forward.rs` | `AvailablePorts` — port pool allocation | + | `core/src/net/host/binding.rs` | `BindInfo::new()` / `update()` — port assignment at bind time | + | `core/src/net/net_controller.rs:259` | `NetServiceData::update()` — vhost/forward/DNS reconciliation, 5443 hack removal | + | `core/src/net/vhost.rs` | `VHostController` / `ProxyTarget` — source-IP gating for public/private | + | `core/src/net/gateway.rs` | `PublicFilter`, `InterfaceFilter` — may need refactoring | + | `sdk/base/lib/interfaces/Host.ts` | SDK `MultiHost.bindPort()` — no changes needed | From 8f809dab216efd647b4c6ebf90567b6726bf7c30 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Sun, 8 Feb 2026 11:17:43 -0700 Subject: [PATCH 02/17] docs: add user-controlled public/private and port forward mapping to design --- agents/TODO.md | 81 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 73 insertions(+), 8 deletions(-) diff --git a/agents/TODO.md b/agents/TODO.md index 2ee9bbe2b..9d39f2d7a 100644 --- a/agents/TODO.md +++ b/agents/TODO.md @@ -57,7 +57,46 @@ Pending tasks for AI agents. Remove items when completed. After this change, `assigned_ssl_port` may match the preferred port if it was available, or fall back to the dynamic range as before. - #### 2. Eliminate the Port 5443 Hack: Source-IP-Based Public/Private Gating (`vhost.rs`, `net_controller.rs`) + #### 2. User-Controlled Public/Private on Bindings + + There are three distinct concepts for public/private in the system: + + 1. **Gateway public/private** (descriptive): A property of the gateway/network interface — whether + its ports are publicly reachable from the internet without port forwards. This is + `NetworkInterfaceInfo::public()`. + 2. **Binding public/private** (user intent): Whether the user wants this binding to be reachable from + the internet. This is a **new** user-controlled field on the binding. + 3. **`public_enabled` / `private_disabled`** (per-gateway overrides): Override the default behavior + where private gateways are enabled and public gateways are disabled. These sets are per-gateway + exceptions to that default. + + Add a `public` field to `BindInfo` (or `NetInfo`) representing user intent: + + - **When `public = false`** (default): Block WAN traffic to this binding using source-IP gating + (see Section 3). The binding is only accessible on the LAN. + - **When `public = true`**: Allow WAN traffic. Additionally, maintain a port forward mapping in + patch-db (see Section 4) so the user knows what to configure on their router. + + **New RPC endpoints** (`binding.rs`): + + Following the existing `HostApiKind` pattern, add a new subcommand to the `binding` parent handler: + + - **`set-public`** — Set whether a binding should be publicly accessible. + + ```ts + interface BindingSetPublicParams { + internalPort: number + public: boolean + } + ``` + + Mutates `BindInfo` to set the `public` field, syncs the host. Uses `sync_db` metadata. + + This yields two RPC methods: + - `server.host.binding.set-public` + - `package.host.binding.set-public` + + #### 3. Eliminate the Port 5443 Hack: Source-IP-Based WAN Blocking (`vhost.rs`, `net_controller.rs`) **Current problem**: The `if ssl.preferred_external_port == 443` branch (line 341 of `net_controller.rs`) creates a bespoke dual-vhost setup: port 5443 for private-only access and port @@ -73,7 +112,7 @@ Pending tasks for AI agents. Remove items when completed. This applies to **all** vhost targets, not just port 443: - **Add a `public` field to `ProxyTarget`** (or an enum: `Public`, `Private`, `Both`) indicating - what traffic this target accepts. + what traffic this target accepts, derived from the binding's user-controlled `public` field. - **Modify `VHostTarget::filter()`** (`vhost.rs:342`): Instead of (or in addition to) checking the network interface via `GatewayInfo`, check the source IP of the TCP connection against known gateway IPs. If the source IP matches a gateway or IP outside the subnet, the connection is public; @@ -82,7 +121,21 @@ Pending tasks for AI agents. Remove items when completed. serve both public and private traffic, with per-target source-IP gating determining which backend handles which connections. - #### 3. Simplify `update()` Domain Vhost Logic (`net_controller.rs`) + #### 4. Port Forward Mapping in Patch-DB + + When a binding is marked `public = true`, StartOS must record the required port forwards in patch-db + so the frontend can display them to the user. The user then configures these on their router manually. + + For each public binding, store: + - The external port the router should forward (the actual vhost port used for domains, or the + `assigned_port` / `assigned_ssl_port` for non-domain access) + - The protocol (TCP/UDP) + - The StartOS LAN IP as the forward target + - Which service/binding this forward is for (for display purposes) + + This mapping should be in the public database model so the frontend can read and display it. + + #### 5. Simplify `update()` Domain Vhost Logic (`net_controller.rs`) With source-IP gating in the vhost controller: @@ -93,16 +146,17 @@ Pending tasks for AI agents. Remove items when completed. This succeeds if the port is either unused or already has an SSL listener (SNI handles sharing). It fails only if the port is already in use by a non-SSL binding, or is a restricted port. On failure, fall back to `assigned_ssl_port`. - - Each domain vhost entry declares whether it's public, private, or both — the vhost controller uses - source IP to enforce this. + - The binding's `public` field determines the `ProxyTarget`'s public/private gating. - Hostname info must exactly match the actual vhost port used: for server hostnames, report `ssl_port: assigned_ssl_port`. For domains, report `ssl_port: preferred_external_port` if it was successfully used for the domain vhost, otherwise report `ssl_port: assigned_ssl_port`. - #### 4. No SDK or Frontend Changes Needed + #### 6. SDK and Frontend Changes - - SDK: `preferredExternalPort` is already exposed in `BindOptions` and `AddSslOptions`. - - Frontend: Already reads `assigned_port`/`assigned_ssl_port` from `NetInfo`. + - **SDK**: `preferredExternalPort` is already exposed. No additional SDK changes needed. + - **Frontend**: Needs to display the port forward mapping from patch-db, showing the user what + router configuration is required for their public bindings. Also needs UI for toggling the + `public` field on a binding. Bindings are always private by default. ### Key Files @@ -114,3 +168,14 @@ Pending tasks for AI agents. Remove items when completed. | `core/src/net/vhost.rs` | `VHostController` / `ProxyTarget` — source-IP gating for public/private | | `core/src/net/gateway.rs` | `PublicFilter`, `InterfaceFilter` — may need refactoring | | `sdk/base/lib/interfaces/Host.ts` | SDK `MultiHost.bindPort()` — no changes needed | + | `core/src/db/model/public.rs` | Public DB model — port forward mapping | + +- [ ] Auto-configure port forwards via UPnP/NAT-PMP/PCP - @dr-bonez + + **Blocked by**: "Support preferred external ports besides 443" (must be implemented and tested + end-to-end first). + + **Goal**: When a binding is marked public, automatically configure port forwards on the user's router + using UPnP, NAT-PMP, or PCP, instead of requiring manual router configuration. Fall back to + displaying manual instructions (the port forward mapping from patch-db) when auto-configuration is + unavailable or fails. From 2e03a95e4716bcec1e4f01a587ee8476f28494bd Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Mon, 9 Feb 2026 13:10:57 -0700 Subject: [PATCH 03/17] docs: overhaul interfaces page design with view/manage split and per-address controls --- agents/TODO.md | 175 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 144 insertions(+), 31 deletions(-) diff --git a/agents/TODO.md b/agents/TODO.md index 9d39f2d7a..956343afb 100644 --- a/agents/TODO.md +++ b/agents/TODO.md @@ -57,44 +57,73 @@ Pending tasks for AI agents. Remove items when completed. After this change, `assigned_ssl_port` may match the preferred port if it was available, or fall back to the dynamic range as before. - #### 2. User-Controlled Public/Private on Bindings + #### 2. Per-Address Enable/Disable (replaces gateway overrides) - There are three distinct concepts for public/private in the system: + **Current model being removed**: `NetInfo` has `private_disabled: OrdSet` and + `public_enabled: OrdSet` — gateway-level toggles where private gateways are enabled by + default and public gateways are disabled by default. The `set-gateway-enabled` RPC endpoint and the + `InterfaceFilter` impl on `NetInfo` use these sets. This model is unintuitive because users think in + terms of individual addresses, not gateways. - 1. **Gateway public/private** (descriptive): A property of the gateway/network interface — whether - its ports are publicly reachable from the internet without port forwards. This is - `NetworkInterfaceInfo::public()`. - 2. **Binding public/private** (user intent): Whether the user wants this binding to be reachable from - the internet. This is a **new** user-controlled field on the binding. - 3. **`public_enabled` / `private_disabled`** (per-gateway overrides): Override the default behavior - where private gateways are enabled and public gateways are disabled. These sets are per-gateway - exceptions to that default. + **New model**: Per-address enable/disable. Each computed address in `hostname_info` gets an `enabled` + field. Users toggle individual addresses on the View page (see Section 6). - Add a `public` field to `BindInfo` (or `NetInfo`) representing user intent: + **How disabling works per address type**: - - **When `public = false`** (default): Block WAN traffic to this binding using source-IP gating - (see Section 3). The binding is only accessible on the LAN. - - **When `public = true`**: Allow WAN traffic. Additionally, maintain a port forward mapping in - patch-db (see Section 4) so the user knows what to configure on their router. + The enforcement mechanism varies by address type because different addresses are reached through + different network paths: - **New RPC endpoints** (`binding.rs`): + - **WAN IP:port** (public gateway IP addresses): Disabled via **source-IP gating** in the vhost + layer (Section 3). Public and private traffic share the same port listener, so we can't just + remove the vhost entry — that would also block private traffic. Instead, the vhost target is + tagged with which source-IP classes it accepts. When a WAN IP address is disabled, the vhost + target rejects connections whose source IP matches the gateway (i.e., NAT'd internet traffic) + or falls outside the gateway's LAN subnets. LAN traffic to the same port is unaffected. + - **LAN IP:port** (private gateway IP addresses): Also enforced via **source-IP gating**. When + disabled, the vhost target rejects connections from LAN subnets on that gateway. This is the + inverse of the WAN case — same mechanism, different source-IP class. + - **Hostname-based addresses** (`.local`, domains): Disabled by **not creating the vhost/SNI + entry** for that hostname. Since hostname-based routing uses SNI (SSL) or Host header (HTTP), + removing the entry means the hostname simply doesn't resolve to a backend. No traffic reaches + the service for that hostname. + - **Onion addresses**: Disabled by **not creating the Tor hidden service mapping**. The Tor + daemon won't advertise the onion:port, so no traffic arrives. - Following the existing `HostApiKind` pattern, add a new subcommand to the `binding` parent handler: + **Backend changes**: - - **`set-public`** — Set whether a binding should be publicly accessible. + - **Remove from `NetInfo`**: Delete the `private_disabled` and `public_enabled` fields entirely. + - **Add to `NetInfo`**: A `disabled` set containing identifiers for addresses the user has explicitly + disabled. The identifier must be stable across network changes — e.g., `(gateway_id, hostname_kind)` + for IP/Local addresses, `(gateway_id, domain_value)` for domain addresses, or `onion_value` for + onion addresses. Exact format TBD at implementation time. + - **Default behavior preserved**: Private-gateway addresses default to enabled, public-gateway + addresses default to disabled. An address is enabled if it's not in the `disabled` set AND either + (a) the gateway is private, or (b) the user has explicitly enabled it. + - **Add `enabled` field to `HostnameInfo`**: The computed `hostname_info` output should include + whether each address is enabled, derived from the `disabled` set during `NetServiceData::update()`. + - **Remove `set-gateway-enabled`** RPC endpoint from `binding.rs`. + - **Remove `InterfaceFilter` impl for `NetInfo`**: The per-gateway filter logic is replaced by + per-address filtering in `update()`. + + **New RPC endpoint** (`binding.rs`): + + Following the existing `HostApiKind` pattern, replace `set-gateway-enabled` with: + + - **`set-address-enabled`** — Toggle an individual address on or off. ```ts - interface BindingSetPublicParams { + interface BindingSetAddressEnabledParams { internalPort: number - public: boolean + addressId: AddressId // identifies a specific HostnameInfo entry + enabled: boolean } ``` - Mutates `BindInfo` to set the `public` field, syncs the host. Uses `sync_db` metadata. + Mutates `NetInfo.disabled` and syncs the host. Uses `sync_db` metadata. This yields two RPC methods: - - `server.host.binding.set-public` - - `package.host.binding.set-public` + - `server.host.binding.set-address-enabled` + - `package.host.binding.set-address-enabled` #### 3. Eliminate the Port 5443 Hack: Source-IP-Based WAN Blocking (`vhost.rs`, `net_controller.rs`) @@ -151,22 +180,106 @@ Pending tasks for AI agents. Remove items when completed. `ssl_port: assigned_ssl_port`. For domains, report `ssl_port: preferred_external_port` if it was successfully used for the domain vhost, otherwise report `ssl_port: assigned_ssl_port`. - #### 6. SDK and Frontend Changes + #### 6. Frontend: Interfaces Page Overhaul (View/Manage Split) - - **SDK**: `preferredExternalPort` is already exposed. No additional SDK changes needed. - - **Frontend**: Needs to display the port forward mapping from patch-db, showing the user what - router configuration is required for their public bindings. Also needs UI for toggling the - `public` field on a binding. Bindings are always private by default. + The current interfaces page is a single page showing gateways (with toggle), addresses, public + domains, Tor domains, and private domains. It gets split into two pages: **View** and **Manage**. + + **SDK**: `preferredExternalPort` is already exposed. No additional SDK changes needed. + + ##### View Page + + Displays all computed addresses for the interface (from `hostname_info`) as a flat list. For each + address: + + - **Enable/disable toggle**: Calls `set-address-enabled` RPC endpoint to toggle the address. Reflects + the `enabled` field from `HostnameInfo`. + - **Address details**: URL, type (IPv4, IPv6, .local, domain, onion), access level (public/private), + gateway name. + - **Port forward info**: For public addresses, display the required port forward mapping (external + port, protocol, LAN IP target) so the user knows what to configure on their router. + - **Test button**: Per-address reachability test (see Section 7). The backend returns structured + failure data (which check failed, relevant context like expected IP, actual IP, port, etc.). + The frontend constructs user-facing fix messaging from the structured result. + + No gateway-level toggles. The old `gateways.component.ts` toggle UI is removed. + + ##### Manage Page + + Simple CRUD interface for configuring which addresses exist. Three sections: + + - **Public domains**: Add/remove. Uses existing RPC endpoints: + - `{server,package}.host.address.domain.public.add` + - `{server,package}.host.address.domain.public.remove` + - **Private domains**: Add/remove. Uses existing RPC endpoints: + - `{server,package}.host.address.domain.private.add` + - `{server,package}.host.address.domain.private.remove` + - **Onion addresses**: Add/remove. Uses existing RPC endpoints: + - `{server,package}.host.address.onion.add` + - `{server,package}.host.address.onion.remove` + + ##### Key Frontend Files to Modify + + | File | Change | + |------|--------| + | `web/projects/ui/src/app/routes/portal/components/interfaces/` | Overhaul: split into view/manage | + | `web/projects/ui/src/app/routes/portal/components/interfaces/gateways.component.ts` | Remove (replaced by per-address toggles on View page) | + | `web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts` | Update `MappedServiceInterface` to use `enabled` field from `HostnameInfo` | + | `web/projects/ui/src/app/routes/portal/components/interfaces/addresses/` | Refactor for View page with enable/disable toggles and test buttons | + | `web/projects/ui/src/app/routes/portal/routes/services/services.routes.ts` | Add routes for view/manage sub-pages | + | `web/projects/ui/src/app/routes/portal/routes/system/system.routes.ts` | Add routes for view/manage sub-pages | + + #### 7. Reachability Test Endpoint + + New RPC endpoint that tests whether an address is actually reachable, with diagnostic info on + failure. + + **RPC endpoint** (`binding.rs` or new file): + + - **`test-address`** — Test reachability of a specific address. + + ```ts + interface BindingTestAddressParams { + internalPort: number + addressId: AddressId + } + ``` + + The backend simply performs the raw checks and returns the results. The **frontend** owns all + interpretation — it already knows the address type, expected IP, expected port, etc. from the + `HostnameInfo` data, so it can compare against the backend results and construct fix messaging. + + ```ts + interface TestAddressResult { + dns: string[] | null // resolved IPs, null if not a domain address or lookup failed + portOpen: boolean | null // TCP connect result, null if not applicable + } + ``` + + This yields two RPC methods: + - `server.host.binding.test-address` + - `package.host.binding.test-address` + + The frontend already has the full `HostnameInfo` context (expected IP, domain, port, gateway, + public/private). It compares the backend's raw results against the expected state and constructs + localized fix instructions. For example: + - `dns` returned but doesn't contain the expected WAN IP → "Update DNS A record for {domain} + to {wanIp}" + - `dns` is `null` for a domain address → "DNS lookup failed for {domain}" + - `portOpen` is `false` → "Configure port forward on your router: external {port} TCP → + {lanIp}:{port}" ### Key Files | File | Role | |------|------| | `core/src/net/forward.rs` | `AvailablePorts` — port pool allocation | - | `core/src/net/host/binding.rs` | `BindInfo::new()` / `update()` — port assignment at bind time | - | `core/src/net/net_controller.rs:259` | `NetServiceData::update()` — vhost/forward/DNS reconciliation, 5443 hack removal | + | `core/src/net/host/binding.rs` | `BindInfo`/`NetInfo` — remove gateway overrides, add per-address disable set, new RPC endpoints | + | `core/src/net/net_controller.rs:259` | `NetServiceData::update()` — compute `enabled` on `HostnameInfo`, vhost/forward/DNS reconciliation, 5443 hack removal | | `core/src/net/vhost.rs` | `VHostController` / `ProxyTarget` — source-IP gating for public/private | - | `core/src/net/gateway.rs` | `PublicFilter`, `InterfaceFilter` — may need refactoring | + | `core/src/net/gateway.rs` | `InterfaceFilter` — remove `NetInfo` impl, simplify | + | `core/src/net/service_interface.rs` | `HostnameInfo` — add `enabled` field | + | `core/src/net/host/address.rs` | Existing domain/onion CRUD endpoints (no changes needed) | | `sdk/base/lib/interfaces/Host.ts` | SDK `MultiHost.bindPort()` — no changes needed | | `core/src/db/model/public.rs` | Public DB model — port forward mapping | From 1974dfd66f2848242ecef3e0adea09a5275d1d32 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Mon, 9 Feb 2026 13:29:49 -0700 Subject: [PATCH 04/17] docs: move address enable/disable to overflow menu, add SSL indicator, defer UI placement decisions --- agents/TODO.md | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/agents/TODO.md b/agents/TODO.md index 956343afb..b2e34b44c 100644 --- a/agents/TODO.md +++ b/agents/TODO.md @@ -190,20 +190,16 @@ Pending tasks for AI agents. Remove items when completed. ##### View Page Displays all computed addresses for the interface (from `hostname_info`) as a flat list. For each - address: - - - **Enable/disable toggle**: Calls `set-address-enabled` RPC endpoint to toggle the address. Reflects - the `enabled` field from `HostnameInfo`. - - **Address details**: URL, type (IPv4, IPv6, .local, domain, onion), access level (public/private), - gateway name. - - **Port forward info**: For public addresses, display the required port forward mapping (external - port, protocol, LAN IP target) so the user knows what to configure on their router. - - **Test button**: Per-address reachability test (see Section 7). The backend returns structured - failure data (which check failed, relevant context like expected IP, actual IP, port, etc.). - The frontend constructs user-facing fix messaging from the structured result. + address, show: URL, type (IPv4, IPv6, .local, domain, onion), access level (public/private), + gateway name, SSL indicator (especially relevant for onion addresses which may have both SSL and + non-SSL entries), enable/disable state, port forward info for public addresses, and a test button + for reachability (see Section 7). No gateway-level toggles. The old `gateways.component.ts` toggle UI is removed. + **Note**: Exact UI element placement (where toggles, buttons, info badges go) is sensitive. + Prompt the user for specific placement decisions during implementation. + ##### Manage Page Simple CRUD interface for configuring which addresses exist. Three sections: @@ -225,7 +221,7 @@ Pending tasks for AI agents. Remove items when completed. | `web/projects/ui/src/app/routes/portal/components/interfaces/` | Overhaul: split into view/manage | | `web/projects/ui/src/app/routes/portal/components/interfaces/gateways.component.ts` | Remove (replaced by per-address toggles on View page) | | `web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts` | Update `MappedServiceInterface` to use `enabled` field from `HostnameInfo` | - | `web/projects/ui/src/app/routes/portal/components/interfaces/addresses/` | Refactor for View page with enable/disable toggles and test buttons | + | `web/projects/ui/src/app/routes/portal/components/interfaces/addresses/` | Refactor for View page with overflow menu (enable/disable) and test buttons | | `web/projects/ui/src/app/routes/portal/routes/services/services.routes.ts` | Add routes for view/manage sub-pages | | `web/projects/ui/src/app/routes/portal/routes/system/system.routes.ts` | Add routes for view/manage sub-pages | From 2ee403e7de2a993bf24e35eae2cd644c70964097 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Tue, 10 Feb 2026 13:28:24 -0700 Subject: [PATCH 05/17] chore: remove tor from startos core Tor is being moved from a built-in OS feature to a service. This removes the Arti-based Tor client, onion address management, hidden service creation, and all related code from the core backend, frontend, and SDK. - Delete core/src/net/tor/ module (~2060 lines) - Remove OnionAddress, TorSecretKey, TorController from all consumers - Remove HostnameInfo::Onion and HostAddress::Onion variants - Remove onion CRUD RPC endpoints and tor subcommand - Remove tor key handling from account and backup/restore - Remove ~12 tor-related Cargo dependencies (arti-client, torut, etc.) - Remove tor UI components, API methods, mock data, and routes - Remove OnionHostname and tor patterns/regexes from SDK - Add v0_4_0_alpha_20 database migration to strip onion data - Bump version to 0.4.0-alpha.20 --- .claude/settings.json | 3 +- agents/TODO.md | 27 + core/Cargo.lock | 2724 +------- core/Cargo.toml | 42 +- core/src/account.rs | 36 - core/src/backup/os.rs | 13 - core/src/db/model/public.rs | 1 - core/src/error.rs | 19 +- core/src/net/host/address.rs | 98 - core/src/net/host/mod.rs | 38 +- core/src/net/keys.rs | 11 +- core/src/net/mod.rs | 2 - core/src/net/net_controller.rs | 143 +- core/src/net/service_interface.rs | 19 - core/src/net/socks.rs | 38 +- core/src/net/tor/arti.rs | 964 --- core/src/net/tor/ctor.rs | 1092 --- core/src/net/tor/mod.rs | 10 - core/src/service/effects/net/ssl.rs | 20 +- core/src/version/mod.rs | 12 +- core/src/version/v0_3_6_alpha_0.rs | 157 +- core/src/version/v0_3_6_alpha_10.rs | 3 +- core/src/version/v0_4_0_alpha_12.rs | 46 - core/src/version/v0_4_0_alpha_20.rs | 110 + sdk/base/lib/exver/exver.ts | 5967 ++++++++--------- sdk/base/lib/osBindings/ErrorData.ts | 2 +- sdk/base/lib/osBindings/Host.ts | 1 - sdk/base/lib/osBindings/HostnameInfo.ts | 10 +- sdk/base/lib/osBindings/OnionHostname.ts | 7 - sdk/base/lib/osBindings/index.ts | 1 - sdk/base/lib/util/getServiceInterface.ts | 65 +- sdk/base/lib/util/patterns.ts | 10 - sdk/base/lib/util/regexes.ts | 8 - sdk/package/scripts/oldSpecToBuilder.ts | 94 +- .../shared/src/types/workspace-config.ts | 1 - .../interfaces/interface.component.ts | 3 - .../interfaces/interface.service.ts | 43 +- .../interfaces/tor-domains.component.ts | 193 - .../routes/portal/routes/logs/logs.routes.ts | 4 - .../routes/logs/routes/outlet.component.ts | 6 - .../routes/logs/routes/tor.component.ts | 32 - .../services/routes/interface.component.ts | 1 - .../routes/sideload/sideload.component.ts | 8 - .../routes/general/general.component.ts | 49 +- .../system/routes/general/wipe.component.ts | 36 - .../routes/startos-ui/startos-ui.component.ts | 1 - .../ui/src/app/services/api/api.fixures.ts | 11 - .../ui/src/app/services/api/api.types.ts | 33 +- .../app/services/api/embassy-api.service.ts | 26 - .../services/api/embassy-live-api.service.ts | 60 - .../services/api/embassy-mock-api.service.ts | 130 - .../ui/src/app/services/api/mock-patch.ts | 20 - .../ui/src/app/services/config.service.ts | 3 +- 53 files changed, 3147 insertions(+), 9306 deletions(-) delete mode 100644 core/src/net/tor/arti.rs delete mode 100644 core/src/net/tor/ctor.rs delete mode 100644 core/src/net/tor/mod.rs create mode 100644 core/src/version/v0_4_0_alpha_20.rs delete mode 100644 sdk/base/lib/osBindings/OnionHostname.ts delete mode 100644 web/projects/ui/src/app/routes/portal/components/interfaces/tor-domains.component.ts delete mode 100644 web/projects/ui/src/app/routes/portal/routes/logs/routes/tor.component.ts delete mode 100644 web/projects/ui/src/app/routes/portal/routes/system/routes/general/wipe.component.ts diff --git a/.claude/settings.json b/.claude/settings.json index ce5d2734a..671a08447 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,5 +1,6 @@ { "attribution": { - "commit": "" + "commit": "", + "pr": "" } } diff --git a/agents/TODO.md b/agents/TODO.md index b2e34b44c..e38dbdec4 100644 --- a/agents/TODO.md +++ b/agents/TODO.md @@ -279,6 +279,33 @@ Pending tasks for AI agents. Remove items when completed. | `sdk/base/lib/interfaces/Host.ts` | SDK `MultiHost.bindPort()` — no changes needed | | `core/src/db/model/public.rs` | Public DB model — port forward mapping | +- [ ] Remove Tor from StartOS core - @dr-bonez + + **Goal**: Remove all built-in Tor functionality from StartOS. Tor will now be provided by a service + running on StartOS rather than being integrated into the core OS. + + **Scope**: Remove the Arti-based Tor client, onion address management in the networking stack, Tor + hidden service creation in the vhost/net controller layers, and any Tor-specific configuration in the + database models. The core should no longer start, manage, or depend on a Tor daemon. + + **Key areas to modify**: + + | Area | Change | + |------|--------| + | `core/src/net/tor/` | Remove the Tor module entirely (Arti client, hidden service management) | + | `core/src/net/net_controller.rs` | Remove Tor hidden service creation/teardown in `update()` | + | `core/src/net/host/address.rs` | Remove onion address CRUD RPC endpoints (`address.onion.add`, `address.onion.remove`) | + | `core/src/net/host/binding.rs` | Remove onion-related fields from `NetInfo` | + | `core/src/net/service_interface.rs` | Remove onion-related variants from `HostnameInfo` | + | `core/src/db/model/` | Remove Tor/onion fields from public and private DB models | + | `sdk/base/lib/interfaces/Host.ts` | Remove onion-related types and options from the SDK | + | `web/projects/ui/` | Remove onion address UI from interfaces pages | + | `Cargo.toml` / dependencies | Remove Arti and related Tor crate dependencies | + + **Migration**: Existing onion address data in the database should be cleaned up during migration. + Services that previously relied on the OS-provided Tor integration will need to use the new Tor + service instead (service-level integration is out of scope for this task). + - [ ] Auto-configure port forwards via UPnP/NAT-PMP/PCP - @dr-bonez **Blocked by**: "Support preferred external ports besides 443" (must be implemented and tested diff --git a/core/Cargo.lock b/core/Cargo.lock index 484974f9f..0e96d08e4 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -26,22 +26,10 @@ dependencies = [ "cfg-if", "cipher 0.3.0", "cpufeatures", - "ctr 0.8.0", + "ctr", "opaque-debug", ] -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher 0.4.4", - "cpufeatures", - "zeroize", -] - [[package]] name = "ahash" version = "0.7.8" @@ -95,52 +83,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "amplify" -version = "4.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f7fb4ac7c881e54a8e7015e399b6112a2a5bc958b6c89ac510840ff20273b31" -dependencies = [ - "amplify_derive", - "amplify_num", - "ascii", - "getrandom 0.2.17", - "getrandom 0.3.4", - "wasm-bindgen", -] - -[[package]] -name = "amplify_derive" -version = "4.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a6309e6b8d89b36b9f959b7a8fa093583b94922a0f6438a24fb08936de4d428" -dependencies = [ - "amplify_syn", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "amplify_num" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99bcb75a2982047f733547042fc3968c0f460dfcf7d90b90dea3b2744580e9ad" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "amplify_syn" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7736fb8d473c0d83098b5bac44df6a561e20470375cd8bcae30516dc889fd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "android_system_properties" version = "0.1.5" @@ -150,12 +92,6 @@ dependencies = [ "libc", ] -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - [[package]] name = "ansi-regex" version = "0.1.0" @@ -230,15 +166,6 @@ dependencies = [ "object 0.32.2", ] -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] - [[package]] name = "arc-swap" version = "1.8.0" @@ -272,62 +199,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -[[package]] -name = "arti-client" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "async-trait", - "cfg-if", - "derive-deftly", - "derive_builder_fork_arti", - "derive_more 2.1.1", - "educe", - "fs-mistrust", - "futures", - "hostname-validator", - "humantime", - "humantime-serde", - "libc", - "once_cell", - "postage", - "rand 0.9.2", - "safelog", - "serde", - "thiserror 2.0.17", - "time", - "tor-async-utils", - "tor-basic-utils", - "tor-chanmgr", - "tor-circmgr", - "tor-config", - "tor-config-path", - "tor-dirmgr", - "tor-error", - "tor-guardmgr", - "tor-hsclient", - "tor-hscrypto", - "tor-hsservice", - "tor-keymgr", - "tor-linkspec", - "tor-llcrypto", - "tor-memquota", - "tor-netdir", - "tor-netdoc", - "tor-persist", - "tor-proto", - "tor-protover", - "tor-rtcompat", - "tracing", - "void", -] - -[[package]] -name = "ascii" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" - [[package]] name = "ascii-canvas" version = "3.0.0" @@ -343,7 +214,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" dependencies = [ - "asn1-rs-derive 0.5.1", + "asn1-rs-derive", "asn1-rs-impl", "displaydoc", "nom 7.1.3", @@ -353,21 +224,6 @@ dependencies = [ "time", ] -[[package]] -name = "asn1-rs" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" -dependencies = [ - "asn1-rs-derive 0.6.0", - "asn1-rs-impl", - "displaydoc", - "nom 7.1.3", - "num-traits", - "rusticata-macros", - "thiserror 2.0.17", -] - [[package]] name = "asn1-rs-derive" version = "0.5.1" @@ -380,18 +236,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "asn1-rs-derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", - "synstructure", -] - [[package]] name = "asn1-rs-impl" version = "0.2.0" @@ -403,12 +247,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "assert_matches" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" - [[package]] name = "async-acme" version = "0.6.0" @@ -474,7 +312,6 @@ checksum = "d10e4f991a553474232bc0a31799f6d24b034a84c0971d80d2e2f78b2e576e40" dependencies = [ "compression-codecs", "compression-core", - "futures-io", "pin-project-lite", "tokio", ] @@ -493,21 +330,6 @@ dependencies = [ "slab", ] -[[package]] -name = "async-global-executor" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" -dependencies = [ - "async-channel 2.5.0", - "async-executor", - "async-io", - "async-lock", - "blocking", - "futures-lite", - "once_cell", -] - [[package]] name = "async-io" version = "2.6.0" @@ -537,18 +359,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "async-native-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9343dc5acf07e79ff82d0c37899f079db3534d99f189a1837c8e549c99405bec" -dependencies = [ - "futures-util", - "native-tls", - "thiserror 1.0.69", - "url", -] - [[package]] name = "async-process" version = "2.5.0" @@ -596,33 +406,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "async-std" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" -dependencies = [ - "async-channel 1.9.0", - "async-global-executor", - "async-io", - "async-lock", - "async-process", - "crossbeam-utils", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite", - "gloo-timers", - "kv-log-macro", - "log", - "memchr", - "once_cell", - "pin-project-lite", - "pin-utils", - "slab", - "wasm-bindgen-futures", -] - [[package]] name = "async-stream" version = "0.3.6" @@ -662,35 +445,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "async_executors" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a982d2f86de6137cc05c9db9a915a19886c97911f9790d04f174cede74be01a5" -dependencies = [ - "async-std", - "blanket", - "futures-core", - "futures-task", - "futures-util", - "pin-project", - "rustc_version", - "tokio", -] - -[[package]] -name = "asynchronous-codec" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233" -dependencies = [ - "bytes", - "futures-sink", - "futures-util", - "memchr", - "pin-project-lite", -] - [[package]] name = "atoi" version = "2.0.0" @@ -700,21 +454,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "atomic" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" - -[[package]] -name = "atomic" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" -dependencies = [ - "bytemuck", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -816,7 +555,7 @@ dependencies = [ "miniz_oxide", "object 0.37.3", "rustc-demangle", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -845,12 +584,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" -[[package]] -name = "base32" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" - [[package]] name = "base32" version = "0.5.1" @@ -863,12 +596,6 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1adf9755786e27479693dedd3271691a92b5e242ab139cacb9fb8e7fb5381111" -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.21.7" @@ -999,15 +726,6 @@ dependencies = [ "wyz 0.5.1", ] -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest 0.10.7", -] - [[package]] name = "blake2b_simd" version = "1.0.4" @@ -1035,17 +753,6 @@ dependencies = [ "rayon-core", ] -[[package]] -name = "blanket" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0b121a9fe0df916e362fb3271088d071159cdf11db0e4182d02152850756eff" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "block" version = "0.1.6" @@ -1058,7 +765,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "block-padding", "generic-array", ] @@ -1071,12 +777,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block-padding" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" - [[package]] name = "blocking" version = "1.6.2" @@ -1090,12 +790,6 @@ dependencies = [ "piper", ] -[[package]] -name = "bounded-vec-deque" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2225b558afc76c596898f5f1b3fc35cfce0eb1b13635cbd7d1b2a7177dc10ccd" - [[package]] name = "brotli" version = "8.0.2" @@ -1124,7 +818,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", - "regex-automata 0.4.13", "serde", ] @@ -1134,12 +827,6 @@ version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" -[[package]] -name = "by_address" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" - [[package]] name = "bytemuck" version = "1.24.0" @@ -1164,17 +851,6 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" -[[package]] -name = "caret" -version = "0.5.3" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" - -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - [[package]] name = "cc" version = "1.2.53" @@ -1222,7 +898,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -1279,7 +955,6 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", - "zeroize", ] [[package]] @@ -1301,7 +976,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.1", + "strsim", "terminal_size", ] @@ -1342,17 +1017,6 @@ dependencies = [ "cc", ] -[[package]] -name = "coarsetime" -version = "0.1.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e58eb270476aa4fc7843849f8a35063e8743b4dbcdf6dd0f8ea0886980c204c2" -dependencies = [ - "libc", - "wasix", - "wasm-bindgen", -] - [[package]] name = "color-eyre" version = "0.6.5" @@ -1414,7 +1078,6 @@ dependencies = [ "brotli", "compression-core", "flate2", - "liblzma", "memchr", "zstd", "zstd-safe", @@ -1520,12 +1183,6 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - [[package]] name = "convert_case" version = "0.6.0" @@ -1555,15 +1212,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "cookie-factory" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" -dependencies = [ - "futures", -] - [[package]] name = "cookie_store" version = "0.22.0" @@ -1667,52 +1315,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "criterion" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" -dependencies = [ - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "is-terminal", - "itertools 0.10.5", - "num-traits", - "once_cell", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_derive", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-cycles-per-byte" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1029452fa751c93f8834962dd74807d69f0a6c7624d5b06625b393aeb6a14fc2" -dependencies = [ - "cfg-if", - "criterion", -] - -[[package]] -name = "criterion-plot" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" -dependencies = [ - "cast", - "itertools 0.10.5", -] - [[package]] name = "critical-section" version = "1.2.0" @@ -1770,7 +1372,7 @@ checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ "bitflags 2.10.0", "crossterm_winapi", - "derive_more 2.1.1", + "derive_more", "document-features", "futures-core", "mio", @@ -1818,23 +1420,13 @@ dependencies = [ "typenum", ] -[[package]] -name = "crypto-mac" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" -dependencies = [ - "generic-array", - "subtle", -] - [[package]] name = "csscolorparser" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fda6aace1fbef3aa217b27f4c8d7d071ef2a70a5ca51050b1f17d40299d3f16" dependencies = [ - "phf 0.11.3", + "phf", ] [[package]] @@ -1867,15 +1459,6 @@ dependencies = [ "cipher 0.3.0", ] -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher 0.4.4", -] - [[package]] name = "curve25519-dalek" version = "3.2.0" @@ -1916,76 +1499,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "darling" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" -dependencies = [ - "darling_core 0.14.4", - "darling_macro 0.14.4", -] - -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - -[[package]] -name = "darling_core" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.10.0", - "syn 1.0.109", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.11.1", - "syn 2.0.114", -] - -[[package]] -name = "darling_macro" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" -dependencies = [ - "darling_core 0.14.4", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core 0.21.3", - "quote", - "syn 2.0.114", -] - [[package]] name = "data-encoding" version = "2.10.0" @@ -2010,7 +1523,7 @@ version = "9.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" dependencies = [ - "asn1-rs 0.6.2", + "asn1-rs", "displaydoc", "nom 7.1.3", "num-bigint", @@ -2018,20 +1531,6 @@ dependencies = [ "rusticata-macros", ] -[[package]] -name = "der-parser" -version = "10.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" -dependencies = [ - "asn1-rs 0.7.1", - "cookie-factory", - "displaydoc", - "nom 7.1.3", - "num-traits", - "rusticata-macros", -] - [[package]] name = "der_derive" version = "0.7.3" @@ -2050,90 +1549,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", - "serde_core", -] - -[[package]] -name = "derive-deftly" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957bb73a3a9c0bbcac67e129b81954661b3cfcb9e28873d8441f91b54852e77a" -dependencies = [ - "derive-deftly-macros", - "heck 0.5.0", -] - -[[package]] -name = "derive-deftly-macros" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea41269bd490d251b9eca50ccb43117e641cc68b129849757c15ece88fe0574" -dependencies = [ - "heck 0.5.0", - "indexmap 2.13.0", - "itertools 0.14.0", - "proc-macro-crate", - "proc-macro2", - "quote", - "sha3 0.10.8", - "strum", - "syn 2.0.114", - "void", -] - -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "derive_builder_core_fork_arti" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24c1b715c79be6328caa9a5e1a387a196ea503740f0722ec3dd8f67a9e72314d" -dependencies = [ - "darling 0.14.4", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "derive_builder_fork_arti" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3eae24d595f4d0ecc90a9a5a6d11c2bd8dafe2375ec4a1ec63250e5ade7d228" -dependencies = [ - "derive_builder_macro_fork_arti", -] - -[[package]] -name = "derive_builder_macro_fork_arti" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69887769a2489cd946bf782eb2b1bb2cb7bc88551440c94a765d4f040c08ebf3" -dependencies = [ - "derive_builder_core_fork_arti", - "syn 1.0.109", -] - -[[package]] -name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "convert_case 0.4.0", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.114", ] [[package]] @@ -2156,7 +1571,6 @@ dependencies = [ "quote", "rustc_version", "syn 2.0.114", - "unicode-xid", ] [[package]] @@ -2186,24 +1600,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "directories" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys", -] - [[package]] name = "dirs-next" version = "2.0.0" @@ -2214,18 +1610,6 @@ dependencies = [ "dirs-sys-next", ] -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.5.2", - "windows-sys 0.61.2", -] - [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -2233,7 +1617,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", - "redox_users 0.4.6", + "redox_users", "winapi", ] @@ -2296,12 +1680,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" -[[package]] -name = "downcast-rs" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" - [[package]] name = "dunce" version = "1.0.5" @@ -2314,33 +1692,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "dynasm" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7d4c414c94bc830797115b8e5f434d58e7e80cb42ba88508c14bc6ea270625" -dependencies = [ - "bitflags 2.10.0", - "byteorder", - "lazy_static", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "dynasmrt" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602f7458a3859195fb840e6e0cce5f4330dd9dfbfece0edaf31fe427af346f55" -dependencies = [ - "byteorder", - "dynasm", - "fnv", - "memmap2 0.9.9", -] - [[package]] name = "ecdsa" version = "0.16.9" @@ -2397,7 +1748,6 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek 4.1.3", "ed25519 2.2.3", - "merlin", "rand_core 0.6.4", "serde", "sha2 0.10.9", @@ -2406,18 +1756,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "educe" -version = "0.4.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0042ff8246a363dbe77d2ceedb073339e85a804b9a47636c6e016a9a32c05f" -dependencies = [ - "enum-ordinalize", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "either" version = "1.15.0" @@ -2517,19 +1855,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "enum-ordinalize" -version = "3.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf1fa3f06bbff1ea5b1a9c7b14aa992a39657db60a2759457328d7e058f49ee" -dependencies = [ - "num-bigint", - "num-traits", - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "enumflags2" version = "0.7.12" @@ -2557,18 +1882,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "equix" -version = "0.2.5" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "arrayvec 0.7.6", - "hashx", - "num-traits", - "thiserror 2.0.17", - "visibility", -] - [[package]] name = "errno" version = "0.3.14" @@ -2645,18 +1958,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - [[package]] name = "fastrand" version = "2.3.0" @@ -2688,19 +1989,6 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" -[[package]] -name = "figment" -version = "0.10.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" -dependencies = [ - "atomic 0.6.1", - "serde", - "toml 0.8.23", - "uncased", - "version_check", -] - [[package]] name = "filedescriptor" version = "0.8.3" @@ -2730,12 +2018,6 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" -[[package]] -name = "fixed-capacity-vec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b31a14f5ee08ed1a40e1252b35af18bed062e3f39b69aab34decde36bc43e40" - [[package]] name = "fixedbitset" version = "0.4.2" @@ -2752,12 +2034,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "fluid-let" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "749cff877dc1af878a0b31a41dd221a753634401ea0ef2f87b62d3171522485a" - [[package]] name = "fnv" version = "1.0.7" @@ -2809,20 +2085,6 @@ dependencies = [ "itertools 0.8.2", ] -[[package]] -name = "fs-mistrust" -version = "0.10.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "derive_builder_fork_arti", - "dirs", - "libc", - "pwd-grp", - "serde", - "thiserror 2.0.17", - "walkdir", -] - [[package]] name = "fs2" version = "0.4.3" @@ -2839,36 +2101,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" -[[package]] -name = "fslock" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "fslock-arti-fork" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b21bd626aaab7b904b20bef6d9e06298914a0c8d9fb8b010483766b2e532791" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "fslock-guard" -version = "0.2.4" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "fslock-arti-fork", - "thiserror 2.0.17", - "winapi", -] - [[package]] name = "funty" version = "1.1.0" @@ -2975,17 +2207,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "futures-rustls" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" -dependencies = [ - "futures-io", - "rustls 0.23.36", - "rustls-pki-types", -] - [[package]] name = "futures-sink" version = "0.3.31" @@ -3043,7 +2264,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75cec8bb4d3d32542cfcb9517f78366b52c17931e30d7ee1682c13686c19cee7" dependencies = [ "futures", - "futures-rustls 0.25.1", + "futures-rustls", "hyper", "log", "serde", @@ -3115,12 +2336,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "glob-match" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d" - [[package]] name = "globset" version = "0.4.18" @@ -3145,18 +2360,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "gpt" version = "4.1.0" @@ -3180,18 +2383,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "growable-bloom-filter" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d174ccb4ba660d431329e7f0797870d0a4281e36353ec4b4a3c5eab6c2cfb6f1" -dependencies = [ - "serde", - "serde_bytes", - "serde_derive", - "xxhash-rust", -] - [[package]] name = "h2" version = "0.4.13" @@ -3292,20 +2483,6 @@ dependencies = [ "hashbrown 0.15.5", ] -[[package]] -name = "hashx" -version = "0.3.4" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "arrayvec 0.7.6", - "blake2", - "dynasmrt", - "fixed-capacity-vec", - "hex", - "rand_core 0.9.5", - "thiserror 2.0.17", -] - [[package]] name = "hdrhistogram" version = "7.5.4" @@ -3427,17 +2604,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac 0.12.1", -] - -[[package]] -name = "hmac" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" -dependencies = [ - "crypto-mac", - "digest 0.9.0", + "hmac", ] [[package]] @@ -3466,15 +2633,9 @@ checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" dependencies = [ "cfg-if", "libc", - "windows-link 0.2.1", + "windows-link", ] -[[package]] -name = "hostname-validator" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f558a64ac9af88b5ba400d99b579451af0d39c6d360980045b91aac966d705e2" - [[package]] name = "http" version = "1.4.0" @@ -3526,16 +2687,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" -[[package]] -name = "humantime-serde" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" -dependencies = [ - "humantime", - "serde", -] - [[package]] name = "hyper" version = "1.8.1" @@ -3643,7 +2794,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core", ] [[package]] @@ -3758,12 +2909,6 @@ dependencies = [ "rustc-hash", ] -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "idna" version = "1.1.0" @@ -3894,7 +3039,6 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", - "serde", ] [[package]] @@ -3973,15 +3117,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "inventory" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" -dependencies = [ - "rustversion", -] - [[package]] name = "ipconfig" version = "0.3.2" @@ -4224,45 +3359,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "k12" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4dc5fdb62af2f520116927304f15d25b3c2667b4817b90efdc045194c912c54" -dependencies = [ - "digest 0.10.7", - "sha3 0.10.8", -] - -[[package]] -name = "keccak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" -dependencies = [ - "cpufeatures", -] - -[[package]] -name = "kqueue" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - [[package]] name = "kv" version = "0.24.0" @@ -4278,15 +3374,6 @@ dependencies = [ "toml 0.5.11", ] -[[package]] -name = "kv-log-macro" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" -dependencies = [ - "log", -] - [[package]] name = "lalrpop" version = "0.20.2" @@ -4404,27 +3491,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link 0.2.1", -] - -[[package]] -name = "liblzma" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73c36d08cad03a3fbe2c4e7bb3a9e84c57e4ee4135ed0b065cade3d98480c648" -dependencies = [ - "liblzma-sys", -] - -[[package]] -name = "liblzma-sys" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736" -dependencies = [ - "cc", - "libc", - "pkg-config", + "windows-link", ] [[package]] @@ -4454,17 +3521,6 @@ dependencies = [ "redox_syscall 0.7.0", ] -[[package]] -name = "libsqlite3-sys" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - [[package]] name = "libyml" version = "0.0.5" @@ -4519,9 +3575,6 @@ name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -dependencies = [ - "value-bag", -] [[package]] name = "lru-slab" @@ -4618,18 +3671,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "merlin" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" -dependencies = [ - "byteorder", - "keccak", - "rand_core 0.6.4", - "zeroize", -] - [[package]] name = "miette" version = "7.6.0" @@ -4854,38 +3895,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "notify" -version = "8.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" -dependencies = [ - "bitflags 2.10.0", - "inotify", - "kqueue", - "libc", - "log", - "mio", - "notify-types", - "walkdir", - "windows-sys 0.60.2", -] - -[[package]] -name = "notify-types" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" - -[[package]] -name = "ntapi" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c70f219e21142367c70c0b30c6a9e3a14d55b4d12a204d897fbec83a0363f081" -dependencies = [ - "winapi", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -5050,25 +4059,6 @@ dependencies = [ "objc_id", ] -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "objc2-io-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" -dependencies = [ - "libc", - "objc2-core-foundation", -] - [[package]] name = "objc_id" version = "0.1.1" @@ -5102,7 +4092,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" dependencies = [ - "asn1-rs 0.6.2", + "asn1-rs", ] [[package]] @@ -5121,14 +4111,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "oneshot-fused-workaround" -version = "0.2.3" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "futures", -] - [[package]] name = "onig" version = "6.5.1" @@ -5151,12 +4133,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - [[package]] name = "opaque-debug" version = "0.3.1" @@ -5236,21 +4212,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "ordered-float" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] - [[package]] name = "ordered-stream" version = "0.2.0" @@ -5261,15 +4222,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "os_str_bytes" -version = "6.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" -dependencies = [ - "memchr", -] - [[package]] name = "overload" version = "0.1.1" @@ -5371,15 +4323,9 @@ dependencies = [ "libc", "redox_syscall 0.5.18", "smallvec", - "windows-link 0.2.1", + "windows-link", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "patch-db" version = "0.1.0" @@ -5428,7 +4374,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest 0.10.7", - "hmac 0.12.1", + "hmac", ] [[package]] @@ -5515,19 +4461,8 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_macros 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_macros 0.13.1", - "phf_shared 0.13.1", - "serde", + "phf_macros", + "phf_shared", ] [[package]] @@ -5536,41 +4471,18 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ - "phf_shared 0.11.3", + "phf_shared", "rand 0.8.5", ] -[[package]] -name = "phf_generator" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" -dependencies = [ - "fastrand", - "phf_shared 0.13.1", -] - [[package]] name = "phf_macros" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "phf_macros" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" -dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", + "phf_generator", + "phf_shared", "proc-macro2", "quote", "syn 2.0.114", @@ -5585,15 +4497,6 @@ dependencies = [ "siphasher", ] -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", -] - [[package]] name = "pico-args" version = "0.5.0" @@ -5683,34 +4586,6 @@ dependencies = [ "time", ] -[[package]] -name = "plotters" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" - -[[package]] -name = "plotters-svg" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" -dependencies = [ - "plotters-backend", -] - [[package]] name = "polling" version = "3.11.0" @@ -5739,7 +4614,7 @@ checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e" dependencies = [ "anyhow", "bitflags 1.3.2", - "downcast-rs 1.2.1", + "downcast-rs", "filedescriptor", "lazy_static", "libc", @@ -5752,21 +4627,6 @@ dependencies = [ "winreg 0.10.1", ] -[[package]] -name = "postage" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" -dependencies = [ - "atomic 0.5.3", - "crossbeam-queue", - "futures", - "parking_lot 0.12.5", - "pin-project", - "static_assertions", - "thiserror 1.0.69", -] - [[package]] name = "potential_utf" version = "0.1.4" @@ -5840,17 +4700,6 @@ dependencies = [ "elliptic-curve", ] -[[package]] -name = "priority-queue" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93980406f12d9f8140ed5abe7155acb10bb1e69ea55c88960b9c2f117445ef96" -dependencies = [ - "equivalent", - "indexmap 2.13.0", - "serde", -] - [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -5860,28 +4709,6 @@ dependencies = [ "toml_edit 0.23.10+spec-1.0.0", ] -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "proc-macro2" version = "1.0.105" @@ -6012,18 +4839,6 @@ dependencies = [ "psl-types", ] -[[package]] -name = "pwd-grp" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e2023f41b5fcb7c30eb5300a5733edfaa9e0e0d502d51b586f65633fd39e40c" -dependencies = [ - "derive-deftly", - "libc", - "paste", - "thiserror 2.0.17", -] - [[package]] name = "pxfm" version = "0.1.27" @@ -6294,17 +5109,6 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "rand_jitter" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16df48f071248e67b8fc5e866d9448d45c08ad8b672baaaf796e2f15e606ff0" -dependencies = [ - "libc", - "rand_core 0.9.5", - "winapi", -] - [[package]] name = "rand_xorshift" version = "0.4.0" @@ -6323,16 +5127,6 @@ dependencies = [ "rand_core 0.9.5", ] -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", -] - [[package]] name = "rayon-core" version = "1.13.0" @@ -6355,15 +5149,6 @@ dependencies = [ "yasna", ] -[[package]] -name = "rdrand" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92195228612ac8eed47adbc2ed0f04e513a4ccb98175b6f2bd04d963b533655" -dependencies = [ - "rand_core 0.6.4", -] - [[package]] name = "redox_syscall" version = "0.2.16" @@ -6402,37 +5187,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 2.0.17", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "regex" version = "1.12.2" @@ -6544,18 +5298,13 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" -[[package]] -name = "retry-error" -version = "0.6.5" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" - [[package]] name = "rfc6979" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac 0.12.1", + "hmac", "subtle", ] @@ -6644,21 +5393,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rusqlite" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" -dependencies = [ - "bitflags 2.10.0", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", - "time", -] - [[package]] name = "rust-argon2" version = "3.0.0" @@ -6916,18 +5650,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" -[[package]] -name = "safelog" -version = "0.4.8" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "derive_more 2.1.1", - "educe", - "either", - "fluid-let", - "thiserror 2.0.17", -] - [[package]] name = "same-file" version = "1.0.6" @@ -6937,15 +5659,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "sanitize-filename" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc984f4f9ceb736a7bb755c3e3bd17dc56370af2600c9780dcc48c66453da34d" -dependencies = [ - "regex", -] - [[package]] name = "schannel" version = "0.1.28" @@ -6955,30 +5668,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - [[package]] name = "scoped-tls" version = "1.0.1" @@ -7070,26 +5759,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float", - "serde", -] - -[[package]] -name = "serde_bytes" -version = "0.11.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" -dependencies = [ - "serde", - "serde_core", -] - [[package]] name = "serde_cbor" version = "0.11.1" @@ -7128,16 +5797,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "serde_ignored" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115dffd5f3853e06e746965a20dcbae6ee747ae30b543d91b0e089668bb07798" -dependencies = [ - "serde", - "serde_core", -] - [[package]] name = "serde_json" version = "1.0.149" @@ -7215,37 +5874,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_with" -version = "3.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" -dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.13.0", - "schemars 0.9.0", - "schemars 1.2.0", - "serde_core", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" -dependencies = [ - "darling 0.21.3", - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -7332,28 +5960,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "sha3" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" -dependencies = [ - "block-buffer 0.9.0", - "digest 0.9.0", - "keccak", - "opaque-debug", -] - -[[package]] -name = "sha3" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" -dependencies = [ - "digest 0.10.7", - "keccak", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -7379,17 +5985,6 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" -[[package]] -name = "shellexpand" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" -dependencies = [ - "bstr", - "dirs", - "os_str_bytes", -] - [[package]] name = "shlex" version = "1.3.0" @@ -7483,28 +6078,6 @@ dependencies = [ "parking_lot 0.11.2", ] -[[package]] -name = "slotmap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" -dependencies = [ - "serde", - "version_check", -] - -[[package]] -name = "slotmap-careful" -version = "0.2.5" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "paste", - "serde", - "slotmap", - "thiserror 2.0.17", - "void", -] - [[package]] name = "smallstr" version = "0.3.1" @@ -7708,7 +6281,7 @@ dependencies = [ "futures-util", "hex", "hkdf", - "hmac 0.12.1", + "hmac", "home", "itoa", "log", @@ -7749,7 +6322,7 @@ dependencies = [ "proc-macro2", "quote", "regex-syntax 0.6.29", - "strsim 0.11.1", + "strsim", "syn 2.0.114", "unicode-width 0.1.14", ] @@ -7817,17 +6390,16 @@ dependencies = [ [[package]] name = "start-os" -version = "0.4.0-alpha.19" +version = "0.4.0-alpha.20" dependencies = [ - "aes 0.7.5", - "arti-client", + "aes", "async-acme", "async-compression", "async-stream", "async-trait", "axum", "backtrace-on-stack-overflow", - "base32 0.5.1", + "base32", "base64 0.22.1", "base64ct", "basic-cookies", @@ -7842,7 +6414,6 @@ dependencies = [ "const_format", "cookie", "cookie_store", - "curve25519-dalek 4.1.3", "der", "digest 0.10.7", "divrem", @@ -7858,7 +6429,7 @@ dependencies = [ "hashing-serializer", "hex", "hickory-server", - "hmac 0.12.1", + "hmac", "http", "http-body-util", "hyper", @@ -7917,7 +6488,6 @@ dependencies = [ "rpc-toolkit", "rust-argon2", "rust-i18n", - "safelog", "semver", "serde", "serde_json", @@ -7941,14 +6511,6 @@ dependencies = [ "tokio-tungstenite 0.26.2", "tokio-util", "toml 0.9.11+spec-1.1.0", - "tor-cell", - "tor-hscrypto", - "tor-hsservice", - "tor-keymgr", - "tor-llcrypto", - "tor-proto", - "tor-rtcompat", - "torut", "tower-service", "tracing", "tracing-error", @@ -7977,7 +6539,7 @@ checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" dependencies = [ "new_debug_unreachable", "parking_lot 0.12.5", - "phf_shared 0.11.3", + "phf_shared", "precomputed-hash", ] @@ -8010,12 +6572,6 @@ dependencies = [ "vte", ] -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "strsim" version = "0.11.1" @@ -8027,9 +6583,6 @@ name = "strum" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros", -] [[package]] name = "strum_macros" @@ -8133,20 +6686,6 @@ dependencies = [ "yaml-rust", ] -[[package]] -name = "sysinfo" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252800745060e7b9ffb7b2badbd8b31cfa4aa2e61af879d0a3bf2a317c20217d" -dependencies = [ - "libc", - "memchr", - "ntapi", - "objc2-core-foundation", - "objc2-io-kit", - "windows", -] - [[package]] name = "system-configuration" version = "0.6.1" @@ -8351,20 +6890,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", - "serde_core", "zerovec", ] -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "tinyvec" version = "1.10.0" @@ -8500,7 +7028,6 @@ checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", - "futures-io", "futures-sink", "pin-project-lite", "tokio", @@ -8647,1001 +7174,6 @@ dependencies = [ "tonic", ] -[[package]] -name = "tor-async-utils" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "derive-deftly", - "educe", - "futures", - "oneshot-fused-workaround", - "pin-project", - "postage", - "thiserror 2.0.17", - "void", -] - -[[package]] -name = "tor-basic-utils" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "derive_more 2.1.1", - "hex", - "itertools 0.14.0", - "libc", - "paste", - "rand 0.9.2", - "rand_chacha 0.9.0", - "serde", - "slab", - "smallvec", - "thiserror 2.0.17", -] - -[[package]] -name = "tor-bytes" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "bytes", - "derive-deftly", - "digest 0.10.7", - "educe", - "getrandom 0.3.4", - "safelog", - "thiserror 2.0.17", - "tor-error", - "tor-llcrypto", - "zeroize", -] - -[[package]] -name = "tor-cell" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "amplify", - "bitflags 2.10.0", - "bytes", - "caret", - "derive-deftly", - "derive_more 2.1.1", - "educe", - "itertools 0.14.0", - "paste", - "rand 0.9.2", - "smallvec", - "thiserror 2.0.17", - "tor-basic-utils", - "tor-bytes", - "tor-cert", - "tor-error", - "tor-hscrypto", - "tor-linkspec", - "tor-llcrypto", - "tor-memquota", - "tor-protover", - "tor-units", - "void", -] - -[[package]] -name = "tor-cert" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "caret", - "derive_builder_fork_arti", - "derive_more 2.1.1", - "digest 0.10.7", - "thiserror 2.0.17", - "tor-bytes", - "tor-checkable", - "tor-llcrypto", -] - -[[package]] -name = "tor-chanmgr" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "async-trait", - "caret", - "derive_builder_fork_arti", - "derive_more 2.1.1", - "educe", - "futures", - "oneshot-fused-workaround", - "postage", - "rand 0.9.2", - "safelog", - "serde", - "thiserror 2.0.17", - "tor-async-utils", - "tor-basic-utils", - "tor-cell", - "tor-config", - "tor-error", - "tor-keymgr", - "tor-linkspec", - "tor-llcrypto", - "tor-memquota", - "tor-netdir", - "tor-proto", - "tor-rtcompat", - "tor-socksproto", - "tor-units", - "tracing", - "void", -] - -[[package]] -name = "tor-checkable" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "humantime", - "signature 2.2.0", - "thiserror 2.0.17", - "tor-llcrypto", -] - -[[package]] -name = "tor-circmgr" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "amplify", - "async-trait", - "bounded-vec-deque", - "cfg-if", - "derive-deftly", - "derive_builder_fork_arti", - "derive_more 2.1.1", - "downcast-rs 2.0.2", - "dyn-clone", - "educe", - "futures", - "humantime-serde", - "itertools 0.14.0", - "once_cell", - "oneshot-fused-workaround", - "pin-project", - "rand 0.9.2", - "retry-error", - "safelog", - "serde", - "static_assertions", - "thiserror 2.0.17", - "tor-async-utils", - "tor-basic-utils", - "tor-cell", - "tor-chanmgr", - "tor-config", - "tor-error", - "tor-guardmgr", - "tor-linkspec", - "tor-memquota", - "tor-netdir", - "tor-netdoc", - "tor-persist", - "tor-proto", - "tor-protover", - "tor-relay-selection", - "tor-rtcompat", - "tor-units", - "tracing", - "void", - "weak-table", -] - -[[package]] -name = "tor-config" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "amplify", - "cfg-if", - "derive-deftly", - "derive_builder_fork_arti", - "educe", - "either", - "figment", - "fs-mistrust", - "futures", - "itertools 0.14.0", - "notify", - "paste", - "postage", - "regex", - "serde", - "serde-value", - "serde_ignored", - "strum", - "thiserror 2.0.17", - "toml 0.9.11+spec-1.1.0", - "tor-basic-utils", - "tor-error", - "tor-rtcompat", - "tracing", - "void", -] - -[[package]] -name = "tor-config-path" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "directories", - "serde", - "shellexpand", - "thiserror 2.0.17", - "tor-error", - "tor-general-addr", -] - -[[package]] -name = "tor-consdiff" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "digest 0.10.7", - "hex", - "thiserror 2.0.17", - "tor-llcrypto", -] - -[[package]] -name = "tor-dirclient" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "async-compression", - "base64ct", - "derive_more 2.1.1", - "futures", - "hex", - "http", - "httparse", - "httpdate", - "itertools 0.14.0", - "memchr", - "thiserror 2.0.17", - "tor-circmgr", - "tor-error", - "tor-hscrypto", - "tor-linkspec", - "tor-llcrypto", - "tor-netdoc", - "tor-proto", - "tor-rtcompat", - "tracing", -] - -[[package]] -name = "tor-dirmgr" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "async-trait", - "base64ct", - "derive_builder_fork_arti", - "derive_more 2.1.1", - "digest 0.10.7", - "educe", - "event-listener 5.4.1", - "fs-mistrust", - "fslock", - "futures", - "hex", - "humantime", - "humantime-serde", - "itertools 0.14.0", - "memmap2 0.9.9", - "oneshot-fused-workaround", - "paste", - "postage", - "rand 0.9.2", - "rusqlite", - "safelog", - "scopeguard", - "serde", - "serde_json", - "signature 2.2.0", - "static_assertions", - "strum", - "thiserror 2.0.17", - "time", - "tor-async-utils", - "tor-basic-utils", - "tor-checkable", - "tor-circmgr", - "tor-config", - "tor-consdiff", - "tor-dirclient", - "tor-error", - "tor-guardmgr", - "tor-llcrypto", - "tor-netdir", - "tor-netdoc", - "tor-persist", - "tor-proto", - "tor-protover", - "tor-rtcompat", - "tracing", -] - -[[package]] -name = "tor-error" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "derive_more 2.1.1", - "futures", - "paste", - "retry-error", - "static_assertions", - "strum", - "thiserror 2.0.17", - "tracing", - "void", -] - -[[package]] -name = "tor-general-addr" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "arbitrary", - "derive_more 2.1.1", - "thiserror 2.0.17", - "void", -] - -[[package]] -name = "tor-guardmgr" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "amplify", - "base64ct", - "derive-deftly", - "derive_builder_fork_arti", - "derive_more 2.1.1", - "dyn-clone", - "educe", - "futures", - "humantime", - "humantime-serde", - "itertools 0.14.0", - "num_enum", - "oneshot-fused-workaround", - "pin-project", - "postage", - "rand 0.9.2", - "safelog", - "serde", - "strum", - "thiserror 2.0.17", - "tor-async-utils", - "tor-basic-utils", - "tor-config", - "tor-error", - "tor-linkspec", - "tor-llcrypto", - "tor-netdir", - "tor-netdoc", - "tor-persist", - "tor-proto", - "tor-relay-selection", - "tor-rtcompat", - "tor-units", - "tracing", -] - -[[package]] -name = "tor-hsclient" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "async-trait", - "derive-deftly", - "derive_more 2.1.1", - "educe", - "either", - "futures", - "itertools 0.14.0", - "oneshot-fused-workaround", - "postage", - "rand 0.9.2", - "retry-error", - "safelog", - "slotmap-careful", - "strum", - "thiserror 2.0.17", - "tor-async-utils", - "tor-basic-utils", - "tor-bytes", - "tor-cell", - "tor-checkable", - "tor-circmgr", - "tor-config", - "tor-dirclient", - "tor-error", - "tor-hscrypto", - "tor-keymgr", - "tor-linkspec", - "tor-llcrypto", - "tor-memquota", - "tor-netdir", - "tor-netdoc", - "tor-persist", - "tor-proto", - "tor-protover", - "tor-rtcompat", - "tracing", -] - -[[package]] -name = "tor-hscrypto" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "cipher 0.4.4", - "data-encoding", - "derive-deftly", - "derive_more 2.1.1", - "digest 0.10.7", - "equix", - "hex", - "humantime", - "itertools 0.14.0", - "paste", - "rand 0.9.2", - "safelog", - "serde", - "signature 2.2.0", - "subtle", - "thiserror 2.0.17", - "tor-basic-utils", - "tor-bytes", - "tor-error", - "tor-key-forge", - "tor-llcrypto", - "tor-memquota", - "tor-units", - "void", - "zeroize", -] - -[[package]] -name = "tor-hsservice" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "amplify", - "async-trait", - "base64ct", - "cfg-if", - "derive-deftly", - "derive_builder_fork_arti", - "derive_more 2.1.1", - "digest 0.10.7", - "educe", - "fs-mistrust", - "futures", - "growable-bloom-filter", - "hex", - "humantime", - "itertools 0.14.0", - "k12", - "once_cell", - "oneshot-fused-workaround", - "postage", - "rand 0.9.2", - "rand_core 0.9.5", - "retry-error", - "safelog", - "serde", - "serde_with", - "strum", - "thiserror 2.0.17", - "tor-async-utils", - "tor-basic-utils", - "tor-bytes", - "tor-cell", - "tor-circmgr", - "tor-config", - "tor-config-path", - "tor-dirclient", - "tor-error", - "tor-hscrypto", - "tor-keymgr", - "tor-linkspec", - "tor-llcrypto", - "tor-log-ratelim", - "tor-netdir", - "tor-netdoc", - "tor-persist", - "tor-proto", - "tor-protover", - "tor-relay-selection", - "tor-rtcompat", - "tracing", - "void", -] - -[[package]] -name = "tor-key-forge" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "derive-deftly", - "derive_more 2.1.1", - "downcast-rs 2.0.2", - "paste", - "rand 0.9.2", - "signature 2.2.0", - "ssh-key", - "thiserror 2.0.17", - "tor-bytes", - "tor-cert", - "tor-checkable", - "tor-error", - "tor-llcrypto", -] - -[[package]] -name = "tor-keymgr" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "amplify", - "arrayvec 0.7.6", - "cfg-if", - "derive-deftly", - "derive_builder_fork_arti", - "derive_more 2.1.1", - "downcast-rs 2.0.2", - "dyn-clone", - "fs-mistrust", - "glob-match", - "humantime", - "inventory", - "itertools 0.14.0", - "rand 0.9.2", - "safelog", - "serde", - "signature 2.2.0", - "ssh-key", - "thiserror 2.0.17", - "tor-basic-utils", - "tor-bytes", - "tor-config", - "tor-config-path", - "tor-error", - "tor-hscrypto", - "tor-key-forge", - "tor-llcrypto", - "tor-persist", - "tracing", - "visibility", - "walkdir", - "zeroize", -] - -[[package]] -name = "tor-linkspec" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "base64ct", - "by_address", - "caret", - "derive-deftly", - "derive_builder_fork_arti", - "derive_more 2.1.1", - "hex", - "itertools 0.14.0", - "safelog", - "serde", - "serde_with", - "strum", - "thiserror 2.0.17", - "tor-basic-utils", - "tor-bytes", - "tor-config", - "tor-llcrypto", - "tor-memquota", - "tor-protover", -] - -[[package]] -name = "tor-llcrypto" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "aes 0.8.4", - "base64ct", - "ctr 0.9.2", - "curve25519-dalek 4.1.3", - "der-parser 10.0.0", - "derive-deftly", - "derive_more 2.1.1", - "digest 0.10.7", - "ed25519-dalek 2.2.0", - "educe", - "getrandom 0.3.4", - "hex", - "rand 0.9.2", - "rand_chacha 0.9.0", - "rand_core 0.6.4", - "rand_core 0.9.5", - "rand_jitter", - "rdrand", - "rsa", - "safelog", - "serde", - "sha1", - "sha2 0.10.9", - "sha3 0.10.8", - "signature 2.2.0", - "subtle", - "thiserror 2.0.17", - "tor-memquota", - "visibility", - "x25519-dalek", - "zeroize", -] - -[[package]] -name = "tor-log-ratelim" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "futures", - "humantime", - "thiserror 2.0.17", - "tor-error", - "tor-rtcompat", - "tracing", - "weak-table", -] - -[[package]] -name = "tor-memquota" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "cfg-if", - "derive-deftly", - "derive_more 2.1.1", - "dyn-clone", - "educe", - "futures", - "itertools 0.14.0", - "paste", - "pin-project", - "serde", - "slotmap-careful", - "static_assertions", - "sysinfo", - "thiserror 2.0.17", - "tor-async-utils", - "tor-basic-utils", - "tor-config", - "tor-error", - "tor-log-ratelim", - "tor-rtcompat", - "tracing", - "void", -] - -[[package]] -name = "tor-netdir" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "async-trait", - "bitflags 2.10.0", - "derive_more 2.1.1", - "digest 0.10.7", - "futures", - "hex", - "humantime", - "itertools 0.14.0", - "num_enum", - "rand 0.9.2", - "serde", - "static_assertions", - "strum", - "thiserror 2.0.17", - "time", - "tor-basic-utils", - "tor-error", - "tor-hscrypto", - "tor-linkspec", - "tor-llcrypto", - "tor-netdoc", - "tor-protover", - "tor-units", - "tracing", - "typed-index-collections", -] - -[[package]] -name = "tor-netdoc" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "amplify", - "base64ct", - "bitflags 2.10.0", - "cipher 0.4.4", - "derive-deftly", - "derive_builder_fork_arti", - "derive_more 2.1.1", - "digest 0.10.7", - "educe", - "hex", - "humantime", - "itertools 0.14.0", - "memchr", - "paste", - "phf 0.13.1", - "rand 0.9.2", - "serde", - "serde_with", - "signature 2.2.0", - "smallvec", - "strum", - "subtle", - "thiserror 2.0.17", - "time", - "tinystr", - "tor-basic-utils", - "tor-bytes", - "tor-cell", - "tor-cert", - "tor-checkable", - "tor-error", - "tor-hscrypto", - "tor-linkspec", - "tor-llcrypto", - "tor-protover", - "tor-units", - "void", - "weak-table", - "zeroize", -] - -[[package]] -name = "tor-persist" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "amplify", - "derive-deftly", - "derive_more 2.1.1", - "filetime", - "fs-mistrust", - "fslock", - "fslock-guard", - "futures", - "itertools 0.14.0", - "oneshot-fused-workaround", - "paste", - "sanitize-filename", - "serde", - "serde_json", - "thiserror 2.0.17", - "time", - "tor-async-utils", - "tor-basic-utils", - "tor-error", - "tracing", - "void", -] - -[[package]] -name = "tor-proto" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "amplify", - "asynchronous-codec", - "bitvec 1.0.1", - "bytes", - "caret", - "cfg-if", - "cipher 0.4.4", - "coarsetime", - "criterion-cycles-per-byte", - "derive-deftly", - "derive_builder_fork_arti", - "derive_more 2.1.1", - "digest 0.10.7", - "educe", - "futures", - "futures-util", - "hkdf", - "hmac 0.12.1", - "itertools 0.14.0", - "oneshot-fused-workaround", - "pin-project", - "postage", - "rand 0.9.2", - "rand_core 0.9.5", - "safelog", - "slotmap-careful", - "smallvec", - "static_assertions", - "subtle", - "sync_wrapper", - "thiserror 2.0.17", - "tokio", - "tokio-util", - "tor-async-utils", - "tor-basic-utils", - "tor-bytes", - "tor-cell", - "tor-cert", - "tor-checkable", - "tor-config", - "tor-error", - "tor-hscrypto", - "tor-linkspec", - "tor-llcrypto", - "tor-log-ratelim", - "tor-memquota", - "tor-protover", - "tor-rtcompat", - "tor-rtmock", - "tor-units", - "tracing", - "typenum", - "visibility", - "void", - "zeroize", -] - -[[package]] -name = "tor-protover" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "caret", - "paste", - "serde_with", - "thiserror 2.0.17", - "tor-bytes", -] - -[[package]] -name = "tor-relay-selection" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "rand 0.9.2", - "serde", - "tor-basic-utils", - "tor-linkspec", - "tor-netdir", - "tor-netdoc", -] - -[[package]] -name = "tor-rtcompat" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "arbitrary", - "async-io", - "async-native-tls", - "async-std", - "async-trait", - "async_executors", - "asynchronous-codec", - "coarsetime", - "derive_more 2.1.1", - "dyn-clone", - "educe", - "futures", - "futures-rustls 0.26.0", - "hex", - "libc", - "native-tls", - "paste", - "pin-project", - "rustls-pki-types", - "rustls-webpki 0.103.9", - "thiserror 2.0.17", - "tokio", - "tokio-util", - "tor-error", - "tor-general-addr", - "tracing", - "void", -] - -[[package]] -name = "tor-rtmock" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "amplify", - "assert_matches", - "async-trait", - "derive-deftly", - "derive_more 2.1.1", - "educe", - "futures", - "humantime", - "itertools 0.14.0", - "oneshot-fused-workaround", - "pin-project", - "priority-queue", - "slotmap-careful", - "strum", - "thiserror 2.0.17", - "tor-error", - "tor-general-addr", - "tor-rtcompat", - "tracing", - "tracing-test", - "void", -] - -[[package]] -name = "tor-socksproto" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "amplify", - "caret", - "derive-deftly", - "educe", - "safelog", - "subtle", - "thiserror 2.0.17", - "tor-bytes", - "tor-error", -] - -[[package]] -name = "tor-units" -version = "0.33.0" -source = "git+https://github.com/Start9Labs/arti.git?branch=patch%2Fdisable-exit#24730694701a83432d791d80802db8bda0699700" -dependencies = [ - "derive-deftly", - "derive_more 2.1.1", - "serde", - "thiserror 2.0.17", - "tor-memquota", -] - -[[package]] -name = "torut" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99febc413f26cf855b3a309c5872edff5c31e0ffe9c2fce5681868761df36f69" -dependencies = [ - "base32 0.4.0", - "base64 0.13.1", - "derive_more 0.99.20", - "ed25519-dalek 1.0.1", - "hex", - "hmac 0.11.0", - "rand 0.7.3", - "serde", - "serde_derive", - "sha2 0.9.9", - "sha3 0.9.1", - "tokio", -] - [[package]] name = "tower" version = "0.5.3" @@ -9786,27 +7318,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "tracing-test" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68" -dependencies = [ - "tracing-core", - "tracing-subscriber", - "tracing-test-macro", -] - -[[package]] -name = "tracing-test-macro" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" -dependencies = [ - "quote", - "syn 2.0.114", -] - [[package]] name = "triomphe" version = "0.1.15" @@ -9902,16 +7413,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "typed-index-collections" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5318ee4ce62a4e948a33915574021a7a953d83e84fba6e25c72ffcfd7dad35ff" -dependencies = [ - "bincode 2.0.1", - "serde", -] - [[package]] name = "typenum" version = "1.19.0" @@ -9941,15 +7442,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" -[[package]] -name = "uncased" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" -dependencies = [ - "version_check", -] - [[package]] name = "unicase" version = "2.9.0" @@ -10086,12 +7578,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "value-bag" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" - [[package]] name = "vcpkg" version = "0.2.15" @@ -10110,17 +7596,6 @@ version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" -[[package]] -name = "visibility" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "visit-rs" version = "0.1.9" @@ -10144,12 +7619,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "void" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" - [[package]] name = "vte" version = "0.14.1" @@ -10214,15 +7683,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" -[[package]] -name = "wasix" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1757e0d1f8456693c7e5c6c629bdb54884e032aa0bb53c155f6a39f94440d332" -dependencies = [ - "wasi 0.11.1+wasi-snapshot-preview1", -] - [[package]] name = "wasm-bindgen" version = "0.2.108" @@ -10302,7 +7762,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715" dependencies = [ "bitflags 1.3.2", - "downcast-rs 1.2.1", + "downcast-rs", "libc", "nix 0.24.3", "scoped-tls", @@ -10368,12 +7828,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "weak-table" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323f4da9523e9a669e1eaf9c6e763892769b1d38c623913647bfdc1532fe4549" - [[package]] name = "web-sys" version = "0.3.85" @@ -10489,41 +7943,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core 0.61.2", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - [[package]] name = "windows-core" version = "0.62.2" @@ -10532,20 +7951,9 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] @@ -10570,46 +7978,21 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - [[package]] name = "windows-registry" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] @@ -10618,16 +8001,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -10636,7 +8010,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -10690,7 +8064,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -10745,7 +8119,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.2.1", + "windows-link", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -10756,15 +8130,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -11049,9 +8414,9 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" dependencies = [ - "asn1-rs 0.6.2", + "asn1-rs", "data-encoding", - "der-parser 9.0.0", + "der-parser", "lazy_static", "nom 7.1.3", "oid-registry", @@ -11309,7 +8674,6 @@ version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ - "serde", "yoke", "zerofrom", "zerovec-derive", diff --git a/core/Cargo.toml b/core/Cargo.toml index 0256ac433..9937dfaa1 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -15,7 +15,7 @@ license = "MIT" name = "start-os" readme = "README.md" repository = "https://github.com/Start9Labs/start-os" -version = "0.4.0-alpha.19" # VERSION_BUMP +version = "0.4.0-alpha.20" # VERSION_BUMP [lib] name = "startos" @@ -42,17 +42,6 @@ name = "tunnelbox" path = "src/main/tunnelbox.rs" [features] -arti = [ - "arti-client", - "safelog", - "tor-cell", - "tor-hscrypto", - "tor-hsservice", - "tor-keymgr", - "tor-llcrypto", - "tor-proto", - "tor-rtcompat", -] beta = [] console = ["console-subscriber", "tokio/tracing"] default = [] @@ -62,16 +51,6 @@ unstable = ["backtrace-on-stack-overflow"] [dependencies] aes = { version = "0.7.5", features = ["ctr"] } -arti-client = { version = "0.33", features = [ - "compression", - "ephemeral-keystore", - "experimental-api", - "onion-service-client", - "onion-service-service", - "rustls", - "static", - "tokio", -], default-features = false, git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true } async-acme = { version = "0.6.0", git = "https://github.com/dr-bonez/async-acme.git", features = [ "use_rustls", "use_tokio", @@ -100,7 +79,6 @@ console-subscriber = { version = "0.5.0", optional = true } const_format = "0.2.34" cookie = "0.18.0" cookie_store = "0.22.0" -curve25519-dalek = "4.1.3" der = { version = "0.7.9", features = ["derive", "pem"] } digest = "0.10.7" divrem = "1.0.0" @@ -216,7 +194,6 @@ rpassword = "7.2.0" rust-argon2 = "3.0.0" rust-i18n = "3.1.5" rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git" } -safelog = { version = "0.4.8", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true } semver = { version = "1.0.20", features = ["serde"] } serde = { version = "1.0", features = ["derive", "rc"] } serde_cbor = { package = "ciborium", version = "0.2.1" } @@ -244,23 +221,6 @@ tokio-stream = { version = "0.1.14", features = ["io-util", "net", "sync"] } tokio-tar = { git = "https://github.com/dr-bonez/tokio-tar.git" } tokio-tungstenite = { version = "0.26.2", features = ["native-tls", "url"] } tokio-util = { version = "0.7.9", features = ["io"] } -tor-cell = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true } -tor-hscrypto = { version = "0.33", features = [ - "full", -], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true } -tor-hsservice = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true } -tor-keymgr = { version = "0.33", features = [ - "ephemeral-keystore", -], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true } -tor-llcrypto = { version = "0.33", features = [ - "full", -], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true } -tor-proto = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true } -tor-rtcompat = { version = "0.33", features = [ - "rustls", - "tokio", -], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true } -torut = "0.2.1" tower-service = "0.3.3" tracing = "0.1.39" tracing-error = "0.2.0" diff --git a/core/src/account.rs b/core/src/account.rs index d583c95f7..2216fc361 100644 --- a/core/src/account.rs +++ b/core/src/account.rs @@ -8,7 +8,6 @@ use openssl::x509::X509; use crate::db::model::DatabaseModel; use crate::hostname::{Hostname, generate_hostname, generate_id}; use crate::net::ssl::{gen_nistp256, make_root_cert}; -use crate::net::tor::TorSecretKey; use crate::prelude::*; use crate::util::serde::Pem; @@ -26,7 +25,6 @@ pub struct AccountInfo { pub server_id: String, pub hostname: Hostname, pub password: String, - pub tor_keys: Vec, pub root_ca_key: PKey, pub root_ca_cert: X509, pub ssh_key: ssh_key::PrivateKey, @@ -36,7 +34,6 @@ impl AccountInfo { pub fn new(password: &str, start_time: SystemTime) -> Result { let server_id = generate_id(); let hostname = generate_hostname(); - let tor_key = vec![TorSecretKey::generate()]; let root_ca_key = gen_nistp256()?; let root_ca_cert = make_root_cert(&root_ca_key, &hostname, start_time)?; let ssh_key = ssh_key::PrivateKey::from(ssh_key::private::Ed25519Keypair::random( @@ -48,7 +45,6 @@ impl AccountInfo { server_id, hostname, password: hash_password(password)?, - tor_keys: tor_key, root_ca_key, root_ca_cert, ssh_key, @@ -61,17 +57,6 @@ impl AccountInfo { let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?); let password = db.as_private().as_password().de()?; let key_store = db.as_private().as_key_store(); - let tor_addrs = db - .as_public() - .as_server_info() - .as_network() - .as_host() - .as_onions() - .de()?; - let tor_keys = tor_addrs - .into_iter() - .map(|tor_addr| key_store.as_onion().get_key(&tor_addr)) - .collect::>()?; let cert_store = key_store.as_local_certs(); let root_ca_key = cert_store.as_root_key().de()?.0; let root_ca_cert = cert_store.as_root_cert().de()?.0; @@ -82,7 +67,6 @@ impl AccountInfo { server_id, hostname, password, - tor_keys, root_ca_key, root_ca_cert, ssh_key, @@ -97,17 +81,6 @@ impl AccountInfo { server_info .as_pubkey_mut() .ser(&self.ssh_key.public_key().to_openssh()?)?; - server_info - .as_network_mut() - .as_host_mut() - .as_onions_mut() - .ser( - &self - .tor_keys - .iter() - .map(|tor_key| tor_key.onion_address()) - .collect(), - )?; server_info.as_password_hash_mut().ser(&self.password)?; db.as_private_mut().as_password_mut().ser(&self.password)?; db.as_private_mut() @@ -117,9 +90,6 @@ impl AccountInfo { .as_developer_key_mut() .ser(Pem::new_ref(&self.developer_key))?; let key_store = db.as_private_mut().as_key_store_mut(); - for tor_key in &self.tor_keys { - key_store.as_onion_mut().insert_key(tor_key)?; - } let cert_store = key_store.as_local_certs_mut(); if cert_store.as_root_cert().de()?.0 != self.root_ca_cert { cert_store @@ -148,11 +118,5 @@ impl AccountInfo { self.hostname.no_dot_host_name(), self.hostname.local_domain_name(), ] - .into_iter() - .chain( - self.tor_keys - .iter() - .map(|k| InternedString::from_display(&k.onion_address())), - ) } } diff --git a/core/src/backup/os.rs b/core/src/backup/os.rs index 380772b77..8837f72f0 100644 --- a/core/src/backup/os.rs +++ b/core/src/backup/os.rs @@ -7,9 +7,7 @@ use ssh_key::private::Ed25519Keypair; use crate::account::AccountInfo; use crate::hostname::{Hostname, generate_hostname, generate_id}; -use crate::net::tor::TorSecretKey; use crate::prelude::*; -use crate::util::crypto::ed25519_expand_key; use crate::util::serde::{Base32, Base64, Pem}; pub struct OsBackup { @@ -85,10 +83,6 @@ impl OsBackupV0 { &mut ssh_key::rand_core::OsRng::default(), ssh_key::Algorithm::Ed25519, )?, - tor_keys: TorSecretKey::from_bytes(self.tor_key.0) - .ok() - .into_iter() - .collect(), developer_key: ed25519_dalek::SigningKey::generate( &mut ssh_key::rand_core::OsRng::default(), ), @@ -119,10 +113,6 @@ impl OsBackupV1 { root_ca_key: self.root_ca_key.0, root_ca_cert: self.root_ca_cert.0, ssh_key: ssh_key::PrivateKey::from(Ed25519Keypair::from_seed(&self.net_key.0)), - tor_keys: TorSecretKey::from_bytes(ed25519_expand_key(&self.net_key.0)) - .ok() - .into_iter() - .collect(), developer_key: ed25519_dalek::SigningKey::from_bytes(&self.net_key), }, ui: self.ui, @@ -140,7 +130,6 @@ struct OsBackupV2 { root_ca_key: Pem>, // PEM Encoded OpenSSL Key root_ca_cert: Pem, // PEM Encoded OpenSSL X509 Certificate ssh_key: Pem, // PEM Encoded OpenSSH Key - tor_keys: Vec, // Base64 Encoded Ed25519 Expanded Secret Key compat_s9pk_key: Pem, // PEM Encoded ED25519 Key ui: Value, // JSON Value } @@ -154,7 +143,6 @@ impl OsBackupV2 { root_ca_key: self.root_ca_key.0, root_ca_cert: self.root_ca_cert.0, ssh_key: self.ssh_key.0, - tor_keys: self.tor_keys, developer_key: self.compat_s9pk_key.0, }, ui: self.ui, @@ -167,7 +155,6 @@ impl OsBackupV2 { root_ca_key: Pem(backup.account.root_ca_key.clone()), root_ca_cert: Pem(backup.account.root_ca_cert.clone()), ssh_key: Pem(backup.account.ssh_key.clone()), - tor_keys: backup.account.tor_keys.clone(), compat_s9pk_key: Pem(backup.account.developer_key.clone()), ui: backup.ui.clone(), } diff --git a/core/src/db/model/public.rs b/core/src/db/model/public.rs index 20c5bc390..d038577a6 100644 --- a/core/src/db/model/public.rs +++ b/core/src/db/model/public.rs @@ -89,7 +89,6 @@ impl Public { )] .into_iter() .collect(), - onions: account.tor_keys.iter().map(|k| k.onion_address()).collect(), public_domains: BTreeMap::new(), private_domains: BTreeSet::new(), hostname_info: BTreeMap::new(), diff --git a/core/src/error.rs b/core/src/error.rs index dba631303..0624be4dc 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -42,11 +42,11 @@ pub enum ErrorKind { ParseUrl = 19, DiskNotAvailable = 20, BlockDevice = 21, - InvalidOnionAddress = 22, + // InvalidOnionAddress = 22, Pack = 23, ValidateS9pk = 24, DiskCorrupted = 25, // Remove - Tor = 26, + // Tor = 26, ConfigGen = 27, ParseNumber = 28, Database = 29, @@ -126,11 +126,11 @@ impl ErrorKind { ParseUrl => t!("error.parse-url"), DiskNotAvailable => t!("error.disk-not-available"), BlockDevice => t!("error.block-device"), - InvalidOnionAddress => t!("error.invalid-onion-address"), + // InvalidOnionAddress => t!("error.invalid-onion-address"), Pack => t!("error.pack"), ValidateS9pk => t!("error.validate-s9pk"), DiskCorrupted => t!("error.disk-corrupted"), // Remove - Tor => t!("error.tor"), + // Tor => t!("error.tor"), ConfigGen => t!("error.config-gen"), ParseNumber => t!("error.parse-number"), Database => t!("error.database"), @@ -370,17 +370,6 @@ impl From for Error { Error::new(e, kind) } } -#[cfg(feature = "arti")] -impl From for Error { - fn from(e: arti_client::Error) -> Self { - Error::new(e, ErrorKind::Tor) - } -} -impl From for Error { - fn from(e: torut::control::ConnError) -> Self { - Error::new(e, ErrorKind::Tor) - } -} impl From for Error { fn from(e: zbus::Error) -> Self { Error::new(e, ErrorKind::DBus) diff --git a/core/src/net/host/address.rs b/core/src/net/host/address.rs index 9c60ababe..59f74fdb8 100644 --- a/core/src/net/host/address.rs +++ b/core/src/net/host/address.rs @@ -12,7 +12,6 @@ use crate::context::{CliContext, RpcContext}; use crate::db::model::DatabaseModel; use crate::net::acme::AcmeProvider; use crate::net::host::{HostApiKind, all_hosts}; -use crate::net::tor::OnionAddress; use crate::prelude::*; use crate::util::serde::{HandlerExtSerde, display_serializable}; @@ -21,9 +20,6 @@ use crate::util::serde::{HandlerExtSerde, display_serializable}; #[serde(rename_all_fields = "camelCase")] #[serde(tag = "kind")] pub enum HostAddress { - Onion { - address: OnionAddress, - }, Domain { address: InternedString, public: Option, @@ -38,18 +34,7 @@ pub struct PublicDomainConfig { } fn handle_duplicates(db: &mut DatabaseModel) -> Result<(), Error> { - let mut onions = BTreeSet::::new(); let mut domains = BTreeSet::::new(); - let check_onion = |onions: &mut BTreeSet, onion: OnionAddress| { - if onions.contains(&onion) { - return Err(Error::new( - eyre!("onion address {onion} is already in use"), - ErrorKind::InvalidRequest, - )); - } - onions.insert(onion); - Ok(()) - }; let check_domain = |domains: &mut BTreeSet, domain: InternedString| { if domains.contains(&domain) { return Err(Error::new( @@ -68,9 +53,6 @@ fn handle_duplicates(db: &mut DatabaseModel) -> Result<(), Error> { not_in_use.push(host); continue; } - for onion in host.as_onions().de()? { - check_onion(&mut onions, onion)?; - } let public = host.as_public_domains().keys()?; for domain in &public { check_domain(&mut domains, domain.clone())?; @@ -82,16 +64,11 @@ fn handle_duplicates(db: &mut DatabaseModel) -> Result<(), Error> { } } for host in not_in_use { - host.as_onions_mut() - .mutate(|o| Ok(o.retain(|o| !onions.contains(o))))?; host.as_public_domains_mut() .mutate(|d| Ok(d.retain(|d, _| !domains.contains(d))))?; host.as_private_domains_mut() .mutate(|d| Ok(d.retain(|d| !domains.contains(d))))?; - for onion in host.as_onions().de()? { - check_onion(&mut onions, onion)?; - } let public = host.as_public_domains().keys()?; for domain in &public { check_domain(&mut domains, domain.clone())?; @@ -159,29 +136,6 @@ pub fn address_api() ) .with_inherited(Kind::inheritance), ) - .subcommand( - "onion", - ParentHandler::::new() - .subcommand( - "add", - from_fn_async(add_onion::) - .with_metadata("sync_db", Value::Bool(true)) - .with_inherited(|_, a| a) - .no_display() - .with_about("about.add-address-to-host") - .with_call_remote::(), - ) - .subcommand( - "remove", - from_fn_async(remove_onion::) - .with_metadata("sync_db", Value::Bool(true)) - .with_inherited(|_, a| a) - .no_display() - .with_about("about.remove-address-from-host") - .with_call_remote::(), - ) - .with_inherited(Kind::inheritance), - ) .subcommand( "list", from_fn_async(list_addresses::) @@ -199,9 +153,6 @@ pub fn address_api() table.add_row(row![bc => "ADDRESS", "PUBLIC", "ACME PROVIDER"]); for address in &res { match address { - HostAddress::Onion { address } => { - table.add_row(row![address, true, "N/A"]); - } HostAddress::Domain { address, public: Some(PublicDomainConfig { gateway, acme }), @@ -351,55 +302,6 @@ pub async fn remove_private_domain( Ok(()) } -#[derive(Deserialize, Serialize, Parser)] -pub struct OnionParams { - #[arg(help = "help.arg.onion-address")] - pub onion: String, -} - -pub async fn add_onion( - ctx: RpcContext, - OnionParams { onion }: OnionParams, - inheritance: Kind::Inheritance, -) -> Result<(), Error> { - let onion = onion.parse::()?; - ctx.db - .mutate(|db| { - db.as_private().as_key_store().as_onion().get_key(&onion)?; - - Kind::host_for(&inheritance, db)? - .as_onions_mut() - .mutate(|a| Ok(a.insert(onion)))?; - handle_duplicates(db) - }) - .await - .result?; - - Kind::sync_host(&ctx, inheritance).await?; - - Ok(()) -} - -pub async fn remove_onion( - ctx: RpcContext, - OnionParams { onion }: OnionParams, - inheritance: Kind::Inheritance, -) -> Result<(), Error> { - let onion = onion.parse::()?; - ctx.db - .mutate(|db| { - Kind::host_for(&inheritance, db)? - .as_onions_mut() - .mutate(|a| Ok(a.remove(&onion))) - }) - .await - .result?; - - Kind::sync_host(&ctx, inheritance).await?; - - Ok(()) -} - pub async fn list_addresses( ctx: RpcContext, _: Empty, diff --git a/core/src/net/host/mod.rs b/core/src/net/host/mod.rs index 620991ca7..6fdd65d92 100644 --- a/core/src/net/host/mod.rs +++ b/core/src/net/host/mod.rs @@ -15,7 +15,6 @@ use crate::net::forward::AvailablePorts; use crate::net::host::address::{HostAddress, PublicDomainConfig, address_api}; use crate::net::host::binding::{BindInfo, BindOptions, binding}; use crate::net::service_interface::HostnameInfo; -use crate::net::tor::OnionAddress; use crate::prelude::*; use crate::{HostId, PackageId}; @@ -28,8 +27,6 @@ pub mod binding; #[ts(export)] pub struct Host { pub bindings: BTreeMap, - #[ts(type = "string[]")] - pub onions: BTreeSet, pub public_domains: BTreeMap, pub private_domains: BTreeSet, /// COMPUTED: NetService::update @@ -46,19 +43,13 @@ impl Host { Self::default() } pub fn addresses<'a>(&'a self) -> impl Iterator + 'a { - self.onions + self.public_domains .iter() - .cloned() - .map(|address| HostAddress::Onion { address }) - .chain( - self.public_domains - .iter() - .map(|(address, config)| HostAddress::Domain { - address: address.clone(), - public: Some(config.clone()), - private: self.private_domains.contains(address), - }), - ) + .map(|(address, config)| HostAddress::Domain { + address: address.clone(), + public: Some(config.clone()), + private: self.private_domains.contains(address), + }) .chain( self.private_domains .iter() @@ -112,22 +103,7 @@ pub fn host_for<'a>( .as_hosts_mut(), ) } - let tor_key = if host_info(db, package_id)?.as_idx(host_id).is_none() { - Some( - db.as_private_mut() - .as_key_store_mut() - .as_onion_mut() - .new_key()?, - ) - } else { - None - }; - host_info(db, package_id)?.upsert(host_id, || { - let mut h = Host::new(); - h.onions - .insert(tor_key.or_not_found("generated tor key")?.onion_address()); - Ok(h) - }) + host_info(db, package_id)?.upsert(host_id, || Ok(Host::new())) } pub fn all_hosts(db: &mut DatabaseModel) -> impl Iterator, Error>> { diff --git a/core/src/net/keys.rs b/core/src/net/keys.rs index 41e96eedd..077f56875 100644 --- a/core/src/net/keys.rs +++ b/core/src/net/keys.rs @@ -3,28 +3,21 @@ use serde::{Deserialize, Serialize}; use crate::account::AccountInfo; use crate::net::acme::AcmeCertStore; use crate::net::ssl::CertStore; -use crate::net::tor::OnionStore; use crate::prelude::*; #[derive(Debug, Deserialize, Serialize, HasModel)] #[model = "Model"] #[serde(rename_all = "camelCase")] pub struct KeyStore { - pub onion: OnionStore, pub local_certs: CertStore, #[serde(default)] pub acme: AcmeCertStore, } impl KeyStore { pub fn new(account: &AccountInfo) -> Result { - let mut res = Self { - onion: OnionStore::new(), + Ok(Self { local_certs: CertStore::new(account)?, acme: AcmeCertStore::new(), - }; - for tor_key in account.tor_keys.iter().cloned() { - res.onion.insert(tor_key); - } - Ok(res) + }) } } diff --git a/core/src/net/mod.rs b/core/src/net/mod.rs index f30c1383b..f199b3194 100644 --- a/core/src/net/mod.rs +++ b/core/src/net/mod.rs @@ -14,7 +14,6 @@ pub mod socks; pub mod ssl; pub mod static_server; pub mod tls; -pub mod tor; pub mod tunnel; pub mod utils; pub mod vhost; @@ -23,7 +22,6 @@ pub mod wifi; pub fn net_api() -> ParentHandler { ParentHandler::new() - .subcommand("tor", tor::tor_api::().with_about("about.tor-commands")) .subcommand( "acme", acme::acme_api::().with_about("about.setup-acme-certificate"), diff --git a/core/src/net/net_controller.rs b/core/src/net/net_controller.rs index dc46fde33..b1c6e0a6a 100644 --- a/core/src/net/net_controller.rs +++ b/core/src/net/net_controller.rs @@ -3,7 +3,7 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}; use std::sync::{Arc, Weak}; use color_eyre::eyre::eyre; -use imbl::{OrdMap, vector}; +use imbl::vector; use imbl_value::InternedString; use ipnet::IpNet; use tokio::sync::Mutex; @@ -19,24 +19,22 @@ use crate::net::dns::DnsController; use crate::net::forward::{InterfacePortForwardController, START9_BRIDGE_IFACE, add_iptables_rule}; use crate::net::gateway::{ AndFilter, DynInterfaceFilter, IdFilter, InterfaceFilter, NetworkInterfaceController, OrFilter, - PublicFilter, SecureFilter, TypeFilter, + PublicFilter, SecureFilter, }; use crate::net::host::address::HostAddress; use crate::net::host::binding::{AddSslOptions, BindId, BindOptions}; use crate::net::host::{Host, Hosts, host_for}; -use crate::net::service_interface::{GatewayInfo, HostnameInfo, IpHostname, OnionHostname}; +use crate::net::service_interface::{GatewayInfo, HostnameInfo, IpHostname}; use crate::net::socks::SocksController; -use crate::net::tor::{OnionAddress, TorController, TorSecretKey}; use crate::net::utils::ipv6_is_local; use crate::net::vhost::{AlpnInfo, DynVHostTarget, ProxyTarget, VHostController}; use crate::prelude::*; use crate::service::effects::callbacks::ServiceCallbacks; use crate::util::serde::MaybeUtf8String; -use crate::{GatewayId, HOST_IP, HostId, OptionExt, PackageId}; +use crate::{HOST_IP, HostId, OptionExt, PackageId}; pub struct NetController { pub(crate) db: TypedPatchDb, - pub(super) tor: TorController, pub(super) vhost: VHostController, pub(super) tls_client_config: Arc, pub(crate) net_iface: Arc, @@ -54,8 +52,7 @@ impl NetController { socks_listen: SocketAddr, ) -> Result { let net_iface = Arc::new(NetworkInterfaceController::new(db.clone())); - let tor = TorController::new()?; - let socks = SocksController::new(socks_listen, tor.clone())?; + let socks = SocksController::new(socks_listen)?; let crypto_provider = Arc::new(tokio_rustls::rustls::crypto::ring::default_provider()); let tls_client_config = Arc::new(crate::net::tls::client_config( crypto_provider.clone(), @@ -87,7 +84,6 @@ impl NetController { .await?; Ok(Self { db: db.clone(), - tor, vhost: VHostController::new(db.clone(), net_iface.clone(), crypto_provider), tls_client_config, dns: DnsController::init(db, &net_iface.watcher).await?, @@ -168,7 +164,6 @@ struct HostBinds { forwards: BTreeMap)>, vhosts: BTreeMap<(Option, u16), (ProxyTarget, Arc<()>)>, private_dns: BTreeMap>, - tor: BTreeMap, Vec>)>, } pub struct NetServiceData { @@ -260,8 +255,6 @@ impl NetServiceData { let mut forwards: BTreeMap = BTreeMap::new(); let mut vhosts: BTreeMap<(Option, u16), ProxyTarget> = BTreeMap::new(); let mut private_dns: BTreeSet = BTreeSet::new(); - let mut tor: BTreeMap)> = - BTreeMap::new(); let mut hostname_info: BTreeMap> = BTreeMap::new(); let binds = self.binds.entry(id.clone()).or_default(); @@ -308,29 +301,6 @@ impl NetServiceData { } for address in host.addresses() { match address { - HostAddress::Onion { address } => { - let hostname = InternedString::from_display(&address); - if hostnames.insert(hostname.clone()) { - vhosts.insert( - (Some(hostname), external), - ProxyTarget { - filter: OrFilter( - TypeFilter(NetworkInterfaceType::Loopback), - IdFilter(GatewayId::from(InternedString::from( - START9_BRIDGE_IFACE, - ))), - ) - .into_dyn(), - acme: None, - addr, - add_x_forwarded_headers: ssl.add_x_forwarded_headers, - connect_ssl: connect_ssl - .clone() - .map(|_| ctrl.tls_client_config.clone()), - }, - ); // TODO: wrap onion ssl stream directly in tor ctrl - } - } HostAddress::Domain { address, public, @@ -615,66 +585,6 @@ impl NetServiceData { } } - struct TorHostnamePorts { - non_ssl: Option, - ssl: Option, - } - let mut tor_hostname_ports = BTreeMap::::new(); - let mut tor_binds = OrdMap::::new(); - for (internal, info) in &host.bindings { - if !info.enabled { - continue; - } - tor_binds.insert( - info.options.preferred_external_port, - SocketAddr::from((self.ip, *internal)), - ); - if let (Some(ssl), Some(ssl_internal)) = - (&info.options.add_ssl, info.net.assigned_ssl_port) - { - tor_binds.insert( - ssl.preferred_external_port, - SocketAddr::from(([127, 0, 0, 1], ssl_internal)), - ); - tor_hostname_ports.insert( - *internal, - TorHostnamePorts { - non_ssl: Some(info.options.preferred_external_port) - .filter(|p| *p != ssl.preferred_external_port), - ssl: Some(ssl.preferred_external_port), - }, - ); - } else { - tor_hostname_ports.insert( - *internal, - TorHostnamePorts { - non_ssl: Some(info.options.preferred_external_port), - ssl: None, - }, - ); - } - } - - for tor_addr in host.onions.iter() { - let key = peek - .as_private() - .as_key_store() - .as_onion() - .get_key(tor_addr)?; - tor.insert(key.onion_address(), (key, tor_binds.clone())); - for (internal, ports) in &tor_hostname_ports { - let mut bind_hostname_info = hostname_info.remove(internal).unwrap_or_default(); - bind_hostname_info.push(HostnameInfo::Onion { - hostname: OnionHostname { - value: InternedString::from_display(tor_addr), - port: ports.non_ssl, - ssl_port: ports.ssl, - }, - }); - hostname_info.insert(*internal, bind_hostname_info); - } - } - let all = binds .forwards .keys() @@ -763,48 +673,9 @@ impl NetServiceData { } ctrl.dns.gc_private_domains(&rm)?; - let all = binds - .tor - .keys() - .chain(tor.keys()) - .cloned() - .collect::>(); - for onion in all { - let mut prev = binds.tor.remove(&onion); - if let Some((key, tor_binds)) = tor.remove(&onion).filter(|(_, b)| !b.is_empty()) { - prev = prev.filter(|(b, _)| b == &tor_binds); - binds.tor.insert( - onion, - if let Some(prev) = prev { - prev - } else { - let service = ctrl.tor.service(key)?; - let rcs = service.proxy_all(tor_binds.iter().map(|(k, v)| (*k, *v))); - (tor_binds, rcs) - }, - ); - } else { - if let Some((_, rc)) = prev { - drop(rc); - ctrl.tor.gc(Some(onion)).await?; - } - } - } - - let res = ctrl - .db - .mutate(|db| { - host_for(db, self.id.as_ref(), &id)? - .as_hostname_info_mut() - .ser(&hostname_info) - }) - .await; - res.result?; if let Some(pkg_id) = self.id.as_ref() { - if res.revision.is_some() { - if let Some(cbs) = ctrl.callbacks.get_host_info(&(pkg_id.clone(), id)) { - cbs.call(vector![]).await?; - } + if let Some(cbs) = ctrl.callbacks.get_host_info(&(pkg_id.clone(), id)) { + cbs.call(vector![]).await?; } } Ok(()) diff --git a/core/src/net/service_interface.rs b/core/src/net/service_interface.rs index 499e1a321..ca960e569 100644 --- a/core/src/net/service_interface.rs +++ b/core/src/net/service_interface.rs @@ -17,15 +17,11 @@ pub enum HostnameInfo { public: bool, hostname: IpHostname, }, - Onion { - hostname: OnionHostname, - }, } impl HostnameInfo { pub fn to_san_hostname(&self) -> InternedString { match self { Self::Ip { hostname, .. } => hostname.to_san_hostname(), - Self::Onion { hostname } => hostname.to_san_hostname(), } } } @@ -39,21 +35,6 @@ pub struct GatewayInfo { pub public: bool, } -#[derive(Clone, Debug, Deserialize, Serialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -pub struct OnionHostname { - #[ts(type = "string")] - pub value: InternedString, - pub port: Option, - pub ssl_port: Option, -} -impl OnionHostname { - pub fn to_san_hostname(&self) -> InternedString { - self.value.clone() - } -} - #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] diff --git a/core/src/net/socks.rs b/core/src/net/socks.rs index 5d1be66f0..7f54a8010 100644 --- a/core/src/net/socks.rs +++ b/core/src/net/socks.rs @@ -8,7 +8,6 @@ use socks5_impl::server::{AuthAdaptor, ClientConnection, Server}; use tokio::net::{TcpListener, TcpStream}; use crate::HOST_IP; -use crate::net::tor::TorController; use crate::prelude::*; use crate::util::actor::background::BackgroundJobQueue; use crate::util::future::NonDetachingJoinHandle; @@ -22,7 +21,7 @@ pub struct SocksController { _thread: NonDetachingJoinHandle<()>, } impl SocksController { - pub fn new(listen: SocketAddr, tor: TorController) -> Result { + pub fn new(listen: SocketAddr) -> Result { Ok(Self { _thread: tokio::spawn(async move { let auth: AuthAdaptor<()> = Arc::new(NoAuth); @@ -45,7 +44,6 @@ impl SocksController { loop { match server.accept().await { Ok((stream, _)) => { - let tor = tor.clone(); bg.add_job(async move { if let Err(e) = async { match stream @@ -57,40 +55,6 @@ impl SocksController { .await .with_kind(ErrorKind::Network)? { - ClientConnection::Connect( - reply, - Address::DomainAddress(domain, port), - ) if domain.ends_with(".onion") => { - if let Ok(mut target) = tor - .connect_onion(&domain.parse()?, port) - .await - { - let mut sock = reply - .reply( - Reply::Succeeded, - Address::unspecified(), - ) - .await - .with_kind(ErrorKind::Network)?; - tokio::io::copy_bidirectional( - &mut sock, - &mut target, - ) - .await - .with_kind(ErrorKind::Network)?; - } else { - let mut sock = reply - .reply( - Reply::HostUnreachable, - Address::unspecified(), - ) - .await - .with_kind(ErrorKind::Network)?; - sock.shutdown() - .await - .with_kind(ErrorKind::Network)?; - } - } ClientConnection::Connect(reply, addr) => { if let Ok(mut target) = match addr { Address::DomainAddress(domain, port) => { diff --git a/core/src/net/tor/arti.rs b/core/src/net/tor/arti.rs deleted file mode 100644 index c44d8d528..000000000 --- a/core/src/net/tor/arti.rs +++ /dev/null @@ -1,964 +0,0 @@ -use std::borrow::Cow; -use std::collections::{BTreeMap, BTreeSet}; -use std::net::SocketAddr; -use std::str::FromStr; -use std::sync::{Arc, Weak}; -use std::time::{Duration, Instant}; - -use arti_client::config::onion_service::OnionServiceConfigBuilder; -use arti_client::{TorClient, TorClientConfig}; -use base64::Engine; -use clap::Parser; -use color_eyre::eyre::eyre; -use futures::{FutureExt, StreamExt}; -use imbl_value::InternedString; -use itertools::Itertools; -use rpc_toolkit::{Context, Empty, HandlerExt, ParentHandler, from_fn_async}; -use serde::{Deserialize, Serialize}; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::TcpStream; -use tokio::sync::Notify; -use tor_cell::relaycell::msg::Connected; -use tor_hscrypto::pk::{HsId, HsIdKeypair}; -use tor_hsservice::status::State as ArtiOnionServiceState; -use tor_hsservice::{HsNickname, RunningOnionService}; -use tor_keymgr::config::ArtiKeystoreKind; -use tor_proto::client::stream::IncomingStreamRequest; -use tor_rtcompat::tokio::TokioRustlsRuntime; -use ts_rs::TS; - -use crate::context::{CliContext, RpcContext}; -use crate::prelude::*; -use crate::util::actor::background::BackgroundJobQueue; -use crate::util::future::{NonDetachingJoinHandle, Until}; -use crate::util::io::ReadWriter; -use crate::util::serde::{ - BASE64, Base64, HandlerExtSerde, WithIoFormat, deserialize_from_str, display_serializable, - serialize_display, -}; -use crate::util::sync::{SyncMutex, SyncRwLock, Watch}; - -const BOOTSTRAP_PROGRESS_TIMEOUT: Duration = Duration::from_secs(300); -const HS_BOOTSTRAP_TIMEOUT: Duration = Duration::from_secs(300); -const RETRY_COOLDOWN: Duration = Duration::from_secs(15); -const HEALTH_CHECK_FAILURE_ALLOWANCE: usize = 5; -const HEALTH_CHECK_COOLDOWN: Duration = Duration::from_secs(120); - -#[derive(Debug, Clone, Copy)] -pub struct OnionAddress(pub HsId); -impl std::fmt::Display for OnionAddress { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - safelog::DisplayRedacted::fmt_unredacted(&self.0, f) - } -} -impl FromStr for OnionAddress { - type Err = Error; - fn from_str(s: &str) -> Result { - Ok(Self( - if s.ends_with(".onion") { - Cow::Borrowed(s) - } else { - Cow::Owned(format!("{s}.onion")) - } - .parse::() - .with_kind(ErrorKind::Tor)?, - )) - } -} -impl Serialize for OnionAddress { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serialize_display(self, serializer) - } -} -impl<'de> Deserialize<'de> for OnionAddress { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserialize_from_str(deserializer) - } -} -impl PartialEq for OnionAddress { - fn eq(&self, other: &Self) -> bool { - self.0.as_ref() == other.0.as_ref() - } -} -impl Eq for OnionAddress {} -impl PartialOrd for OnionAddress { - fn partial_cmp(&self, other: &Self) -> Option { - self.0.as_ref().partial_cmp(other.0.as_ref()) - } -} -impl Ord for OnionAddress { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.0.as_ref().cmp(other.0.as_ref()) - } -} - -pub struct TorSecretKey(pub HsIdKeypair); -impl TorSecretKey { - pub fn onion_address(&self) -> OnionAddress { - OnionAddress(HsId::from(self.0.as_ref().public().to_bytes())) - } - pub fn from_bytes(bytes: [u8; 64]) -> Result { - Ok(Self( - tor_llcrypto::pk::ed25519::ExpandedKeypair::from_secret_key_bytes(bytes) - .ok_or_else(|| { - Error::new( - eyre!("{}", t!("net.tor.invalid-ed25519-key")), - ErrorKind::Tor, - ) - })? - .into(), - )) - } - pub fn generate() -> Self { - Self( - tor_llcrypto::pk::ed25519::ExpandedKeypair::from( - &tor_llcrypto::pk::ed25519::Keypair::generate(&mut rand::rng()), - ) - .into(), - ) - } -} -impl Clone for TorSecretKey { - fn clone(&self) -> Self { - Self(HsIdKeypair::from( - tor_llcrypto::pk::ed25519::ExpandedKeypair::from_secret_key_bytes( - self.0.as_ref().to_secret_key_bytes(), - ) - .unwrap(), - )) - } -} -impl std::fmt::Display for TorSecretKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - BASE64.encode(self.0.as_ref().to_secret_key_bytes()) - ) - } -} -impl FromStr for TorSecretKey { - type Err = Error; - fn from_str(s: &str) -> Result { - Self::from_bytes(Base64::<[u8; 64]>::from_str(s)?.0) - } -} -impl Serialize for TorSecretKey { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serialize_display(self, serializer) - } -} -impl<'de> Deserialize<'de> for TorSecretKey { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserialize_from_str(deserializer) - } -} - -#[derive(Default, Deserialize, Serialize)] -pub struct OnionStore(BTreeMap); -impl Map for OnionStore { - type Key = OnionAddress; - type Value = TorSecretKey; - fn key_str(key: &Self::Key) -> Result, Error> { - Self::key_string(key) - } - fn key_string(key: &Self::Key) -> Result { - Ok(InternedString::from_display(key)) - } -} -impl OnionStore { - pub fn new() -> Self { - Self::default() - } - pub fn insert(&mut self, key: TorSecretKey) { - self.0.insert(key.onion_address(), key); - } -} -impl Model { - pub fn new_key(&mut self) -> Result { - let key = TorSecretKey::generate(); - self.insert(&key.onion_address(), &key)?; - Ok(key) - } - pub fn insert_key(&mut self, key: &TorSecretKey) -> Result<(), Error> { - self.insert(&key.onion_address(), &key) - } - pub fn get_key(&self, address: &OnionAddress) -> Result { - self.as_idx(address) - .or_not_found(lazy_format!("private key for {address}"))? - .de() - } -} -impl std::fmt::Debug for OnionStore { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - struct OnionStoreMap<'a>(&'a BTreeMap); - impl<'a> std::fmt::Debug for OnionStoreMap<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - #[derive(Debug)] - struct KeyFor(#[allow(unused)] OnionAddress); - let mut map = f.debug_map(); - for (k, v) in self.0 { - map.key(k); - map.value(&KeyFor(v.onion_address())); - } - map.finish() - } - } - f.debug_tuple("OnionStore") - .field(&OnionStoreMap(&self.0)) - .finish() - } -} - -pub fn tor_api() -> ParentHandler { - ParentHandler::new() - .subcommand( - "list-services", - from_fn_async(list_services) - .with_display_serializable() - .with_custom_display_fn(|handle, result| display_services(handle.params, result)) - .with_about("about.display-tor-v3-onion-addresses") - .with_call_remote::(), - ) - .subcommand( - "reset", - from_fn_async(reset) - .no_display() - .with_about("about.reset-tor-daemon") - .with_call_remote::(), - ) - .subcommand( - "key", - key::().with_about("about.manage-onion-service-key-store"), - ) -} - -pub fn key() -> ParentHandler { - ParentHandler::new() - .subcommand( - "generate", - from_fn_async(generate_key) - .with_about("about.generate-onion-service-key-add-to-store") - .with_call_remote::(), - ) - .subcommand( - "add", - from_fn_async(add_key) - .with_about("about.add-onion-service-key-to-store") - .with_call_remote::(), - ) - .subcommand( - "list", - from_fn_async(list_keys) - .with_custom_display_fn(|_, res| { - for addr in res { - println!("{addr}"); - } - Ok(()) - }) - .with_about("about.list-onion-services-with-keys-in-store") - .with_call_remote::(), - ) -} - -pub async fn generate_key(ctx: RpcContext) -> Result { - ctx.db - .mutate(|db| { - Ok(db - .as_private_mut() - .as_key_store_mut() - .as_onion_mut() - .new_key()? - .onion_address()) - }) - .await - .result -} - -#[derive(Deserialize, Serialize, Parser)] -pub struct AddKeyParams { - #[arg(help = "help.arg.onion-secret-key")] - pub key: Base64<[u8; 64]>, -} - -pub async fn add_key( - ctx: RpcContext, - AddKeyParams { key }: AddKeyParams, -) -> Result { - let key = TorSecretKey::from_bytes(key.0)?; - ctx.db - .mutate(|db| { - db.as_private_mut() - .as_key_store_mut() - .as_onion_mut() - .insert_key(&key) - }) - .await - .result?; - Ok(key.onion_address()) -} - -pub async fn list_keys(ctx: RpcContext) -> Result, Error> { - ctx.db - .peek() - .await - .into_private() - .into_key_store() - .into_onion() - .keys() -} - -#[derive(Deserialize, Serialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "kebab-case")] -pub struct ResetParams { - #[arg( - name = "wipe-state", - short = 'w', - long = "wipe-state", - help = "help.arg.wipe-tor-state" - )] - wipe_state: bool, -} - -pub async fn reset(ctx: RpcContext, ResetParams { wipe_state }: ResetParams) -> Result<(), Error> { - ctx.net_controller.tor.reset(wipe_state).await -} - -pub fn display_services( - params: WithIoFormat, - services: BTreeMap, -) -> Result<(), Error> { - use prettytable::*; - - if let Some(format) = params.format { - return display_serializable(format, services); - } - - let mut table = Table::new(); - table.add_row(row![bc => "ADDRESS", "STATE", "BINDINGS"]); - for (service, info) in services { - let row = row![ - &service.to_string(), - &format!("{:?}", info.state), - &info - .bindings - .into_iter() - .map(|(port, addr)| lazy_format!("{port} -> {addr}")) - .join("; ") - ]; - table.add_row(row); - } - table.print_tty(false)?; - Ok(()) -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum OnionServiceState { - Shutdown, - Bootstrapping, - DegradedReachable, - DegradedUnreachable, - Running, - Recovering, - Broken, -} -impl From for OnionServiceState { - fn from(value: ArtiOnionServiceState) -> Self { - match value { - ArtiOnionServiceState::Shutdown => Self::Shutdown, - ArtiOnionServiceState::Bootstrapping => Self::Bootstrapping, - ArtiOnionServiceState::DegradedReachable => Self::DegradedReachable, - ArtiOnionServiceState::DegradedUnreachable => Self::DegradedUnreachable, - ArtiOnionServiceState::Running => Self::Running, - ArtiOnionServiceState::Recovering => Self::Recovering, - ArtiOnionServiceState::Broken => Self::Broken, - _ => unreachable!(), - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct OnionServiceInfo { - pub state: OnionServiceState, - pub bindings: BTreeMap, -} - -pub async fn list_services( - ctx: RpcContext, - _: Empty, -) -> Result, Error> { - ctx.net_controller.tor.list_services().await -} - -#[derive(Clone)] -pub struct TorController(Arc); -struct TorControllerInner { - client: Watch<(usize, TorClient)>, - _bootstrapper: NonDetachingJoinHandle<()>, - services: SyncMutex>, - reset: Arc, -} -impl TorController { - pub fn new() -> Result { - let mut config = TorClientConfig::builder(); - config - .storage() - .keystore() - .primary() - .kind(ArtiKeystoreKind::Ephemeral.into()); - let client = Watch::new(( - 0, - TorClient::with_runtime(TokioRustlsRuntime::current()?) - .config(config.build().with_kind(ErrorKind::Tor)?) - .local_resource_timeout(Duration::from_secs(0)) - .create_unbootstrapped()?, - )); - let reset = Arc::new(Notify::new()); - let bootstrapper_reset = reset.clone(); - let bootstrapper_client = client.clone(); - let bootstrapper = tokio::spawn(async move { - loop { - let (epoch, client): (usize, _) = bootstrapper_client.read(); - if let Err(e) = Until::new() - .with_async_fn(|| bootstrapper_reset.notified().map(Ok)) - .run(async { - let mut events = client.bootstrap_events(); - let bootstrap_fut = - client.bootstrap().map(|res| res.with_kind(ErrorKind::Tor)); - let failure_fut = async { - let mut prev_frac = 0_f32; - let mut prev_inst = Instant::now(); - while let Some(event) = - tokio::time::timeout(BOOTSTRAP_PROGRESS_TIMEOUT, events.next()) - .await - .with_kind(ErrorKind::Tor)? - { - if event.ready_for_traffic() { - return Ok::<_, Error>(()); - } - let frac = event.as_frac(); - if frac == prev_frac { - if prev_inst.elapsed() > BOOTSTRAP_PROGRESS_TIMEOUT { - return Err(Error::new( - eyre!( - "{}", - t!( - "net.tor.bootstrap-no-progress", - duration = crate::util::serde::Duration::from( - BOOTSTRAP_PROGRESS_TIMEOUT - ) - .to_string() - ) - ), - ErrorKind::Tor, - )); - } - } else { - prev_frac = frac; - prev_inst = Instant::now(); - } - } - futures::future::pending().await - }; - if let Err::<(), Error>(e) = tokio::select! { - res = bootstrap_fut => res, - res = failure_fut => res, - } { - tracing::error!( - "{}", - t!("net.tor.bootstrap-error", error = e.to_string()) - ); - tracing::debug!("{e:?}"); - } else { - bootstrapper_client.send_modify(|_| ()); - - for _ in 0..HEALTH_CHECK_FAILURE_ALLOWANCE { - if let Err::<(), Error>(e) = async { - loop { - let (bg, mut runner) = BackgroundJobQueue::new(); - runner - .run_while(async { - const PING_BUF_LEN: usize = 8; - let key = TorSecretKey::generate(); - let onion = key.onion_address(); - let (hs, stream) = client - .launch_onion_service_with_hsid( - OnionServiceConfigBuilder::default() - .nickname( - onion - .to_string() - .trim_end_matches(".onion") - .parse::() - .with_kind(ErrorKind::Tor)?, - ) - .build() - .with_kind(ErrorKind::Tor)?, - key.clone().0, - ) - .with_kind(ErrorKind::Tor)?; - bg.add_job(async move { - if let Err(e) = async { - let mut stream = - tor_hsservice::handle_rend_requests( - stream, - ); - while let Some(req) = stream.next().await { - let mut stream = req - .accept(Connected::new_empty()) - .await - .with_kind(ErrorKind::Tor)?; - let mut buf = [0; PING_BUF_LEN]; - stream.read_exact(&mut buf).await?; - stream.write_all(&buf).await?; - stream.flush().await?; - stream.shutdown().await?; - } - Ok::<_, Error>(()) - } - .await - { - tracing::error!( - "{}", - t!( - "net.tor.health-error", - error = e.to_string() - ) - ); - tracing::debug!("{e:?}"); - } - }); - - tokio::time::timeout(HS_BOOTSTRAP_TIMEOUT, async { - let mut status = hs.status_events(); - while let Some(status) = status.next().await { - if status.state().is_fully_reachable() { - return Ok(()); - } - } - Err(Error::new( - eyre!( - "{}", - t!("net.tor.status-stream-ended") - ), - ErrorKind::Tor, - )) - }) - .await - .with_kind(ErrorKind::Tor)??; - - let mut stream = client - .connect((onion.to_string(), 8080)) - .await?; - let mut ping_buf = [0; PING_BUF_LEN]; - rand::fill(&mut ping_buf); - stream.write_all(&ping_buf).await?; - stream.flush().await?; - let mut ping_res = [0; PING_BUF_LEN]; - stream.read_exact(&mut ping_res).await?; - ensure_code!( - ping_buf == ping_res, - ErrorKind::Tor, - "ping buffer mismatch" - ); - stream.shutdown().await?; - - Ok::<_, Error>(()) - }) - .await?; - tokio::time::sleep(HEALTH_CHECK_COOLDOWN).await; - } - } - .await - { - tracing::error!( - "{}", - t!("net.tor.client-health-error", error = e.to_string()) - ); - tracing::debug!("{e:?}"); - } - } - tracing::error!( - "{}", - t!( - "net.tor.health-check-failed-recycling", - count = HEALTH_CHECK_FAILURE_ALLOWANCE - ) - ); - } - - Ok(()) - }) - .await - { - tracing::error!( - "{}", - t!("net.tor.bootstrapper-error", error = e.to_string()) - ); - tracing::debug!("{e:?}"); - } - if let Err::<(), Error>(e) = async { - tokio::time::sleep(RETRY_COOLDOWN).await; - bootstrapper_client.send(( - epoch.wrapping_add(1), - TorClient::with_runtime(TokioRustlsRuntime::current()?) - .config(config.build().with_kind(ErrorKind::Tor)?) - .local_resource_timeout(Duration::from_secs(0)) - .create_unbootstrapped_async() - .await?, - )); - tracing::debug!("TorClient recycled"); - Ok(()) - } - .await - { - tracing::error!( - "{}", - t!("net.tor.client-creation-error", error = e.to_string()) - ); - tracing::debug!("{e:?}"); - } - } - }) - .into(); - Ok(Self(Arc::new(TorControllerInner { - client, - _bootstrapper: bootstrapper, - services: SyncMutex::new(BTreeMap::new()), - reset, - }))) - } - - pub fn service(&self, key: TorSecretKey) -> Result { - self.0.services.mutate(|s| { - use std::collections::btree_map::Entry; - let addr = key.onion_address(); - match s.entry(addr) { - Entry::Occupied(e) => Ok(e.get().clone()), - Entry::Vacant(e) => Ok(e - .insert(OnionService::launch(self.0.client.clone(), key)?) - .clone()), - } - }) - } - - pub async fn gc(&self, addr: Option) -> Result<(), Error> { - if let Some(addr) = addr { - if let Some(s) = self.0.services.mutate(|s| { - let rm = if let Some(s) = s.get(&addr) { - !s.gc() - } else { - false - }; - if rm { s.remove(&addr) } else { None } - }) { - s.shutdown().await - } else { - Ok(()) - } - } else { - for s in self.0.services.mutate(|s| { - let mut rm = Vec::new(); - s.retain(|_, s| { - if s.gc() { - true - } else { - rm.push(s.clone()); - false - } - }); - rm - }) { - s.shutdown().await?; - } - Ok(()) - } - } - - pub async fn reset(&self, wipe_state: bool) -> Result<(), Error> { - self.0.reset.notify_waiters(); - Ok(()) - } - - pub async fn list_services(&self) -> Result, Error> { - Ok(self - .0 - .services - .peek(|s| s.iter().map(|(a, s)| (a.clone(), s.info())).collect())) - } - - pub async fn connect_onion( - &self, - addr: &OnionAddress, - port: u16, - ) -> Result, Error> { - if let Some(target) = self.0.services.peek(|s| { - s.get(addr).and_then(|s| { - s.0.bindings.peek(|b| { - b.get(&port).and_then(|b| { - b.iter() - .find(|(_, rc)| rc.strong_count() > 0) - .map(|(a, _)| *a) - }) - }) - }) - }) { - let tcp_stream = TcpStream::connect(target) - .await - .with_kind(ErrorKind::Network)?; - if let Err(e) = socket2::SockRef::from(&tcp_stream).set_keepalive(true) { - tracing::error!( - "{}", - t!("net.tor.failed-to-set-tcp-keepalive", error = e.to_string()) - ); - tracing::debug!("{e:?}"); - } - Ok(Box::new(tcp_stream)) - } else { - let mut client = self.0.client.clone(); - client - .wait_for(|(_, c)| c.bootstrap_status().ready_for_traffic()) - .await; - let stream = client - .read() - .1 - .connect((addr.to_string(), port)) - .await - .with_kind(ErrorKind::Tor)?; - Ok(Box::new(stream)) - } - } -} - -#[derive(Clone)] -pub struct OnionService(Arc); -struct OnionServiceData { - service: Arc>>>, - bindings: Arc>>>>, - _thread: NonDetachingJoinHandle<()>, -} -impl OnionService { - fn launch( - mut client: Watch<(usize, TorClient)>, - key: TorSecretKey, - ) -> Result { - let service = Arc::new(SyncMutex::new(None)); - let bindings = Arc::new(SyncRwLock::new(BTreeMap::< - u16, - BTreeMap>, - >::new())); - Ok(Self(Arc::new(OnionServiceData { - service: service.clone(), - bindings: bindings.clone(), - _thread: tokio::spawn(async move { - let (bg, mut runner) = BackgroundJobQueue::new(); - runner - .run_while(async { - loop { - if let Err(e) = async { - client.wait_for(|(_,c)| c.bootstrap_status().ready_for_traffic()).await; - let epoch = client.peek(|(e, c)| { - ensure_code!(c.bootstrap_status().ready_for_traffic(), ErrorKind::Tor, "TorClient recycled"); - Ok::<_, Error>(*e) - })?; - let addr = key.onion_address(); - let (new_service, stream) = client.peek(|(_, c)| { - c.launch_onion_service_with_hsid( - OnionServiceConfigBuilder::default() - .nickname( - addr - .to_string() - .trim_end_matches(".onion") - .parse::() - .with_kind(ErrorKind::Tor)?, - ) - .build() - .with_kind(ErrorKind::Tor)?, - key.clone().0, - ) - .with_kind(ErrorKind::Tor) - })?; - let mut status_stream = new_service.status_events(); - let mut status = new_service.status(); - if status.state().is_fully_reachable() { - tracing::debug!("{addr} is fully reachable"); - } else { - tracing::debug!("{addr} is not fully reachable"); - } - bg.add_job(async move { - while let Some(new_status) = status_stream.next().await { - if status.state().is_fully_reachable() && !new_status.state().is_fully_reachable() { - tracing::debug!("{addr} is no longer fully reachable"); - } else if !status.state().is_fully_reachable() && new_status.state().is_fully_reachable() { - tracing::debug!("{addr} is now fully reachable"); - } - status = new_status; - // TODO: health daemon? - } - }); - service.replace(Some(new_service)); - let mut stream = tor_hsservice::handle_rend_requests(stream); - while let Some(req) = tokio::select! { - req = stream.next() => req, - _ = client.wait_for(|(e, _)| *e != epoch) => None - } { - bg.add_job({ - let bg = bg.clone(); - let bindings = bindings.clone(); - async move { - if let Err(e) = async { - let IncomingStreamRequest::Begin(begin) = - req.request() - else { - return req - .reject(tor_cell::relaycell::msg::End::new_with_reason( - tor_cell::relaycell::msg::EndReason::DONE, - )) - .await - .with_kind(ErrorKind::Tor); - }; - let Some(target) = bindings.peek(|b| { - b.get(&begin.port()).and_then(|a| { - a.iter() - .find(|(_, rc)| rc.strong_count() > 0) - .map(|(addr, _)| *addr) - }) - }) else { - return req - .reject(tor_cell::relaycell::msg::End::new_with_reason( - tor_cell::relaycell::msg::EndReason::DONE, - )) - .await - .with_kind(ErrorKind::Tor); - }; - bg.add_job(async move { - if let Err(e) = async { - let mut outgoing = - TcpStream::connect(target) - .await - .with_kind(ErrorKind::Network)?; - if let Err(e) = socket2::SockRef::from(&outgoing).set_keepalive(true) { - tracing::error!("{}", t!("net.tor.failed-to-set-tcp-keepalive", error = e.to_string())); - tracing::debug!("{e:?}"); - } - let mut incoming = req - .accept(Connected::new_empty()) - .await - .with_kind(ErrorKind::Tor)?; - if let Err(e) = - tokio::io::copy_bidirectional( - &mut outgoing, - &mut incoming, - ) - .await - { - tracing::trace!("Tor Stream Error: {e}"); - tracing::trace!("{e:?}"); - } - - Ok::<_, Error>(()) - } - .await - { - tracing::trace!("Tor Stream Error: {e}"); - tracing::trace!("{e:?}"); - } - }); - Ok::<_, Error>(()) - } - .await - { - tracing::trace!("Tor Request Error: {e}"); - tracing::trace!("{e:?}"); - } - } - }); - } - Ok::<_, Error>(()) - } - .await - { - tracing::error!("{}", t!("net.tor.client-error", error = e.to_string())); - tracing::debug!("{e:?}"); - } - } - }) - .await - }) - .into(), - }))) - } - - pub async fn proxy_all>>( - &self, - bindings: impl IntoIterator, - ) -> Result { - Ok(self.0.bindings.mutate(|b| { - bindings - .into_iter() - .map(|(port, target)| { - let entry = b.entry(port).or_default().entry(target).or_default(); - if let Some(rc) = entry.upgrade() { - rc - } else { - let rc = Arc::new(()); - *entry = Arc::downgrade(&rc); - rc - } - }) - .collect() - })) - } - - pub fn gc(&self) -> bool { - self.0.bindings.mutate(|b| { - b.retain(|_, targets| { - targets.retain(|_, rc| rc.strong_count() > 0); - !targets.is_empty() - }); - !b.is_empty() - }) - } - - pub async fn shutdown(self) -> Result<(), Error> { - self.0.service.replace(None); - self.0._thread.abort(); - Ok(()) - } - - pub fn state(&self) -> OnionServiceState { - self.0 - .service - .peek(|s| s.as_ref().map(|s| s.status().state().into())) - .unwrap_or(OnionServiceState::Bootstrapping) - } - - pub fn info(&self) -> OnionServiceInfo { - OnionServiceInfo { - state: self.state(), - bindings: self.0.bindings.peek(|b| { - b.iter() - .filter_map(|(port, b)| { - b.iter() - .find(|(_, rc)| rc.strong_count() > 0) - .map(|(addr, _)| (*port, *addr)) - }) - .collect() - }), - } - } -} diff --git a/core/src/net/tor/ctor.rs b/core/src/net/tor/ctor.rs deleted file mode 100644 index 1fc3485fe..000000000 --- a/core/src/net/tor/ctor.rs +++ /dev/null @@ -1,1092 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet}; -use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; -use std::str::FromStr; -use std::sync::atomic::AtomicBool; -use std::sync::{Arc, Weak}; -use std::time::Duration; - -use base64::Engine; -use clap::Parser; -use color_eyre::eyre::eyre; -use futures::future::BoxFuture; -use futures::{FutureExt, TryFutureExt, TryStreamExt}; -use imbl::OrdMap; -use imbl_value::InternedString; -use lazy_static::lazy_static; -use regex::Regex; -use rpc_toolkit::{Context, Empty, HandlerExt, ParentHandler, from_fn_async}; -use serde::{Deserialize, Serialize}; -use tokio::net::TcpStream; -use tokio::process::Command; -use tokio::sync::{mpsc, oneshot}; -use tokio::time::Instant; -use torut::control::{AsyncEvent, AuthenticatedConn, ConnError}; -use torut::onion::{OnionAddressV3, TorSecretKeyV3}; -use tracing::instrument; -use ts_rs::TS; - -use crate::context::{CliContext, RpcContext}; -use crate::logs::{LogSource, LogsParams, journalctl}; -use crate::prelude::*; -use crate::util::Invoke; -use crate::util::collections::ordmap_retain; -use crate::util::future::NonDetachingJoinHandle; -use crate::util::io::{ReadWriter, write_file_atomic}; -use crate::util::serde::{ - BASE64, Base64, HandlerExtSerde, WithIoFormat, deserialize_from_str, display_serializable, - serialize_display, -}; -use crate::util::sync::Watch; - -pub const SYSTEMD_UNIT: &str = "tor@default"; -const STARTING_HEALTH_TIMEOUT: u64 = 120; // 2min - -const TOR_CONTROL: SocketAddr = - SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 1, 1), 9051)); - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct OnionAddress(OnionAddressV3); -impl std::fmt::Display for OnionAddress { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} -impl FromStr for OnionAddress { - type Err = Error; - fn from_str(s: &str) -> Result { - Ok(Self( - s.strip_suffix(".onion") - .unwrap_or(s) - .rsplit(".") - .next() - .unwrap_or(s) - .parse::() - .with_kind(ErrorKind::Tor)?, - )) - } -} -impl Serialize for OnionAddress { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serialize_display(self, serializer) - } -} -impl<'de> Deserialize<'de> for OnionAddress { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserialize_from_str(deserializer) - } -} -impl Ord for OnionAddress { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.0.get_raw_bytes().cmp(&other.0.get_raw_bytes()) - } -} -impl PartialOrd for OnionAddress { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TorSecretKey(pub TorSecretKeyV3); -impl TorSecretKey { - pub fn onion_address(&self) -> OnionAddress { - OnionAddress(self.0.public().get_onion_address()) - } - pub fn from_bytes(bytes: [u8; 64]) -> Result { - Ok(Self(TorSecretKeyV3::from(bytes))) - } - pub fn generate() -> Self { - Self(TorSecretKeyV3::generate()) - } - pub fn is_valid(&self) -> bool { - let bytes = self.0.as_bytes()[..32].try_into().unwrap(); - curve25519_dalek::scalar::clamp_integer(bytes) == bytes - } -} -impl std::fmt::Display for TorSecretKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", BASE64.encode(self.0.as_bytes())) - } -} -impl FromStr for TorSecretKey { - type Err = Error; - fn from_str(s: &str) -> Result { - Self::from_bytes(Base64::<[u8; 64]>::from_str(s)?.0) - } -} -impl Serialize for TorSecretKey { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serialize_display(self, serializer) - } -} -impl<'de> Deserialize<'de> for TorSecretKey { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserialize_from_str(deserializer) - } -} - -#[test] -fn test_generated_is_valid() { - for _ in 0..100 { - assert!(TorSecretKey::generate().is_valid()); - } -} - -#[test] -fn test_tor_key() { - // let key = crate::util::crypto::ed25519_expand_key( - // &hex::decode("c4b1a617bfdbcfb3f31e98c95542ce61718100e81cc6766eeebaa0dab42f0a93") - // .unwrap() - // .try_into() - // .unwrap(), - // ); - let key = - "4FpKpT4GZeEkUvH32AWMsndW+EG3XH46EmSFTh286G4AfG2U/Cc7y7L6k1dW5bl996QGDwe8gnaglq2hR2aD2w" - .parse::() - .unwrap(); - assert_eq!( - InternedString::from_display(&key.onion_address()), - InternedString::from("ja24lucrzgcusm72r2kmiujaa2g6b5o2w4wrwt5crfrhaz2qek5ozhqd.onion") - ); - eprintln!("{:?}", key.0.as_bytes()); - dbg!(key.to_string()); - dbg!(key.0.as_bytes()[0] & 0b111); - dbg!(key.onion_address()); - assert!(key.is_valid()); -} - -#[derive(Default, Deserialize, Serialize)] -pub struct OnionStore(BTreeMap); -impl Map for OnionStore { - type Key = OnionAddress; - type Value = TorSecretKey; - fn key_str(key: &Self::Key) -> Result, Error> { - Self::key_string(key) - } - fn key_string(key: &Self::Key) -> Result { - Ok(InternedString::from_display(key)) - } -} -impl OnionStore { - pub fn new() -> Self { - Self::default() - } - pub fn insert(&mut self, key: TorSecretKey) { - self.0.insert(key.onion_address(), key); - } -} -impl Model { - pub fn new_key(&mut self) -> Result { - let key = TorSecretKey::generate(); - self.insert_key(&key)?; - Ok(key) - } - pub fn insert_key(&mut self, key: &TorSecretKey) -> Result<(), Error> { - self.insert(&key.onion_address(), &key).map(|_| ()) - } - pub fn get_key(&self, address: &OnionAddress) -> Result { - self.as_idx(address) - .or_not_found(lazy_format!("private key for {address}"))? - .de() - } -} -impl std::fmt::Debug for OnionStore { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - struct OnionStoreMap<'a>(&'a BTreeMap); - impl<'a> std::fmt::Debug for OnionStoreMap<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - #[derive(Debug)] - struct KeyFor(#[allow(unused)] OnionAddress); - let mut map = f.debug_map(); - for (k, v) in self.0 { - map.key(k); - map.value(&KeyFor(v.onion_address())); - } - map.finish() - } - } - f.debug_tuple("OnionStore") - .field(&OnionStoreMap(&self.0)) - .finish() - } -} - -enum ErrorLogSeverity { - Fatal { wipe_state: bool }, - Unknown { wipe_state: bool }, -} - -lazy_static! { - static ref LOG_REGEXES: Vec<(Regex, ErrorLogSeverity)> = vec![( - Regex::new("This could indicate a route manipulation attack, network overload, bad local network connectivity, or a bug\\.").unwrap(), - ErrorLogSeverity::Unknown { wipe_state: true } - ),( - Regex::new("died due to an invalid selected path").unwrap(), - ErrorLogSeverity::Fatal { wipe_state: false } - ),( - Regex::new("Tor has not observed any network activity for the past").unwrap(), - ErrorLogSeverity::Unknown { wipe_state: false } - )]; - static ref PROGRESS_REGEX: Regex = Regex::new("PROGRESS=([0-9]+)").unwrap(); -} - -pub fn tor_api() -> ParentHandler { - ParentHandler::new() - .subcommand( - "list-services", - from_fn_async(list_services) - .with_display_serializable() - .with_custom_display_fn(|handle, result| display_services(handle.params, result)) - .with_about("about.display-tor-v3-onion-addresses") - .with_call_remote::(), - ) - .subcommand("logs", logs().with_about("about.display-tor-logs")) - .subcommand( - "logs", - from_fn_async(crate::logs::cli_logs::) - .no_display() - .with_about("about.display-tor-logs"), - ) - .subcommand( - "reset", - from_fn_async(reset) - .no_display() - .with_about("about.reset-tor-daemon") - .with_call_remote::(), - ) - .subcommand( - "key", - key::().with_about("about.manage-onion-service-key-store"), - ) -} - -pub fn key() -> ParentHandler { - ParentHandler::new() - .subcommand( - "generate", - from_fn_async(generate_key) - .with_about("about.generate-onion-service-key-add-to-store") - .with_call_remote::(), - ) - .subcommand( - "add", - from_fn_async(add_key) - .with_about("about.add-onion-service-key-to-store") - .with_call_remote::(), - ) - .subcommand( - "list", - from_fn_async(list_keys) - .with_custom_display_fn(|_, res| { - for addr in res { - println!("{addr}"); - } - Ok(()) - }) - .with_about("about.list-onion-services-with-keys-in-store") - .with_call_remote::(), - ) -} - -pub async fn generate_key(ctx: RpcContext) -> Result { - ctx.db - .mutate(|db| { - Ok(db - .as_private_mut() - .as_key_store_mut() - .as_onion_mut() - .new_key()? - .onion_address()) - }) - .await - .result -} - -#[derive(Deserialize, Serialize, Parser)] -pub struct AddKeyParams { - #[arg(help = "help.arg.onion-secret-key")] - pub key: Base64<[u8; 64]>, -} - -pub async fn add_key( - ctx: RpcContext, - AddKeyParams { key }: AddKeyParams, -) -> Result { - let key = TorSecretKey::from_bytes(key.0)?; - ctx.db - .mutate(|db| { - db.as_private_mut() - .as_key_store_mut() - .as_onion_mut() - .insert_key(&key) - }) - .await - .result?; - Ok(key.onion_address()) -} - -pub async fn list_keys(ctx: RpcContext) -> Result, Error> { - ctx.db - .peek() - .await - .into_private() - .into_key_store() - .into_onion() - .keys() -} - -#[derive(Deserialize, Serialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "kebab-case")] -pub struct ResetParams { - #[arg( - name = "wipe-state", - short = 'w', - long = "wipe-state", - help = "help.arg.wipe-tor-state" - )] - wipe_state: bool, - #[arg(help = "help.arg.reset-reason")] - reason: String, -} - -pub async fn reset( - ctx: RpcContext, - ResetParams { reason, wipe_state }: ResetParams, -) -> Result<(), Error> { - ctx.net_controller - .tor - .reset(wipe_state, Error::new(eyre!("{reason}"), ErrorKind::Tor)) - .await -} - -pub fn display_services( - params: WithIoFormat, - services: Vec, -) -> Result<(), Error> { - use prettytable::*; - - if let Some(format) = params.format { - return display_serializable(format, services); - } - - let mut table = Table::new(); - for service in services { - let row = row![&service.to_string()]; - table.add_row(row); - } - table.print_tty(false)?; - Ok(()) -} - -pub async fn list_services(ctx: RpcContext, _: Empty) -> Result, Error> { - ctx.net_controller.tor.list_services().await -} - -pub fn logs() -> ParentHandler { - crate::logs::logs::(|_: &RpcContext, _| async { - Ok(LogSource::Unit(SYSTEMD_UNIT)) - }) -} - -fn event_handler(_event: AsyncEvent<'static>) -> BoxFuture<'static, Result<(), ConnError>> { - async move { Ok(()) }.boxed() -} - -#[derive(Clone)] -pub struct TorController(Arc); -impl TorController { - const TOR_SOCKS: &[SocketAddr] = &[ - SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 9050)), - SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(10, 0, 3, 1), 9050)), - ]; - - pub fn new() -> Result { - Ok(TorController(Arc::new(TorControl::new( - TOR_CONTROL, - Self::TOR_SOCKS, - )))) - } - - pub fn service(&self, key: TorSecretKey) -> Result { - Ok(TorService { - services: self.0.services.clone(), - key, - }) - } - - pub async fn gc(&self, addr: Option) -> Result<(), Error> { - self.0.services.send_if_modified(|services| { - let mut changed = false; - let mut gc = |bindings: &mut OrdMap>>| { - ordmap_retain(bindings, |_, targets| { - let start_len = targets.len(); - targets.retain(|_, rc| rc.strong_count() > 0); - changed |= start_len != targets.len(); - !targets.is_empty() - }); - if bindings.is_empty() { - changed = true; - false - } else { - true - } - }; - if let Some(addr) = addr { - if !if let Some((_, bindings, needs_sync)) = services.get_mut(&addr) { - let keep = gc(bindings); - if !keep { - *needs_sync = Some(SyncState::Remove); - } - keep - } else { - true - } { - services.remove(&addr); - } - } else { - services.retain(|_, (_, bindings, _)| gc(bindings)); - } - changed - }); - Ok(()) - } - - pub async fn reset(&self, wipe_state: bool, context: Error) -> Result<(), Error> { - self.0 - .send - .send(TorCommand::Reset { - wipe_state, - context, - }) - .ok() - .ok_or_else(|| Error::new(eyre!("TorControl died"), ErrorKind::Tor)) - } - - pub async fn list_services(&self) -> Result, Error> { - let (reply, res) = oneshot::channel(); - self.0 - .send - .send(TorCommand::GetInfo { - query: "onions/current".into(), - reply, - }) - .ok() - .ok_or_else(|| Error::new(eyre!("TorControl died"), ErrorKind::Tor))?; - res.await - .ok() - .ok_or_else(|| Error::new(eyre!("TorControl died"), ErrorKind::Tor))?? - .lines() - .map(|l| l.trim()) - .filter(|l| !l.is_empty()) - .map(|l| l.parse::().with_kind(ErrorKind::Tor)) - .collect() - } - - pub async fn connect_onion( - &self, - addr: &OnionAddress, - port: u16, - ) -> Result, Error> { - if let Some(target) = self.0.services.peek(|s| { - s.get(addr).and_then(|(_, bindings, _)| { - bindings.get(&port).and_then(|b| { - b.iter() - .find(|(_, rc)| rc.strong_count() > 0) - .map(|(a, _)| *a) - }) - }) - }) { - tracing::debug!("Resolving {addr} internally to {target}"); - let tcp_stream = TcpStream::connect(target) - .await - .with_kind(ErrorKind::Network)?; - if let Err(e) = socket2::SockRef::from(&tcp_stream).set_keepalive(true) { - tracing::error!("Failed to set tcp keepalive: {e}"); - tracing::debug!("{e:?}"); - } - Ok(Box::new(tcp_stream)) - } else { - let mut stream = TcpStream::connect(Self::TOR_SOCKS[0]) - .await - .with_kind(ErrorKind::Tor)?; - if let Err(e) = socket2::SockRef::from(&stream).set_keepalive(true) { - tracing::error!("Failed to set tcp keepalive: {e}"); - tracing::debug!("{e:?}"); - } - socks5_impl::client::connect(&mut stream, (addr.to_string(), port), None) - .await - .with_kind(ErrorKind::Tor)?; - Ok(Box::new(stream)) - } - } -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum SyncState { - Add, - Update, - Remove, -} - -pub struct TorService { - services: Watch< - BTreeMap< - OnionAddress, - ( - TorSecretKey, - OrdMap>>, - Option, - ), - >, - >, - key: TorSecretKey, -} - -impl TorService { - pub fn proxy_all>>( - &self, - bindings: impl IntoIterator, - ) -> Rcs { - self.services.send_modify(|services| { - let (_, entry, needs_sync) = services - .entry(self.key.onion_address()) - .or_insert_with(|| (self.key.clone(), OrdMap::new(), Some(SyncState::Add))); - let rcs = bindings - .into_iter() - .map(|(external, target)| { - let binding = entry.entry(external).or_default(); - let target = binding.entry(target).or_default(); - let rc = if let Some(rc) = Weak::upgrade(&*target) { - rc - } else { - if needs_sync.is_none() { - *needs_sync = Some(SyncState::Update); - } - Arc::new(()) - }; - *target = Arc::downgrade(&rc); - rc - }) - .collect(); - - rcs - }) - } -} - -type AuthenticatedConnection = AuthenticatedConn< - TcpStream, - Box) -> BoxFuture<'static, Result<(), ConnError>> + Send + Sync>, ->; - -enum TorCommand { - GetInfo { - query: String, - reply: oneshot::Sender>, - }, - Reset { - wipe_state: bool, - context: Error, - }, -} - -#[instrument(skip_all)] -async fn torctl( - tor_control: SocketAddr, - tor_socks: &[SocketAddr], - recv: &mut mpsc::UnboundedReceiver, - services: &mut Watch< - BTreeMap< - OnionAddress, - ( - TorSecretKey, - OrdMap>>, - Option, - ), - >, - >, - wipe_state: &AtomicBool, - health_timeout: &mut Duration, -) -> Result<(), Error> { - let bootstrap = async { - if Command::new("systemctl") - .arg("is-active") - .arg("--quiet") - .arg("tor") - .invoke(ErrorKind::Tor) - .await - .is_ok() - { - Command::new("systemctl") - .arg("stop") - .arg("tor") - .invoke(ErrorKind::Tor) - .await?; - for _ in 0..30 { - if TcpStream::connect(tor_control).await.is_err() { - break; - } - tokio::time::sleep(Duration::from_secs(1)).await; - } - if TcpStream::connect(tor_control).await.is_ok() { - return Err(Error::new( - eyre!("Tor is failing to shut down"), - ErrorKind::Tor, - )); - } - } - if wipe_state.load(std::sync::atomic::Ordering::SeqCst) { - tokio::fs::remove_dir_all("/var/lib/tor").await?; - wipe_state.store(false, std::sync::atomic::Ordering::SeqCst); - } - - write_file_atomic("/etc/tor/torrc", { - use std::fmt::Write; - let mut conf = String::new(); - - for tor_socks in tor_socks { - writeln!(&mut conf, "SocksPort {tor_socks}").unwrap(); - } - writeln!( - &mut conf, - "ControlPort {tor_control}\nCookieAuthentication 1" - ) - .unwrap(); - conf - }) - .await?; - tokio::fs::create_dir_all("/var/lib/tor").await?; - Command::new("chown") - .arg("-R") - .arg("debian-tor") - .arg("/var/lib/tor") - .invoke(ErrorKind::Filesystem) - .await?; - Command::new("systemctl") - .arg("start") - .arg("tor") - .invoke(ErrorKind::Tor) - .await?; - - let mut tcp_stream = None; - for _ in 0..60 { - if let Ok(conn) = TcpStream::connect(tor_control).await { - tcp_stream = Some(conn); - break; - } - tokio::time::sleep(Duration::from_secs(1)).await; - } - let tcp_stream = tcp_stream.ok_or_else(|| { - Error::new(eyre!("Timed out waiting for tor to start"), ErrorKind::Tor) - })?; - tracing::info!("Tor is started"); - - if let Err(e) = socket2::SockRef::from(&tcp_stream).set_keepalive(true) { - tracing::error!("Failed to set tcp keepalive: {e}"); - tracing::debug!("{e:?}"); - } - - let mut conn = torut::control::UnauthenticatedConn::new(tcp_stream); - let auth = conn - .load_protocol_info() - .await? - .make_auth_data()? - .ok_or_else(|| eyre!("Cookie Auth Not Available")) - .with_kind(crate::ErrorKind::Tor)?; - conn.authenticate(&auth).await?; - let mut connection: AuthenticatedConnection = conn.into_authenticated().await; - connection.set_async_event_handler(Some(Box::new(|event| event_handler(event)))); - - let mut bootstrapped = false; - let mut last_increment = (String::new(), Instant::now()); - for _ in 0..300 { - match connection.get_info("status/bootstrap-phase").await { - Ok(a) => { - if a.contains("TAG=done") { - bootstrapped = true; - break; - } - if let Some(p) = PROGRESS_REGEX.captures(&a) { - if let Some(p) = p.get(1) { - if p.as_str() != &*last_increment.0 { - last_increment = (p.as_str().into(), Instant::now()); - } - } - } - } - Err(e) => { - let e = Error::from(e); - tracing::error!("{}", e); - tracing::debug!("{:?}", e); - } - } - if last_increment.1.elapsed() > Duration::from_secs(30) { - return Err(Error::new( - eyre!("Tor stuck bootstrapping at {}%", last_increment.0), - ErrorKind::Tor, - )); - } - tokio::time::sleep(Duration::from_secs(1)).await; - } - if !bootstrapped { - return Err(Error::new( - eyre!("Timed out waiting for tor to bootstrap"), - ErrorKind::Tor, - )); - } - Ok(connection) - }; - let pre_handler = async { - while let Some(command) = recv.recv().await { - match command { - TorCommand::GetInfo { reply, .. } => { - reply - .send(Err(Error::new( - eyre!("Tor has not finished bootstrapping..."), - ErrorKind::Tor, - ))) - .unwrap_or_default(); - } - TorCommand::Reset { - wipe_state: new_wipe_state, - context, - } => { - wipe_state.fetch_or(new_wipe_state, std::sync::atomic::Ordering::SeqCst); - return Err(context); - } - } - } - Ok(()) - }; - - let mut connection = tokio::select! { - res = bootstrap => res?, - res = pre_handler => return res, - }; - - let hck_key = TorSecretKeyV3::generate(); - connection - .add_onion_v3( - &hck_key, - false, - false, - false, - None, - &mut [(80, SocketAddr::from(([127, 0, 0, 1], 80)))].iter(), - ) - .await?; - - services.send_modify(|s| { - for (_, _, s) in s.values_mut() { - *s = Some(SyncState::Add); - } - }); - - let handler = async { - loop { - let recv = recv.recv(); - tokio::pin!(recv); - let mut changed = services.changed().boxed(); - - match futures::future::select(recv, &mut changed).await { - futures::future::Either::Left((Some(command), _)) => match command { - TorCommand::GetInfo { query, reply } => { - reply - .send(connection.get_info(&query).await.with_kind(ErrorKind::Tor)) - .unwrap_or_default(); - } - TorCommand::Reset { - wipe_state: new_wipe_state, - context, - } => { - wipe_state.fetch_or(new_wipe_state, std::sync::atomic::Ordering::SeqCst); - return Err(context); - } - }, - futures::future::Either::Left((None, _)) => break, - futures::future::Either::Right(_) => { - drop(changed); - let to_add = services.peek_and_mark_seen(|services| { - services - .iter() - .filter(|(_, (_, _, s))| s.is_some()) - .map(|(k, v)| (k.clone(), (*v).clone())) - .collect::>() - }); - - for (addr, (key, bindings, state)) in &to_add { - if matches!(state, Some(SyncState::Update) | Some(SyncState::Remove)) { - connection - .del_onion(&addr.0.get_address_without_dot_onion()) - .await - .with_kind(ErrorKind::Tor)?; - } - let bindings = bindings - .iter() - .filter_map(|(external, targets)| { - targets - .iter() - .find(|(_, rc)| rc.strong_count() > 0) - .map(|(target, _)| (*external, *target)) - }) - .collect::>(); - if !bindings.is_empty() { - connection - .add_onion_v3( - &key.0, - false, - false, - false, - None, - &mut bindings.iter(), - ) - .await?; - } - } - services.send_if_modified(|services| { - for (addr, (_, bindings_a, _)) in to_add { - if let Some((_, bindings_b, needs_sync)) = services.get_mut(&addr) { - if OrdMap::ptr_eq(&bindings_a, bindings_b) - || bindings_a.len() == bindings_b.len() - && bindings_a.iter().zip(bindings_b.iter()).all( - |((a_port, a), (b_port, b))| { - a_port == b_port - && a.len() == b.len() - && a.keys().zip(b.keys()).all(|(a, b)| a == b) - }, - ) - { - *needs_sync = None; - } else { - *needs_sync = Some(SyncState::Update); - } - } - } - false - }); - } - } - } - - Ok(()) - }; - let log_parser = async { - loop { - let mut logs = journalctl( - LogSource::Unit(SYSTEMD_UNIT), - Some(0), - None, - Some("0"), - false, - true, - ) - .await?; - while let Some(log) = logs.try_next().await? { - for (regex, severity) in &*LOG_REGEXES { - if regex.is_match(&log.message) { - let (check, wipe_state) = match severity { - ErrorLogSeverity::Fatal { wipe_state } => (false, *wipe_state), - ErrorLogSeverity::Unknown { wipe_state } => (true, *wipe_state), - }; - let addr = hck_key.public().get_onion_address().to_string(); - if !check - || TcpStream::connect(tor_socks) - .map_err(|e| Error::new(e, ErrorKind::Tor)) - .and_then(|mut tor_socks| async move { - tokio::time::timeout( - Duration::from_secs(30), - socks5_impl::client::connect( - &mut tor_socks, - (addr, 80), - None, - ) - .map_err(|e| Error::new(e, ErrorKind::Tor)), - ) - .map_err(|e| Error::new(e, ErrorKind::Tor)) - .await? - }) - .await - .with_ctx(|_| (ErrorKind::Tor, "Tor is confirmed to be down")) - .log_err() - .is_some() - { - if wipe_state { - Command::new("systemctl") - .arg("stop") - .arg("tor") - .invoke(ErrorKind::Tor) - .await?; - tokio::fs::remove_dir_all("/var/lib/tor").await?; - } - return Err(Error::new(eyre!("{}", log.message), ErrorKind::Tor)); - } - } - } - } - } - }; - let health_checker = async { - let mut last_success = Instant::now(); - loop { - tokio::time::sleep(Duration::from_secs(30)).await; - let addr = hck_key.public().get_onion_address().to_string(); - if TcpStream::connect(tor_socks) - .map_err(|e| Error::new(e, ErrorKind::Tor)) - .and_then(|mut tor_socks| async move { - tokio::time::timeout( - Duration::from_secs(30), - socks5_impl::client::connect(&mut tor_socks, (addr, 80), None) - .map_err(|e| Error::new(e, ErrorKind::Tor)), - ) - .map_err(|e| Error::new(e, ErrorKind::Tor)) - .await - }) - .await - .is_err() - { - if last_success.elapsed() > *health_timeout { - let err = Error::new( - eyre!( - "Tor health check failed for longer than current timeout ({health_timeout:?})" - ), - crate::ErrorKind::Tor, - ); - *health_timeout *= 2; - wipe_state.store(true, std::sync::atomic::Ordering::SeqCst); - return Err(err); - } - } else { - last_success = Instant::now(); - } - } - }; - - tokio::select! { - res = handler => res?, - res = log_parser => res?, - res = health_checker => res?, - } - - Ok(()) -} - -struct TorControl { - _thread: NonDetachingJoinHandle<()>, - send: mpsc::UnboundedSender, - services: Watch< - BTreeMap< - OnionAddress, - ( - TorSecretKey, - OrdMap>>, - Option, - ), - >, - >, -} -impl TorControl { - pub fn new( - tor_control: SocketAddr, - tor_socks: impl AsRef<[SocketAddr]> + Send + 'static, - ) -> Self { - let (send, mut recv) = mpsc::unbounded_channel(); - let services = Watch::new(BTreeMap::new()); - let mut thread_services = services.clone(); - Self { - _thread: tokio::spawn(async move { - let wipe_state = AtomicBool::new(false); - let mut health_timeout = Duration::from_secs(STARTING_HEALTH_TIMEOUT); - loop { - if let Err(e) = torctl( - tor_control, - tor_socks.as_ref(), - &mut recv, - &mut thread_services, - &wipe_state, - &mut health_timeout, - ) - .await - { - tracing::error!("TorControl : {e}"); - tracing::debug!("{e:?}"); - } - tracing::info!("Restarting Tor"); - tokio::time::sleep(Duration::from_secs(1)).await; - } - }) - .into(), - send, - services, - } - } -} - -#[tokio::test] -#[ignore] -async fn test_connection() { - let mut conn = torut::control::UnauthenticatedConn::new( - TcpStream::connect(SocketAddr::from(([127, 0, 0, 1], 9051))) - .await - .unwrap(), - ); - let auth = conn - .load_protocol_info() - .await - .unwrap() - .make_auth_data() - .unwrap() - .ok_or_else(|| eyre!("Cookie Auth Not Available")) - .with_kind(crate::ErrorKind::Tor) - .unwrap(); - conn.authenticate(&auth).await.unwrap(); - let mut connection: AuthenticatedConn< - TcpStream, - fn(AsyncEvent<'static>) -> BoxFuture<'static, Result<(), ConnError>>, - > = conn.into_authenticated().await; - let tor_key = torut::onion::TorSecretKeyV3::generate(); - connection.get_conf("SocksPort").await.unwrap(); - connection - .add_onion_v3( - &tor_key, - false, - false, - false, - None, - &mut [(443_u16, SocketAddr::from(([127, 0, 0, 1], 8443)))].iter(), - ) - .await - .unwrap(); - connection - .del_onion( - &tor_key - .public() - .get_onion_address() - .get_address_without_dot_onion(), - ) - .await - .unwrap(); - connection - .add_onion_v3( - &tor_key, - false, - false, - false, - None, - &mut [(8443_u16, SocketAddr::from(([127, 0, 0, 1], 8443)))].iter(), - ) - .await - .unwrap(); -} diff --git a/core/src/net/tor/mod.rs b/core/src/net/tor/mod.rs deleted file mode 100644 index d4d5c8007..000000000 --- a/core/src/net/tor/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -#[cfg(feature = "arti")] -mod arti; - -#[cfg(not(feature = "arti"))] -mod ctor; - -#[cfg(feature = "arti")] -pub use arti::{OnionAddress, OnionStore, TorController, TorSecretKey, tor_api}; -#[cfg(not(feature = "arti"))] -pub use ctor::{OnionAddress, OnionStore, TorController, TorSecretKey, tor_api}; diff --git a/core/src/service/effects/net/ssl.rs b/core/src/service/effects/net/ssl.rs index fcd1dac9d..317fb50c8 100644 --- a/core/src/service/effects/net/ssl.rs +++ b/core/src/service/effects/net/ssl.rs @@ -55,11 +55,9 @@ pub async fn get_ssl_certificate( .map(|(_, m)| m.as_hosts().as_entries()) .flatten_ok() .map_ok(|(_, m)| { - Ok(m.as_onions() - .de()? - .iter() - .map(InternedString::from_display) - .chain(m.as_public_domains().keys()?) + Ok(m.as_public_domains() + .keys()? + .into_iter() .chain(m.as_private_domains().de()?) .chain( m.as_hostname_info() @@ -68,7 +66,7 @@ pub async fn get_ssl_certificate( .flatten() .map(|h| h.to_san_hostname()), ) - .collect::>()) + .collect::>()) }) .map(|a| a.and_then(|a| a)) .flatten_ok() @@ -181,11 +179,9 @@ pub async fn get_ssl_key( .map(|m| m.as_hosts().as_entries()) .flatten_ok() .map_ok(|(_, m)| { - Ok(m.as_onions() - .de()? - .iter() - .map(InternedString::from_display) - .chain(m.as_public_domains().keys()?) + Ok(m.as_public_domains() + .keys()? + .into_iter() .chain(m.as_private_domains().de()?) .chain( m.as_hostname_info() @@ -194,7 +190,7 @@ pub async fn get_ssl_key( .flatten() .map(|h| h.to_san_hostname()), ) - .collect::>()) + .collect::>()) }) .map(|a| a.and_then(|a| a)) .flatten_ok() diff --git a/core/src/version/mod.rs b/core/src/version/mod.rs index 5c2a2a2f3..a470212f5 100644 --- a/core/src/version/mod.rs +++ b/core/src/version/mod.rs @@ -59,8 +59,9 @@ mod v0_4_0_alpha_16; mod v0_4_0_alpha_17; mod v0_4_0_alpha_18; mod v0_4_0_alpha_19; +mod v0_4_0_alpha_20; -pub type Current = v0_4_0_alpha_19::Version; // VERSION_BUMP +pub type Current = v0_4_0_alpha_20::Version; // VERSION_BUMP impl Current { #[instrument(skip(self, db))] @@ -181,7 +182,8 @@ enum Version { V0_4_0_alpha_16(Wrapper), V0_4_0_alpha_17(Wrapper), V0_4_0_alpha_18(Wrapper), - V0_4_0_alpha_19(Wrapper), // VERSION_BUMP + V0_4_0_alpha_19(Wrapper), + V0_4_0_alpha_20(Wrapper), // VERSION_BUMP Other(exver::Version), } @@ -243,7 +245,8 @@ impl Version { Self::V0_4_0_alpha_16(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_17(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_18(v) => DynVersion(Box::new(v.0)), - Self::V0_4_0_alpha_19(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP + Self::V0_4_0_alpha_19(v) => DynVersion(Box::new(v.0)), + Self::V0_4_0_alpha_20(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP Self::Other(v) => { return Err(Error::new( eyre!("unknown version {v}"), @@ -297,7 +300,8 @@ impl Version { Version::V0_4_0_alpha_16(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_17(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_18(Wrapper(x)) => x.semver(), - Version::V0_4_0_alpha_19(Wrapper(x)) => x.semver(), // VERSION_BUMP + Version::V0_4_0_alpha_19(Wrapper(x)) => x.semver(), + Version::V0_4_0_alpha_20(Wrapper(x)) => x.semver(), // VERSION_BUMP Version::Other(x) => x.clone(), } } diff --git a/core/src/version/v0_3_6_alpha_0.rs b/core/src/version/v0_3_6_alpha_0.rs index c048c97b2..f7a8dc0bf 100644 --- a/core/src/version/v0_3_6_alpha_0.rs +++ b/core/src/version/v0_3_6_alpha_0.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeMap; use std::ffi::OsStr; use std::path::Path; @@ -23,17 +23,14 @@ use crate::disk::mount::filesystem::cifs::Cifs; use crate::disk::mount::util::unmount; use crate::hostname::Hostname; use crate::net::forward::AvailablePorts; -use crate::net::host::Host; use crate::net::keys::KeyStore; -use crate::net::tor::{OnionAddress, TorSecretKey}; use crate::notifications::Notifications; use crate::prelude::*; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::ssh::{SshKeys, SshPubKey}; use crate::util::Invoke; -use crate::util::crypto::ed25519_expand_key; use crate::util::serde::Pem; -use crate::{DATA_DIR, HostId, Id, PACKAGE_DATA, PackageId, ReplayId}; +use crate::{DATA_DIR, PACKAGE_DATA, PackageId, ReplayId}; lazy_static::lazy_static! { static ref V0_3_6_alpha_0: exver::Version = exver::Version::new( @@ -146,12 +143,7 @@ pub struct Version; impl VersionT for Version { type Previous = v0_3_5_2::Version; - type PreUpRes = ( - AccountInfo, - SshKeys, - CifsTargets, - BTreeMap>, - ); + type PreUpRes = (AccountInfo, SshKeys, CifsTargets); fn semver(self) -> exver::Version { V0_3_6_alpha_0.clone() } @@ -166,20 +158,18 @@ impl VersionT for Version { let cifs = previous_cifs(&pg).await?; - let tor_keys = previous_tor_keys(&pg).await?; - Command::new("systemctl") .arg("stop") .arg("postgresql@*.service") .invoke(crate::ErrorKind::Database) .await?; - Ok((account, ssh_keys, cifs, tor_keys)) + Ok((account, ssh_keys, cifs)) } fn up( self, db: &mut Value, - (account, ssh_keys, cifs, tor_keys): Self::PreUpRes, + (account, ssh_keys, cifs): Self::PreUpRes, ) -> Result { let prev_package_data = db["package-data"].clone(); @@ -242,11 +232,7 @@ impl VersionT for Version { "ui": db["ui"], }); - let mut keystore = KeyStore::new(&account)?; - for key in tor_keys.values().flat_map(|v| v.values()) { - assert!(key.is_valid()); - keystore.onion.insert(key.clone()); - } + let keystore = KeyStore::new(&account)?; let private = { let mut value = json!({}); @@ -350,20 +336,6 @@ impl VersionT for Version { false }; - let onions = input[&*id]["installed"]["interface-addresses"] - .as_object() - .into_iter() - .flatten() - .filter_map(|(id, addrs)| { - addrs["tor-address"].as_str().map(|addr| { - Ok(( - HostId::from(Id::try_from(id.clone())?), - addr.parse::()?, - )) - }) - }) - .collect::, Error>>()?; - if let Err(e) = async { let package_s9pk = tokio::fs::File::open(path).await?; let file = MultiCursorFile::open(&package_s9pk).await?; @@ -381,11 +353,8 @@ impl VersionT for Version { .await? .await?; - let to_sync = ctx - .db + ctx.db .mutate(|db| { - let mut to_sync = BTreeSet::new(); - let package = db .as_public_mut() .as_package_data_mut() @@ -396,29 +365,11 @@ impl VersionT for Version { .as_tasks_mut() .remove(&ReplayId::from("needs-config"))?; } - for (id, onion) in onions { - package - .as_hosts_mut() - .upsert(&id, || Ok(Host::new()))? - .as_onions_mut() - .mutate(|o| { - o.clear(); - o.insert(onion); - Ok(()) - })?; - to_sync.insert(id); - } - Ok(to_sync) + Ok(()) }) .await .result?; - if let Some(service) = &*ctx.services.get(&id).await { - for host_id in to_sync { - service.sync_host(host_id.clone()).await?; - } - } - Ok::<_, Error>(()) } .await @@ -481,33 +432,6 @@ async fn previous_account_info(pg: &sqlx::Pool) -> Result>, _>("tor_key") - .with_ctx(|_| (ErrorKind::Database, "tor_key"))? - { - <[u8; 64]>::try_from(bytes).map_err(|e| { - Error::new( - eyre!("expected vec of len 64, got len {}", e.len()), - ErrorKind::ParseDbField, - ) - })? - } else { - ed25519_expand_key( - &<[u8; 32]>::try_from( - account_query - .try_get::, _>("network_key") - .with_kind(ErrorKind::Database)?, - ) - .map_err(|e| { - Error::new( - eyre!("expected vec of len 32, got len {}", e.len()), - ErrorKind::ParseDbField, - ) - })?, - ) - }, - )?], server_id: account_query .try_get("server_id") .with_ctx(|_| (ErrorKind::Database, "server_id"))?, @@ -579,68 +503,3 @@ async fn previous_ssh_keys(pg: &sqlx::Pool) -> Result, -) -> Result>, Error> { - let mut res = BTreeMap::>::new(); - let net_key_query = sqlx::query(r#"SELECT * FROM network_keys"#) - .fetch_all(pg) - .await - .with_kind(ErrorKind::Database)?; - - for row in net_key_query { - let package_id: PackageId = row - .try_get::("package") - .with_ctx(|_| (ErrorKind::Database, "network_keys::package"))? - .parse()?; - let interface_id: HostId = row - .try_get::("interface") - .with_ctx(|_| (ErrorKind::Database, "network_keys::interface"))? - .parse()?; - let key = TorSecretKey::from_bytes(ed25519_expand_key( - &<[u8; 32]>::try_from( - row.try_get::, _>("key") - .with_ctx(|_| (ErrorKind::Database, "network_keys::key"))?, - ) - .map_err(|e| { - Error::new( - eyre!("expected vec of len 32, got len {}", e.len()), - ErrorKind::ParseDbField, - ) - })?, - ))?; - res.entry(package_id).or_default().insert(interface_id, key); - } - - let tor_key_query = sqlx::query(r#"SELECT * FROM tor"#) - .fetch_all(pg) - .await - .with_kind(ErrorKind::Database)?; - - for row in tor_key_query { - let package_id: PackageId = row - .try_get::("package") - .with_ctx(|_| (ErrorKind::Database, "tor::package"))? - .parse()?; - let interface_id: HostId = row - .try_get::("interface") - .with_ctx(|_| (ErrorKind::Database, "tor::interface"))? - .parse()?; - let key = TorSecretKey::from_bytes( - <[u8; 64]>::try_from( - row.try_get::, _>("key") - .with_ctx(|_| (ErrorKind::Database, "tor::key"))?, - ) - .map_err(|e| { - Error::new( - eyre!("expected vec of len 64, got len {}", e.len()), - ErrorKind::ParseDbField, - ) - })?, - )?; - res.entry(package_id).or_default().insert(interface_id, key); - } - - Ok(res) -} diff --git a/core/src/version/v0_3_6_alpha_10.rs b/core/src/version/v0_3_6_alpha_10.rs index 08543d9e1..8cbf902a9 100644 --- a/core/src/version/v0_3_6_alpha_10.rs +++ b/core/src/version/v0_3_6_alpha_10.rs @@ -8,7 +8,6 @@ use super::v0_3_5::V0_3_0_COMPAT; use super::{VersionT, v0_3_6_alpha_9}; use crate::GatewayId; use crate::net::host::address::PublicDomainConfig; -use crate::net::tor::OnionAddress; use crate::prelude::*; lazy_static::lazy_static! { @@ -22,7 +21,7 @@ lazy_static::lazy_static! { #[serde(rename_all = "camelCase")] #[serde(tag = "kind")] enum HostAddress { - Onion { address: OnionAddress }, + Onion { address: String }, Domain { address: InternedString }, } diff --git a/core/src/version/v0_4_0_alpha_12.rs b/core/src/version/v0_4_0_alpha_12.rs index fa7e5a189..d998945a2 100644 --- a/core/src/version/v0_4_0_alpha_12.rs +++ b/core/src/version/v0_4_0_alpha_12.rs @@ -1,11 +1,7 @@ -use std::collections::BTreeSet; - use exver::{PreReleaseSegment, VersionRange}; -use imbl_value::InternedString; use super::v0_3_5::V0_3_0_COMPAT; use super::{VersionT, v0_4_0_alpha_11}; -use crate::net::tor::TorSecretKey; use crate::prelude::*; lazy_static::lazy_static! { @@ -33,48 +29,6 @@ impl VersionT for Version { } #[instrument(skip_all)] fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result { - let mut err = None; - let onion_store = db["private"]["keyStore"]["onion"] - .as_object_mut() - .or_not_found("private.keyStore.onion")?; - onion_store.retain(|o, v| match from_value::(v.clone()) { - Ok(k) => k.is_valid() && &InternedString::from_display(&k.onion_address()) == o, - Err(e) => { - err = Some(e); - true - } - }); - if let Some(e) = err { - return Err(e); - } - let allowed_addresses = onion_store.keys().cloned().collect::>(); - let fix_host = |host: &mut Value| { - Ok::<_, Error>( - host["onions"] - .as_array_mut() - .or_not_found("host.onions")? - .retain(|addr| { - addr.as_str() - .map(|s| allowed_addresses.contains(s)) - .unwrap_or(false) - }), - ) - }; - for (_, pde) in db["public"]["packageData"] - .as_object_mut() - .or_not_found("public.packageData")? - .iter_mut() - { - for (_, host) in pde["hosts"] - .as_object_mut() - .or_not_found("public.packageData[].hosts")? - .iter_mut() - { - fix_host(host)?; - } - } - fix_host(&mut db["public"]["serverInfo"]["network"]["host"])?; - if db["private"]["keyStore"]["localCerts"].is_null() { db["private"]["keyStore"]["localCerts"] = db["private"]["keyStore"]["local_certs"].clone(); diff --git a/core/src/version/v0_4_0_alpha_20.rs b/core/src/version/v0_4_0_alpha_20.rs new file mode 100644 index 000000000..6cb902bde --- /dev/null +++ b/core/src/version/v0_4_0_alpha_20.rs @@ -0,0 +1,110 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{VersionT, v0_4_0_alpha_19}; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_4_0_alpha_20: exver::Version = exver::Version::new( + [0, 4, 0], + [PreReleaseSegment::String("alpha".into()), 20.into()] + ); +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_4_0_alpha_19::Version; + type PreUpRes = (); + + async fn pre_up(self) -> Result { + Ok(()) + } + fn semver(self) -> exver::Version { + V0_4_0_alpha_20.clone() + } + fn compat(self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + #[instrument(skip_all)] + fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result { + // Remove onions and tor-related fields from server host + if let Some(host) = db + .get_mut("public") + .and_then(|p| p.get_mut("serverInfo")) + .and_then(|s| s.get_mut("network")) + .and_then(|n| n.get_mut("host")) + .and_then(|h| h.as_object_mut()) + { + host.remove("onions"); + } + + // Remove onions from all package hosts + if let Some(packages) = db + .get_mut("public") + .and_then(|p| p.get_mut("packageData")) + .and_then(|p| p.as_object_mut()) + { + for (_, package) in packages.iter_mut() { + if let Some(hosts) = package.get_mut("hosts").and_then(|h| h.as_object_mut()) { + for (_, host) in hosts.iter_mut() { + if let Some(host_obj) = host.as_object_mut() { + host_obj.remove("onions"); + } + } + } + } + } + + // Remove onion entries from hostnameInfo in server host + remove_onion_hostname_info( + db.get_mut("public") + .and_then(|p| p.get_mut("serverInfo")) + .and_then(|s| s.get_mut("network")) + .and_then(|n| n.get_mut("host")), + ); + + // Remove onion entries from hostnameInfo in all package hosts + if let Some(packages) = db + .get_mut("public") + .and_then(|p| p.get_mut("packageData")) + .and_then(|p| p.as_object_mut()) + { + for (_, package) in packages.iter_mut() { + if let Some(hosts) = package.get_mut("hosts").and_then(|h| h.as_object_mut()) { + for (_, host) in hosts.iter_mut() { + remove_onion_hostname_info(Some(host)); + } + } + } + } + + // Remove onion store from private keyStore + if let Some(key_store) = db + .get_mut("private") + .and_then(|p| p.get_mut("keyStore")) + .and_then(|k| k.as_object_mut()) + { + key_store.remove("onion"); + } + + Ok(Value::Null) + } + fn down(self, _db: &mut Value) -> Result<(), Error> { + Ok(()) + } +} + +fn remove_onion_hostname_info(host: Option<&mut Value>) { + if let Some(hostname_info) = host + .and_then(|h| h.get_mut("hostnameInfo")) + .and_then(|h| h.as_object_mut()) + { + for (_, infos) in hostname_info.iter_mut() { + if let Some(arr) = infos.as_array_mut() { + arr.retain(|info| info.get("kind").and_then(|k| k.as_str()) != Some("onion")); + } + } + } +} diff --git a/sdk/base/lib/exver/exver.ts b/sdk/base/lib/exver/exver.ts index 352178cca..94202fde1 100644 --- a/sdk/base/lib/exver/exver.ts +++ b/sdk/base/lib/exver/exver.ts @@ -1,3195 +1,2902 @@ /* eslint-disable */ -const peggyParser: { parse: any; SyntaxError: any; DefaultTracer?: any } = // Generated by Peggy 3.0.2. - // - // https://peggyjs.org/ - // @ts-ignore - (function () { - // @ts-ignore - 'use strict' - // @ts-ignore - function peg$subclass(child, parent) { - // @ts-ignore - function C() { - this.constructor = child - } - // @ts-ignore - C.prototype = parent.prototype - // @ts-ignore - child.prototype = new C() - } - // @ts-ignore - function peg$SyntaxError(message, expected, found, location) { - // @ts-ignore - var self = Error.call(this, message) - // istanbul ignore next Check is a necessary evil to support older environments - // @ts-ignore - if (Object.setPrototypeOf) { - // @ts-ignore - Object.setPrototypeOf(self, peg$SyntaxError.prototype) - } - // @ts-ignore - self.expected = expected - // @ts-ignore - self.found = found - // @ts-ignore - self.location = location - // @ts-ignore - self.name = 'SyntaxError' - // @ts-ignore - return self - } - - // @ts-ignore - peg$subclass(peg$SyntaxError, Error) - - // @ts-ignore - function peg$padEnd(str, targetLength, padString) { - // @ts-ignore - padString = padString || ' ' - // @ts-ignore - if (str.length > targetLength) { - return str - } - // @ts-ignore - targetLength -= str.length - // @ts-ignore - padString += padString.repeat(targetLength) - // @ts-ignore - return str + padString.slice(0, targetLength) - } - - // @ts-ignore - peg$SyntaxError.prototype.format = function (sources) { - // @ts-ignore - var str = 'Error: ' + this.message - // @ts-ignore - if (this.location) { - // @ts-ignore - var src = null - // @ts-ignore - var k - // @ts-ignore - for (k = 0; k < sources.length; k++) { - // @ts-ignore - if (sources[k].source === this.location.source) { - // @ts-ignore - src = sources[k].text.split(/\r\n|\n|\r/g) - // @ts-ignore - break - } - } - // @ts-ignore - var s = this.location.start - // @ts-ignore - var offset_s = - this.location.source && - typeof this.location.source.offset === 'function' - ? // @ts-ignore - this.location.source.offset(s) - : // @ts-ignore - s - // @ts-ignore - var loc = - this.location.source + ':' + offset_s.line + ':' + offset_s.column - // @ts-ignore - if (src) { - // @ts-ignore - var e = this.location.end - // @ts-ignore - var filler = peg$padEnd('', offset_s.line.toString().length, ' ') - // @ts-ignore - var line = src[s.line - 1] - // @ts-ignore - var last = s.line === e.line ? e.column : line.length + 1 - // @ts-ignore - var hatLen = last - s.column || 1 - // @ts-ignore - str += - '\n --> ' + - loc + - '\n' + - // @ts-ignore - filler + - ' |\n' + - // @ts-ignore - offset_s.line + - ' | ' + - line + - '\n' + - // @ts-ignore - filler + - ' | ' + - peg$padEnd('', s.column - 1, ' ') + - // @ts-ignore - peg$padEnd('', hatLen, '^') - // @ts-ignore - } else { - // @ts-ignore - str += '\n at ' + loc - } - } - // @ts-ignore - return str - } - - // @ts-ignore - peg$SyntaxError.buildMessage = function (expected, found) { - // @ts-ignore - var DESCRIBE_EXPECTATION_FNS = { - // @ts-ignore - literal: function (expectation) { - // @ts-ignore - return '"' + literalEscape(expectation.text) + '"' - }, - - // @ts-ignore - class: function (expectation) { - // @ts-ignore - var escapedParts = expectation.parts.map(function (part) { - // @ts-ignore - return Array.isArray(part) - ? // @ts-ignore - classEscape(part[0]) + '-' + classEscape(part[1]) - : // @ts-ignore - classEscape(part) - }) - - // @ts-ignore - return ( - '[' + - (expectation.inverted ? '^' : '') + - escapedParts.join('') + - ']' - ) - }, - - // @ts-ignore - any: function () { - // @ts-ignore - return 'any character' - }, - - // @ts-ignore - end: function () { - // @ts-ignore - return 'end of input' - }, - - // @ts-ignore - other: function (expectation) { - // @ts-ignore - return expectation.description - }, - } - - // @ts-ignore - function hex(ch) { - // @ts-ignore - return ch.charCodeAt(0).toString(16).toUpperCase() - } - - // @ts-ignore - function literalEscape(s) { - // @ts-ignore - return ( - s - // @ts-ignore - .replace(/\\/g, '\\\\') - // @ts-ignore - .replace(/"/g, '\\"') - // @ts-ignore - .replace(/\0/g, '\\0') - // @ts-ignore - .replace(/\t/g, '\\t') - // @ts-ignore - .replace(/\n/g, '\\n') - // @ts-ignore - .replace(/\r/g, '\\r') - // @ts-ignore - .replace(/[\x00-\x0F]/g, function (ch) { - return '\\x0' + hex(ch) - }) - // @ts-ignore - .replace(/[\x10-\x1F\x7F-\x9F]/g, function (ch) { - return '\\x' + hex(ch) - }) - ) - } - - // @ts-ignore - function classEscape(s) { - // @ts-ignore - return ( - s - // @ts-ignore - .replace(/\\/g, '\\\\') - // @ts-ignore - .replace(/\]/g, '\\]') - // @ts-ignore - .replace(/\^/g, '\\^') - // @ts-ignore - .replace(/-/g, '\\-') - // @ts-ignore - .replace(/\0/g, '\\0') - // @ts-ignore - .replace(/\t/g, '\\t') - // @ts-ignore - .replace(/\n/g, '\\n') - // @ts-ignore - .replace(/\r/g, '\\r') - // @ts-ignore - .replace(/[\x00-\x0F]/g, function (ch) { - return '\\x0' + hex(ch) - }) - // @ts-ignore - .replace(/[\x10-\x1F\x7F-\x9F]/g, function (ch) { - return '\\x' + hex(ch) - }) - ) - } - - // @ts-ignore - function describeExpectation(expectation) { - // @ts-ignore - return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation) - } - - // @ts-ignore - function describeExpected(expected) { - // @ts-ignore - var descriptions = expected.map(describeExpectation) - // @ts-ignore - var i, j - - // @ts-ignore - descriptions.sort() - - // @ts-ignore - if (descriptions.length > 0) { - // @ts-ignore - for (i = 1, j = 1; i < descriptions.length; i++) { - // @ts-ignore - if (descriptions[i - 1] !== descriptions[i]) { - // @ts-ignore - descriptions[j] = descriptions[i] - // @ts-ignore - j++ - } - } - // @ts-ignore - descriptions.length = j - } - - // @ts-ignore - switch (descriptions.length) { - // @ts-ignore - case 1: - // @ts-ignore - return descriptions[0] - - // @ts-ignore - case 2: - // @ts-ignore - return descriptions[0] + ' or ' + descriptions[1] - - // @ts-ignore - default: - // @ts-ignore - return ( - descriptions.slice(0, -1).join(', ') + - // @ts-ignore - ', or ' + - // @ts-ignore - descriptions[descriptions.length - 1] - ) - } - } - - // @ts-ignore - function describeFound(found) { - // @ts-ignore - return found ? '"' + literalEscape(found) + '"' : 'end of input' - } - - // @ts-ignore - return ( - 'Expected ' + - describeExpected(expected) + - ' but ' + - describeFound(found) + - ' found.' - ) - } - - // @ts-ignore - function peg$parse(input, options) { - // @ts-ignore - options = options !== undefined ? options : {} - - // @ts-ignore - var peg$FAILED = {} - // @ts-ignore - var peg$source = options.grammarSource - - // @ts-ignore - var peg$startRuleFunctions = { - VersionRange: peg$parseVersionRange, - Or: peg$parseOr, - And: peg$parseAnd, - VersionRangeAtom: peg$parseVersionRangeAtom, - Parens: peg$parseParens, - Anchor: peg$parseAnchor, - VersionSpec: peg$parseVersionSpec, - FlavorAtom: peg$parseFlavorAtom, - Not: peg$parseNot, - Any: peg$parseAny, - None: peg$parseNone, - CmpOp: peg$parseCmpOp, - ExtendedVersion: peg$parseExtendedVersion, - EmverVersionRange: peg$parseEmverVersionRange, - EmverVersionRangeAtom: peg$parseEmverVersionRangeAtom, - EmverParens: peg$parseEmverParens, - EmverAnchor: peg$parseEmverAnchor, - EmverNot: peg$parseEmverNot, - Emver: peg$parseEmver, - Flavor: peg$parseFlavor, - FlavorString: peg$parseFlavorString, - String: peg$parseString, - Version: peg$parseVersion, - PreRelease: peg$parsePreRelease, - PreReleaseSegment: peg$parsePreReleaseSegment, - VersionNumber: peg$parseVersionNumber, - Digit: peg$parseDigit, - _: peg$parse_, - } - // @ts-ignore - var peg$startRuleFunction = peg$parseVersionRange - - // @ts-ignore - var peg$c0 = '||' - var peg$c1 = '&&' - var peg$c2 = '(' - var peg$c3 = ')' - var peg$c4 = ':' - var peg$c5 = '#' - var peg$c6 = '!' - var peg$c7 = '*' - var peg$c8 = '>=' - var peg$c9 = '<=' - var peg$c10 = '>' - var peg$c11 = '<' - var peg$c12 = '=' - var peg$c13 = '!=' - var peg$c14 = '^' - var peg$c15 = '~' - var peg$c16 = '.' - var peg$c17 = '-' - - var peg$r0 = /^[a-z]/ - var peg$r1 = /^[a-zA-Z]/ - var peg$r2 = /^[0-9]/ - var peg$r3 = /^[ \t\n\r]/ - - var peg$e0 = peg$literalExpectation('||', false) - var peg$e1 = peg$literalExpectation('&&', false) - var peg$e2 = peg$literalExpectation('(', false) - var peg$e3 = peg$literalExpectation(')', false) - var peg$e4 = peg$literalExpectation(':', false) - var peg$e5 = peg$literalExpectation('#', false) - var peg$e6 = peg$literalExpectation('!', false) - var peg$e7 = peg$literalExpectation('*', false) - var peg$e8 = peg$literalExpectation('>=', false) - var peg$e9 = peg$literalExpectation('<=', false) - var peg$e10 = peg$literalExpectation('>', false) - var peg$e11 = peg$literalExpectation('<', false) - var peg$e12 = peg$literalExpectation('=', false) - var peg$e13 = peg$literalExpectation('!=', false) - var peg$e14 = peg$literalExpectation('^', false) - var peg$e15 = peg$literalExpectation('~', false) - var peg$e16 = peg$literalExpectation('.', false) - var peg$e17 = peg$classExpectation([['a', 'z']], false, false) - var peg$e18 = peg$classExpectation( - [ - ['a', 'z'], - ['A', 'Z'], - ], - false, - false, - ) - var peg$e19 = peg$literalExpectation('-', false) - var peg$e20 = peg$classExpectation([['0', '9']], false, false) - var peg$e21 = peg$otherExpectation('whitespace') - var peg$e22 = peg$classExpectation([' ', '\t', '\n', '\r'], false, false) - // @ts-ignore - - var peg$f0 = function (expr) { - // @ts-ignore - return { type: 'Parens', expr } - } // @ts-ignore - - var peg$f1 = function (operator, version) { - // @ts-ignore - return { type: 'Anchor', operator, version } - } // @ts-ignore - - var peg$f2 = function (flavor, upstream, downstream) { - // @ts-ignore - return { - flavor: flavor || null, - upstream, - downstream: downstream - ? downstream[1] - : { number: [0], prerelease: [] }, - } - } // @ts-ignore - - var peg$f3 = function (flavor) { - // @ts-ignore - return { type: 'Flavor', flavor: flavor } - } // @ts-ignore - - var peg$f4 = function (value) { - // @ts-ignore - return { type: 'Not', value: value } - } // @ts-ignore - - var peg$f5 = function () { - // @ts-ignore - return { type: 'Any' } - } // @ts-ignore - - var peg$f6 = function () { - // @ts-ignore - return { type: 'None' } - } // @ts-ignore - - var peg$f7 = function () { - // @ts-ignore - return '>=' - } // @ts-ignore - - var peg$f8 = function () { - // @ts-ignore - return '<=' - } // @ts-ignore - - var peg$f9 = function () { - // @ts-ignore - return '>' - } // @ts-ignore - - var peg$f10 = function () { - // @ts-ignore - return '<' - } // @ts-ignore - - var peg$f11 = function () { - // @ts-ignore - return '=' - } // @ts-ignore - - var peg$f12 = function () { - // @ts-ignore - return '!=' - } // @ts-ignore - - var peg$f13 = function () { - // @ts-ignore - return '^' - } // @ts-ignore - - var peg$f14 = function () { - // @ts-ignore - return '~' - } // @ts-ignore - - var peg$f15 = function (flavor, upstream, downstream) { - // @ts-ignore - return { flavor: flavor || null, upstream, downstream } - } // @ts-ignore - - var peg$f16 = function (expr) { - // @ts-ignore - return { type: 'Parens', expr } - } // @ts-ignore - - var peg$f17 = function (operator, version) { - // @ts-ignore - return { type: 'Anchor', operator, version } - } // @ts-ignore - - var peg$f18 = function (value) { - // @ts-ignore - return { type: 'Not', value: value } - } // @ts-ignore - - var peg$f19 = function (major, minor, patch, revision) { - // @ts-ignore - return revision - } // @ts-ignore - - var peg$f20 = function (major, minor, patch, revision) { - // @ts-ignore - return { - // @ts-ignore - flavor: null, - // @ts-ignore - upstream: { - // @ts-ignore - number: [major, minor, patch], - // @ts-ignore - prerelease: [], - }, - // @ts-ignore - downstream: { - // @ts-ignore - number: [revision || 0], - // @ts-ignore - prerelease: [], - }, - } - } // @ts-ignore - - var peg$f21 = function (flavor) { - // @ts-ignore - return flavor - } // @ts-ignore - - var peg$f22 = function () { - // @ts-ignore - return text() - } // @ts-ignore - - var peg$f23 = function () { - // @ts-ignore - return text() - } // @ts-ignore - - var peg$f24 = function (number, prerelease) { - // @ts-ignore - return { - // @ts-ignore - number, - // @ts-ignore - prerelease: prerelease || [], - } - } // @ts-ignore - - var peg$f25 = function (first, rest) { - // @ts-ignore - return [first].concat(rest.map((r) => r[1])) - } // @ts-ignore - - var peg$f26 = function (segment) { - // @ts-ignore - return segment - } // @ts-ignore - - var peg$f27 = function (first, rest) { - // @ts-ignore - return [first].concat(rest.map((r) => r[1])) - } // @ts-ignore - - var peg$f28 = function () { - // @ts-ignore - return parseInt(text(), 10) - } - // @ts-ignore - var peg$currPos = 0 - // @ts-ignore - var peg$savedPos = 0 - // @ts-ignore - var peg$posDetailsCache = [{ line: 1, column: 1 }] - // @ts-ignore - var peg$maxFailPos = 0 - // @ts-ignore - var peg$maxFailExpected = [] - // @ts-ignore - var peg$silentFails = 0 - - // @ts-ignore - var peg$result - - // @ts-ignore - if ('startRule' in options) { - // @ts-ignore - if (!(options.startRule in peg$startRuleFunctions)) { - // @ts-ignore - throw new Error( - 'Can\'t start parsing from rule "' + options.startRule + '".', - ) - } - - // @ts-ignore - peg$startRuleFunction = peg$startRuleFunctions[options.startRule] - } - - // @ts-ignore - function text() { - // @ts-ignore - return input.substring(peg$savedPos, peg$currPos) - } - - // @ts-ignore - function offset() { - // @ts-ignore - return peg$savedPos - } - - // @ts-ignore - function range() { - // @ts-ignore - return { - // @ts-ignore - source: peg$source, - // @ts-ignore - start: peg$savedPos, - // @ts-ignore - end: peg$currPos, - } - } - - // @ts-ignore - function location() { - // @ts-ignore - return peg$computeLocation(peg$savedPos, peg$currPos) - } - - // @ts-ignore - function expected(description, location) { - // @ts-ignore - location = - location !== undefined - ? // @ts-ignore - location - : // @ts-ignore - peg$computeLocation(peg$savedPos, peg$currPos) - - // @ts-ignore - throw peg$buildStructuredError( - // @ts-ignore - [peg$otherExpectation(description)], - // @ts-ignore - input.substring(peg$savedPos, peg$currPos), - // @ts-ignore - location, - ) - } - - // @ts-ignore - function error(message, location) { - // @ts-ignore - location = - location !== undefined - ? // @ts-ignore - location - : // @ts-ignore - peg$computeLocation(peg$savedPos, peg$currPos) - - // @ts-ignore - throw peg$buildSimpleError(message, location) - } - - // @ts-ignore - function peg$literalExpectation(text, ignoreCase) { - // @ts-ignore - return { type: 'literal', text: text, ignoreCase: ignoreCase } - } - - // @ts-ignore - function peg$classExpectation(parts, inverted, ignoreCase) { - // @ts-ignore - return { - type: 'class', - parts: parts, - inverted: inverted, - ignoreCase: ignoreCase, - } - } - - // @ts-ignore - function peg$anyExpectation() { - // @ts-ignore - return { type: 'any' } - } - - // @ts-ignore - function peg$endExpectation() { - // @ts-ignore - return { type: 'end' } - } - - // @ts-ignore - function peg$otherExpectation(description) { - // @ts-ignore - return { type: 'other', description: description } - } - - // @ts-ignore - function peg$computePosDetails(pos) { - // @ts-ignore - var details = peg$posDetailsCache[pos] - // @ts-ignore - var p - - // @ts-ignore - if (details) { - // @ts-ignore - return details - // @ts-ignore - } else { - // @ts-ignore - p = pos - 1 - // @ts-ignore - while (!peg$posDetailsCache[p]) { - // @ts-ignore - p-- - } - - // @ts-ignore - details = peg$posDetailsCache[p] - // @ts-ignore - details = { - // @ts-ignore - line: details.line, - // @ts-ignore - column: details.column, - } - - // @ts-ignore - while (p < pos) { - // @ts-ignore - if (input.charCodeAt(p) === 10) { - // @ts-ignore - details.line++ - // @ts-ignore - details.column = 1 - // @ts-ignore - } else { - // @ts-ignore - details.column++ - } - - // @ts-ignore - p++ - } - - // @ts-ignore - peg$posDetailsCache[pos] = details - - // @ts-ignore - return details - } - } - - // @ts-ignore - function peg$computeLocation(startPos, endPos, offset) { - // @ts-ignore - var startPosDetails = peg$computePosDetails(startPos) - // @ts-ignore - var endPosDetails = peg$computePosDetails(endPos) - - // @ts-ignore - var res = { - // @ts-ignore - source: peg$source, - // @ts-ignore - start: { - // @ts-ignore - offset: startPos, - // @ts-ignore - line: startPosDetails.line, - // @ts-ignore - column: startPosDetails.column, - }, - // @ts-ignore - end: { - // @ts-ignore - offset: endPos, - // @ts-ignore - line: endPosDetails.line, - // @ts-ignore - column: endPosDetails.column, - }, - } - // @ts-ignore - if (offset && peg$source && typeof peg$source.offset === 'function') { - // @ts-ignore - res.start = peg$source.offset(res.start) - // @ts-ignore - res.end = peg$source.offset(res.end) - } - // @ts-ignore - return res - } - - // @ts-ignore - function peg$fail(expected) { - // @ts-ignore - if (peg$currPos < peg$maxFailPos) { - return - } - - // @ts-ignore - if (peg$currPos > peg$maxFailPos) { - // @ts-ignore - peg$maxFailPos = peg$currPos - // @ts-ignore - peg$maxFailExpected = [] - } - - // @ts-ignore - peg$maxFailExpected.push(expected) - } - - // @ts-ignore - function peg$buildSimpleError(message, location) { - // @ts-ignore - return new peg$SyntaxError(message, null, null, location) - } - - // @ts-ignore - function peg$buildStructuredError(expected, found, location) { - // @ts-ignore - return new peg$SyntaxError( - // @ts-ignore - peg$SyntaxError.buildMessage(expected, found), - // @ts-ignore - expected, - // @ts-ignore - found, - // @ts-ignore - location, - ) - } - - // @ts-ignore - function // @ts-ignore - peg$parseVersionRange() { - // @ts-ignore - var s0, s1, s2, s3, s4, s5, s6, s7 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - s1 = peg$parseVersionRangeAtom() - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - s2 = [] - // @ts-ignore - s3 = peg$currPos - // @ts-ignore - s4 = peg$parse_() - // @ts-ignore - s5 = peg$currPos - // @ts-ignore - s6 = peg$parseOr() - // @ts-ignore - if (s6 === peg$FAILED) { - // @ts-ignore - s6 = peg$parseAnd() - } - // @ts-ignore - if (s6 !== peg$FAILED) { - // @ts-ignore - s7 = peg$parse_() - // @ts-ignore - s6 = [s6, s7] - // @ts-ignore - s5 = s6 - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s5 - // @ts-ignore - s5 = peg$FAILED - } - // @ts-ignore - if (s5 === peg$FAILED) { - // @ts-ignore - s5 = null - } - // @ts-ignore - s6 = peg$parseVersionRangeAtom() - // @ts-ignore - if (s6 !== peg$FAILED) { - // @ts-ignore - s4 = [s4, s5, s6] - // @ts-ignore - s3 = s4 - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s3 - // @ts-ignore - s3 = peg$FAILED - } - // @ts-ignore - while (s3 !== peg$FAILED) { - // @ts-ignore - s2.push(s3) - // @ts-ignore - s3 = peg$currPos - // @ts-ignore - s4 = peg$parse_() - // @ts-ignore - s5 = peg$currPos - // @ts-ignore - s6 = peg$parseOr() - // @ts-ignore - if (s6 === peg$FAILED) { - // @ts-ignore - s6 = peg$parseAnd() - } - // @ts-ignore - if (s6 !== peg$FAILED) { - // @ts-ignore - s7 = peg$parse_() - // @ts-ignore - s6 = [s6, s7] - // @ts-ignore - s5 = s6 - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s5 - // @ts-ignore - s5 = peg$FAILED - } - // @ts-ignore - if (s5 === peg$FAILED) { - // @ts-ignore - s5 = null - } - // @ts-ignore - s6 = peg$parseVersionRangeAtom() - // @ts-ignore - if (s6 !== peg$FAILED) { - // @ts-ignore - s4 = [s4, s5, s6] - // @ts-ignore - s3 = s4 - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s3 - // @ts-ignore - s3 = peg$FAILED - } - } - // @ts-ignore - s1 = [s1, s2] - // @ts-ignore - s0 = s1 - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseOr() { - // @ts-ignore - var s0 - - // @ts-ignore - if (input.substr(peg$currPos, 2) === peg$c0) { - // @ts-ignore - s0 = peg$c0 - // @ts-ignore - peg$currPos += 2 - // @ts-ignore - } else { - // @ts-ignore - s0 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e0) - } - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseAnd() { - // @ts-ignore - var s0 - - // @ts-ignore - if (input.substr(peg$currPos, 2) === peg$c1) { - // @ts-ignore - s0 = peg$c1 - // @ts-ignore - peg$currPos += 2 - // @ts-ignore - } else { - // @ts-ignore - s0 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e1) - } - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseVersionRangeAtom() { - // @ts-ignore - var s0 - - // @ts-ignore - s0 = peg$parseParens() - // @ts-ignore - if (s0 === peg$FAILED) { - // @ts-ignore - s0 = peg$parseAnchor() - // @ts-ignore - if (s0 === peg$FAILED) { - // @ts-ignore - s0 = peg$parseNot() - // @ts-ignore - if (s0 === peg$FAILED) { - // @ts-ignore - s0 = peg$parseAny() - // @ts-ignore - if (s0 === peg$FAILED) { - // @ts-ignore - s0 = peg$parseNone() - // @ts-ignore - if (s0 === peg$FAILED) { - // @ts-ignore - s0 = peg$parseFlavorAtom() - } - } - } - } - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseParens() { - // @ts-ignore - var s0, s1, s2, s3, s4, s5 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 40) { - // @ts-ignore - s1 = peg$c2 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s1 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e2) - } - } - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - s2 = peg$parse_() - // @ts-ignore - s3 = peg$parseVersionRange() - // @ts-ignore - if (s3 !== peg$FAILED) { - // @ts-ignore - s4 = peg$parse_() - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 41) { - // @ts-ignore - s5 = peg$c3 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s5 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e3) - } - } - // @ts-ignore - if (s5 !== peg$FAILED) { - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s0 = peg$f0(s3) - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseAnchor() { - // @ts-ignore - var s0, s1, s2, s3 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - s1 = peg$parseCmpOp() - // @ts-ignore - if (s1 === peg$FAILED) { - // @ts-ignore - s1 = null - } - // @ts-ignore - s2 = peg$parse_() - // @ts-ignore - s3 = peg$parseVersionSpec() - // @ts-ignore - if (s3 !== peg$FAILED) { - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s0 = peg$f1(s1, s3) - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseVersionSpec() { - // @ts-ignore - var s0, s1, s2, s3, s4, s5 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - s1 = peg$parseFlavor() - // @ts-ignore - if (s1 === peg$FAILED) { - // @ts-ignore - s1 = null - } - // @ts-ignore - s2 = peg$parseVersion() - // @ts-ignore - if (s2 !== peg$FAILED) { - // @ts-ignore - s3 = peg$currPos - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 58) { - // @ts-ignore - s4 = peg$c4 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s4 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e4) - } - } - // @ts-ignore - if (s4 !== peg$FAILED) { - // @ts-ignore - s5 = peg$parseVersion() - // @ts-ignore - if (s5 !== peg$FAILED) { - // @ts-ignore - s4 = [s4, s5] - // @ts-ignore - s3 = s4 - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s3 - // @ts-ignore - s3 = peg$FAILED - } - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s3 - // @ts-ignore - s3 = peg$FAILED - } - // @ts-ignore - if (s3 === peg$FAILED) { - // @ts-ignore - s3 = null - } - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s0 = peg$f2(s1, s2, s3) - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseFlavorAtom() { - // @ts-ignore - var s0, s1, s2 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 35) { - // @ts-ignore - s1 = peg$c5 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s1 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e5) - } - } - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - s2 = peg$parseFlavorString() - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s0 = peg$f3(s2) - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseNot() { - // @ts-ignore - var s0, s1, s2, s3 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 33) { - // @ts-ignore - s1 = peg$c6 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s1 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e6) - } - } - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - s2 = peg$parse_() - // @ts-ignore - s3 = peg$parseVersionRangeAtom() - // @ts-ignore - if (s3 !== peg$FAILED) { - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s0 = peg$f4(s3) - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseAny() { - // @ts-ignore - var s0, s1 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 42) { - // @ts-ignore - s1 = peg$c7 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s1 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e7) - } - } - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s1 = peg$f5() - } - // @ts-ignore - s0 = s1 - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseNone() { - // @ts-ignore - var s0, s1 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 33) { - // @ts-ignore - s1 = peg$c6 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s1 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e6) - } - } - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s1 = peg$f6() - } - // @ts-ignore - s0 = s1 - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseCmpOp() { - // @ts-ignore - var s0, s1 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - if (input.substr(peg$currPos, 2) === peg$c8) { - // @ts-ignore - s1 = peg$c8 - // @ts-ignore - peg$currPos += 2 - // @ts-ignore - } else { - // @ts-ignore - s1 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e8) - } - } - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s1 = peg$f7() - } - // @ts-ignore - s0 = s1 - // @ts-ignore - if (s0 === peg$FAILED) { - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - if (input.substr(peg$currPos, 2) === peg$c9) { - // @ts-ignore - s1 = peg$c9 - // @ts-ignore - peg$currPos += 2 - // @ts-ignore - } else { - // @ts-ignore - s1 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e9) - } - } - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s1 = peg$f8() - } - // @ts-ignore - s0 = s1 - // @ts-ignore - if (s0 === peg$FAILED) { - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 62) { - // @ts-ignore - s1 = peg$c10 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s1 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e10) - } - } - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s1 = peg$f9() - } - // @ts-ignore - s0 = s1 - // @ts-ignore - if (s0 === peg$FAILED) { - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 60) { - // @ts-ignore - s1 = peg$c11 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s1 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e11) - } - } - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s1 = peg$f10() - } - // @ts-ignore - s0 = s1 - // @ts-ignore - if (s0 === peg$FAILED) { - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 61) { - // @ts-ignore - s1 = peg$c12 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s1 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e12) - } - } - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s1 = peg$f11() - } - // @ts-ignore - s0 = s1 - // @ts-ignore - if (s0 === peg$FAILED) { - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - if (input.substr(peg$currPos, 2) === peg$c13) { - // @ts-ignore - s1 = peg$c13 - // @ts-ignore - peg$currPos += 2 - // @ts-ignore - } else { - // @ts-ignore - s1 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e13) - } - } - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s1 = peg$f12() - } - // @ts-ignore - s0 = s1 - // @ts-ignore - if (s0 === peg$FAILED) { - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 94) { - // @ts-ignore - s1 = peg$c14 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s1 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e14) - } - } - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s1 = peg$f13() - } - // @ts-ignore - s0 = s1 - // @ts-ignore - if (s0 === peg$FAILED) { - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 126) { - // @ts-ignore - s1 = peg$c15 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s1 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e15) - } - } - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s1 = peg$f14() - } - // @ts-ignore - s0 = s1 - } - } - } - } - } - } - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseExtendedVersion() { - // @ts-ignore - var s0, s1, s2, s3, s4 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - s1 = peg$parseFlavor() - // @ts-ignore - if (s1 === peg$FAILED) { - // @ts-ignore - s1 = null - } - // @ts-ignore - s2 = peg$parseVersion() - // @ts-ignore - if (s2 !== peg$FAILED) { - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 58) { - // @ts-ignore - s3 = peg$c4 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s3 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e4) - } - } - // @ts-ignore - if (s3 !== peg$FAILED) { - // @ts-ignore - s4 = peg$parseVersion() - // @ts-ignore - if (s4 !== peg$FAILED) { - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s0 = peg$f15(s1, s2, s4) - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseEmverVersionRange() { - // @ts-ignore - var s0, s1, s2, s3, s4, s5, s6, s7 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - s1 = peg$parseEmverVersionRangeAtom() - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - s2 = [] - // @ts-ignore - s3 = peg$currPos - // @ts-ignore - s4 = peg$parse_() - // @ts-ignore - s5 = peg$currPos - // @ts-ignore - s6 = peg$parseOr() - // @ts-ignore - if (s6 === peg$FAILED) { - // @ts-ignore - s6 = peg$parseAnd() - } - // @ts-ignore - if (s6 !== peg$FAILED) { - // @ts-ignore - s7 = peg$parse_() - // @ts-ignore - s6 = [s6, s7] - // @ts-ignore - s5 = s6 - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s5 - // @ts-ignore - s5 = peg$FAILED - } - // @ts-ignore - if (s5 === peg$FAILED) { - // @ts-ignore - s5 = null - } - // @ts-ignore - s6 = peg$parseEmverVersionRangeAtom() - // @ts-ignore - if (s6 !== peg$FAILED) { - // @ts-ignore - s4 = [s4, s5, s6] - // @ts-ignore - s3 = s4 - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s3 - // @ts-ignore - s3 = peg$FAILED - } - // @ts-ignore - while (s3 !== peg$FAILED) { - // @ts-ignore - s2.push(s3) - // @ts-ignore - s3 = peg$currPos - // @ts-ignore - s4 = peg$parse_() - // @ts-ignore - s5 = peg$currPos - // @ts-ignore - s6 = peg$parseOr() - // @ts-ignore - if (s6 === peg$FAILED) { - // @ts-ignore - s6 = peg$parseAnd() - } - // @ts-ignore - if (s6 !== peg$FAILED) { - // @ts-ignore - s7 = peg$parse_() - // @ts-ignore - s6 = [s6, s7] - // @ts-ignore - s5 = s6 - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s5 - // @ts-ignore - s5 = peg$FAILED - } - // @ts-ignore - if (s5 === peg$FAILED) { - // @ts-ignore - s5 = null - } - // @ts-ignore - s6 = peg$parseEmverVersionRangeAtom() - // @ts-ignore - if (s6 !== peg$FAILED) { - // @ts-ignore - s4 = [s4, s5, s6] - // @ts-ignore - s3 = s4 - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s3 - // @ts-ignore - s3 = peg$FAILED - } - } - // @ts-ignore - s1 = [s1, s2] - // @ts-ignore - s0 = s1 - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseEmverVersionRangeAtom() { - // @ts-ignore - var s0 - - // @ts-ignore - s0 = peg$parseEmverParens() - // @ts-ignore - if (s0 === peg$FAILED) { - // @ts-ignore - s0 = peg$parseEmverAnchor() - // @ts-ignore - if (s0 === peg$FAILED) { - // @ts-ignore - s0 = peg$parseEmverNot() - // @ts-ignore - if (s0 === peg$FAILED) { - // @ts-ignore - s0 = peg$parseAny() - // @ts-ignore - if (s0 === peg$FAILED) { - // @ts-ignore - s0 = peg$parseNone() - } - } - } - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseEmverParens() { - // @ts-ignore - var s0, s1, s2, s3, s4, s5 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 40) { - // @ts-ignore - s1 = peg$c2 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s1 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e2) - } - } - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - s2 = peg$parse_() - // @ts-ignore - s3 = peg$parseEmverVersionRange() - // @ts-ignore - if (s3 !== peg$FAILED) { - // @ts-ignore - s4 = peg$parse_() - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 41) { - // @ts-ignore - s5 = peg$c3 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s5 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e3) - } - } - // @ts-ignore - if (s5 !== peg$FAILED) { - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s0 = peg$f16(s3) - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseEmverAnchor() { - // @ts-ignore - var s0, s1, s2, s3 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - s1 = peg$parseCmpOp() - // @ts-ignore - if (s1 === peg$FAILED) { - // @ts-ignore - s1 = null - } - // @ts-ignore - s2 = peg$parse_() - // @ts-ignore - s3 = peg$parseEmver() - // @ts-ignore - if (s3 !== peg$FAILED) { - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s0 = peg$f17(s1, s3) - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseEmverNot() { - // @ts-ignore - var s0, s1, s2, s3 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 33) { - // @ts-ignore - s1 = peg$c6 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s1 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e6) - } - } - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - s2 = peg$parse_() - // @ts-ignore - s3 = peg$parseEmverVersionRangeAtom() - // @ts-ignore - if (s3 !== peg$FAILED) { - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s0 = peg$f18(s3) - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseEmver() { - // @ts-ignore - var s0, s1, s2, s3, s4, s5, s6, s7, s8 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - s1 = peg$parseDigit() - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 46) { - // @ts-ignore - s2 = peg$c16 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s2 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e16) - } - } - // @ts-ignore - if (s2 !== peg$FAILED) { - // @ts-ignore - s3 = peg$parseDigit() - // @ts-ignore - if (s3 !== peg$FAILED) { - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 46) { - // @ts-ignore - s4 = peg$c16 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s4 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e16) - } - } - // @ts-ignore - if (s4 !== peg$FAILED) { - // @ts-ignore - s5 = peg$parseDigit() - // @ts-ignore - if (s5 !== peg$FAILED) { - // @ts-ignore - s6 = peg$currPos - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 46) { - // @ts-ignore - s7 = peg$c16 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s7 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e16) - } - } - // @ts-ignore - if (s7 !== peg$FAILED) { - // @ts-ignore - s8 = peg$parseDigit() - // @ts-ignore - if (s8 !== peg$FAILED) { - // @ts-ignore - peg$savedPos = s6 - // @ts-ignore - s6 = peg$f19(s1, s3, s5, s8) - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s6 - // @ts-ignore - s6 = peg$FAILED - } - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s6 - // @ts-ignore - s6 = peg$FAILED - } - // @ts-ignore - if (s6 === peg$FAILED) { - // @ts-ignore - s6 = null - } - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s0 = peg$f20(s1, s3, s5, s6) - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseFlavor() { - // @ts-ignore - var s0, s1, s2, s3 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 35) { - // @ts-ignore - s1 = peg$c5 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s1 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e5) - } - } - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - s2 = peg$parseFlavorString() - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 58) { - // @ts-ignore - s3 = peg$c4 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s3 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e4) - } - } - // @ts-ignore - if (s3 !== peg$FAILED) { - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s0 = peg$f21(s2) - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseFlavorString() { - // @ts-ignore - var s0, s1, s2 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - s1 = [] - // @ts-ignore - if (peg$r0.test(input.charAt(peg$currPos))) { - // @ts-ignore - s2 = input.charAt(peg$currPos) - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s2 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e17) - } - } - // @ts-ignore - while (s2 !== peg$FAILED) { - // @ts-ignore - s1.push(s2) - // @ts-ignore - if (peg$r0.test(input.charAt(peg$currPos))) { - // @ts-ignore - s2 = input.charAt(peg$currPos) - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s2 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e17) - } - } - } - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s1 = peg$f22() - // @ts-ignore - s0 = s1 - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseString() { - // @ts-ignore - var s0, s1, s2 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - s1 = [] - // @ts-ignore - if (peg$r1.test(input.charAt(peg$currPos))) { - // @ts-ignore - s2 = input.charAt(peg$currPos) - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s2 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e18) - } - } - // @ts-ignore - if (s2 !== peg$FAILED) { - // @ts-ignore - while (s2 !== peg$FAILED) { - // @ts-ignore - s1.push(s2) - // @ts-ignore - if (peg$r1.test(input.charAt(peg$currPos))) { - // @ts-ignore - s2 = input.charAt(peg$currPos) - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s2 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e18) - } - } - } - // @ts-ignore - } else { - // @ts-ignore - s1 = peg$FAILED - } - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s1 = peg$f23() - } - // @ts-ignore - s0 = s1 - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseVersion() { - // @ts-ignore - var s0, s1, s2 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - s1 = peg$parseVersionNumber() - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - s2 = peg$parsePreRelease() - // @ts-ignore - if (s2 === peg$FAILED) { - // @ts-ignore - s2 = null - } - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s0 = peg$f24(s1, s2) - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parsePreRelease() { - // @ts-ignore - var s0, s1, s2, s3, s4, s5, s6 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 45) { - // @ts-ignore - s1 = peg$c17 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s1 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e19) - } - } - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - s2 = peg$parsePreReleaseSegment() - // @ts-ignore - if (s2 !== peg$FAILED) { - // @ts-ignore - s3 = [] - // @ts-ignore - s4 = peg$currPos - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 46) { - // @ts-ignore - s5 = peg$c16 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s5 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e16) - } - } - // @ts-ignore - if (s5 !== peg$FAILED) { - // @ts-ignore - s6 = peg$parsePreReleaseSegment() - // @ts-ignore - if (s6 !== peg$FAILED) { - // @ts-ignore - s5 = [s5, s6] - // @ts-ignore - s4 = s5 - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s4 - // @ts-ignore - s4 = peg$FAILED - } - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s4 - // @ts-ignore - s4 = peg$FAILED - } - // @ts-ignore - while (s4 !== peg$FAILED) { - // @ts-ignore - s3.push(s4) - // @ts-ignore - s4 = peg$currPos - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 46) { - // @ts-ignore - s5 = peg$c16 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s5 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e16) - } - } - // @ts-ignore - if (s5 !== peg$FAILED) { - // @ts-ignore - s6 = peg$parsePreReleaseSegment() - // @ts-ignore - if (s6 !== peg$FAILED) { - // @ts-ignore - s5 = [s5, s6] - // @ts-ignore - s4 = s5 - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s4 - // @ts-ignore - s4 = peg$FAILED - } - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s4 - // @ts-ignore - s4 = peg$FAILED - } - } - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s0 = peg$f25(s2, s3) - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parsePreReleaseSegment() { - // @ts-ignore - var s0, s1, s2 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 46) { - // @ts-ignore - s1 = peg$c16 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s1 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e16) - } - } - // @ts-ignore - if (s1 === peg$FAILED) { - // @ts-ignore - s1 = null - } - // @ts-ignore - s2 = peg$parseDigit() - // @ts-ignore - if (s2 === peg$FAILED) { - // @ts-ignore - s2 = peg$parseString() - } - // @ts-ignore - if (s2 !== peg$FAILED) { - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s0 = peg$f26(s2) - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseVersionNumber() { - // @ts-ignore - var s0, s1, s2, s3, s4, s5 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - s1 = peg$parseDigit() - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - s2 = [] - // @ts-ignore - s3 = peg$currPos - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 46) { - // @ts-ignore - s4 = peg$c16 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s4 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e16) - } - } - // @ts-ignore - if (s4 !== peg$FAILED) { - // @ts-ignore - s5 = peg$parseDigit() - // @ts-ignore - if (s5 !== peg$FAILED) { - // @ts-ignore - s4 = [s4, s5] - // @ts-ignore - s3 = s4 - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s3 - // @ts-ignore - s3 = peg$FAILED - } - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s3 - // @ts-ignore - s3 = peg$FAILED - } - // @ts-ignore - while (s3 !== peg$FAILED) { - // @ts-ignore - s2.push(s3) - // @ts-ignore - s3 = peg$currPos - // @ts-ignore - if (input.charCodeAt(peg$currPos) === 46) { - // @ts-ignore - s4 = peg$c16 - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s4 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e16) - } - } - // @ts-ignore - if (s4 !== peg$FAILED) { - // @ts-ignore - s5 = peg$parseDigit() - // @ts-ignore - if (s5 !== peg$FAILED) { - // @ts-ignore - s4 = [s4, s5] - // @ts-ignore - s3 = s4 - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s3 - // @ts-ignore - s3 = peg$FAILED - } - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s3 - // @ts-ignore - s3 = peg$FAILED - } - } - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s0 = peg$f27(s1, s2) - // @ts-ignore - } else { - // @ts-ignore - peg$currPos = s0 - // @ts-ignore - s0 = peg$FAILED - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parseDigit() { - // @ts-ignore - var s0, s1, s2 - - // @ts-ignore - s0 = peg$currPos - // @ts-ignore - s1 = [] - // @ts-ignore - if (peg$r2.test(input.charAt(peg$currPos))) { - // @ts-ignore - s2 = input.charAt(peg$currPos) - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s2 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e20) - } - } - // @ts-ignore - if (s2 !== peg$FAILED) { - // @ts-ignore - while (s2 !== peg$FAILED) { - // @ts-ignore - s1.push(s2) - // @ts-ignore - if (peg$r2.test(input.charAt(peg$currPos))) { - // @ts-ignore - s2 = input.charAt(peg$currPos) - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s2 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e20) - } - } - } - // @ts-ignore - } else { - // @ts-ignore - s1 = peg$FAILED - } - // @ts-ignore - if (s1 !== peg$FAILED) { - // @ts-ignore - peg$savedPos = s0 - // @ts-ignore - s1 = peg$f28() - } - // @ts-ignore - s0 = s1 - - // @ts-ignore - return s0 - } - - // @ts-ignore - function // @ts-ignore - peg$parse_() { - // @ts-ignore - var s0, s1 - - // @ts-ignore - peg$silentFails++ - // @ts-ignore - s0 = [] - // @ts-ignore - if (peg$r3.test(input.charAt(peg$currPos))) { - // @ts-ignore - s1 = input.charAt(peg$currPos) - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s1 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e22) - } - } - // @ts-ignore - while (s1 !== peg$FAILED) { - // @ts-ignore - s0.push(s1) - // @ts-ignore - if (peg$r3.test(input.charAt(peg$currPos))) { - // @ts-ignore - s1 = input.charAt(peg$currPos) - // @ts-ignore - peg$currPos++ - // @ts-ignore - } else { - // @ts-ignore - s1 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e22) - } - } - } - // @ts-ignore - peg$silentFails-- - // @ts-ignore - s1 = peg$FAILED - // @ts-ignore - if (peg$silentFails === 0) { - peg$fail(peg$e21) - } - - // @ts-ignore - return s0 - } - - // @ts-ignore - peg$result = peg$startRuleFunction() - - // @ts-ignore - if (peg$result !== peg$FAILED && peg$currPos === input.length) { - // @ts-ignore - return peg$result - // @ts-ignore - } else { - // @ts-ignore - if (peg$result !== peg$FAILED && peg$currPos < input.length) { - // @ts-ignore - peg$fail(peg$endExpectation()) - } - - // @ts-ignore - throw peg$buildStructuredError( - // @ts-ignore - peg$maxFailExpected, - // @ts-ignore - peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null, - // @ts-ignore - peg$maxFailPos < input.length - ? // @ts-ignore - peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1) - : // @ts-ignore - peg$computeLocation(peg$maxFailPos, peg$maxFailPos), - ) +const peggyParser: {parse: any, SyntaxError: any, DefaultTracer?: any} = // Generated by Peggy 3.0.2. +// +// https://peggyjs.org/ +// @ts-ignore +(function() { +// @ts-ignore + "use strict"; + +// @ts-ignore +function peg$subclass(child, parent) { +// @ts-ignore + function C() { this.constructor = child; } +// @ts-ignore + C.prototype = parent.prototype; +// @ts-ignore + child.prototype = new C(); +} + +// @ts-ignore +function peg$SyntaxError(message, expected, found, location) { +// @ts-ignore + var self = Error.call(this, message); + // istanbul ignore next Check is a necessary evil to support older environments +// @ts-ignore + if (Object.setPrototypeOf) { +// @ts-ignore + Object.setPrototypeOf(self, peg$SyntaxError.prototype); + } +// @ts-ignore + self.expected = expected; +// @ts-ignore + self.found = found; +// @ts-ignore + self.location = location; +// @ts-ignore + self.name = "SyntaxError"; +// @ts-ignore + return self; +} + +// @ts-ignore +peg$subclass(peg$SyntaxError, Error); + +// @ts-ignore +function peg$padEnd(str, targetLength, padString) { +// @ts-ignore + padString = padString || " "; +// @ts-ignore + if (str.length > targetLength) { return str; } +// @ts-ignore + targetLength -= str.length; +// @ts-ignore + padString += padString.repeat(targetLength); +// @ts-ignore + return str + padString.slice(0, targetLength); +} + +// @ts-ignore +peg$SyntaxError.prototype.format = function(sources) { +// @ts-ignore + var str = "Error: " + this.message; +// @ts-ignore + if (this.location) { +// @ts-ignore + var src = null; +// @ts-ignore + var k; +// @ts-ignore + for (k = 0; k < sources.length; k++) { +// @ts-ignore + if (sources[k].source === this.location.source) { +// @ts-ignore + src = sources[k].text.split(/\r\n|\n|\r/g); +// @ts-ignore + break; } } +// @ts-ignore + var s = this.location.start; +// @ts-ignore + var offset_s = (this.location.source && (typeof this.location.source.offset === "function")) +// @ts-ignore + ? this.location.source.offset(s) +// @ts-ignore + : s; +// @ts-ignore + var loc = this.location.source + ":" + offset_s.line + ":" + offset_s.column; +// @ts-ignore + if (src) { +// @ts-ignore + var e = this.location.end; +// @ts-ignore + var filler = peg$padEnd("", offset_s.line.toString().length, ' '); +// @ts-ignore + var line = src[s.line - 1]; +// @ts-ignore + var last = s.line === e.line ? e.column : line.length + 1; +// @ts-ignore + var hatLen = (last - s.column) || 1; +// @ts-ignore + str += "\n --> " + loc + "\n" +// @ts-ignore + + filler + " |\n" +// @ts-ignore + + offset_s.line + " | " + line + "\n" +// @ts-ignore + + filler + " | " + peg$padEnd("", s.column - 1, ' ') +// @ts-ignore + + peg$padEnd("", hatLen, "^"); +// @ts-ignore + } else { +// @ts-ignore + str += "\n at " + loc; + } + } +// @ts-ignore + return str; +}; - // @ts-ignore +// @ts-ignore +peg$SyntaxError.buildMessage = function(expected, found) { +// @ts-ignore + var DESCRIBE_EXPECTATION_FNS = { +// @ts-ignore + literal: function(expectation) { +// @ts-ignore + return "\"" + literalEscape(expectation.text) + "\""; + }, + +// @ts-ignore + class: function(expectation) { +// @ts-ignore + var escapedParts = expectation.parts.map(function(part) { +// @ts-ignore + return Array.isArray(part) +// @ts-ignore + ? classEscape(part[0]) + "-" + classEscape(part[1]) +// @ts-ignore + : classEscape(part); + }); + +// @ts-ignore + return "[" + (expectation.inverted ? "^" : "") + escapedParts.join("") + "]"; + }, + +// @ts-ignore + any: function() { +// @ts-ignore + return "any character"; + }, + +// @ts-ignore + end: function() { +// @ts-ignore + return "end of input"; + }, + +// @ts-ignore + other: function(expectation) { +// @ts-ignore + return expectation.description; + } + }; + +// @ts-ignore + function hex(ch) { +// @ts-ignore + return ch.charCodeAt(0).toString(16).toUpperCase(); + } + +// @ts-ignore + function literalEscape(s) { +// @ts-ignore + return s +// @ts-ignore + .replace(/\\/g, "\\\\") +// @ts-ignore + .replace(/"/g, "\\\"") +// @ts-ignore + .replace(/\0/g, "\\0") +// @ts-ignore + .replace(/\t/g, "\\t") +// @ts-ignore + .replace(/\n/g, "\\n") +// @ts-ignore + .replace(/\r/g, "\\r") +// @ts-ignore + .replace(/[\x00-\x0F]/g, function(ch) { return "\\x0" + hex(ch); }) +// @ts-ignore + .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return "\\x" + hex(ch); }); + } + +// @ts-ignore + function classEscape(s) { +// @ts-ignore + return s +// @ts-ignore + .replace(/\\/g, "\\\\") +// @ts-ignore + .replace(/\]/g, "\\]") +// @ts-ignore + .replace(/\^/g, "\\^") +// @ts-ignore + .replace(/-/g, "\\-") +// @ts-ignore + .replace(/\0/g, "\\0") +// @ts-ignore + .replace(/\t/g, "\\t") +// @ts-ignore + .replace(/\n/g, "\\n") +// @ts-ignore + .replace(/\r/g, "\\r") +// @ts-ignore + .replace(/[\x00-\x0F]/g, function(ch) { return "\\x0" + hex(ch); }) +// @ts-ignore + .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return "\\x" + hex(ch); }); + } + +// @ts-ignore + function describeExpectation(expectation) { +// @ts-ignore + return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation); + } + +// @ts-ignore + function describeExpected(expected) { +// @ts-ignore + var descriptions = expected.map(describeExpectation); +// @ts-ignore + var i, j; + +// @ts-ignore + descriptions.sort(); + +// @ts-ignore + if (descriptions.length > 0) { +// @ts-ignore + for (i = 1, j = 1; i < descriptions.length; i++) { +// @ts-ignore + if (descriptions[i - 1] !== descriptions[i]) { +// @ts-ignore + descriptions[j] = descriptions[i]; +// @ts-ignore + j++; + } + } +// @ts-ignore + descriptions.length = j; + } + +// @ts-ignore + switch (descriptions.length) { +// @ts-ignore + case 1: +// @ts-ignore + return descriptions[0]; + +// @ts-ignore + case 2: +// @ts-ignore + return descriptions[0] + " or " + descriptions[1]; + +// @ts-ignore + default: +// @ts-ignore + return descriptions.slice(0, -1).join(", ") +// @ts-ignore + + ", or " +// @ts-ignore + + descriptions[descriptions.length - 1]; + } + } + +// @ts-ignore + function describeFound(found) { +// @ts-ignore + return found ? "\"" + literalEscape(found) + "\"" : "end of input"; + } + +// @ts-ignore + return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found."; +}; + +// @ts-ignore +function peg$parse(input, options) { +// @ts-ignore + options = options !== undefined ? options : {}; + +// @ts-ignore + var peg$FAILED = {}; +// @ts-ignore + var peg$source = options.grammarSource; + +// @ts-ignore + var peg$startRuleFunctions = { VersionRange: peg$parseVersionRange, Or: peg$parseOr, And: peg$parseAnd, VersionRangeAtom: peg$parseVersionRangeAtom, Parens: peg$parseParens, Anchor: peg$parseAnchor, VersionSpec: peg$parseVersionSpec, FlavorAtom: peg$parseFlavorAtom, Not: peg$parseNot, Any: peg$parseAny, None: peg$parseNone, CmpOp: peg$parseCmpOp, ExtendedVersion: peg$parseExtendedVersion, EmverVersionRange: peg$parseEmverVersionRange, EmverVersionRangeAtom: peg$parseEmverVersionRangeAtom, EmverParens: peg$parseEmverParens, EmverAnchor: peg$parseEmverAnchor, EmverNot: peg$parseEmverNot, Emver: peg$parseEmver, Flavor: peg$parseFlavor, FlavorString: peg$parseFlavorString, String: peg$parseString, Version: peg$parseVersion, PreRelease: peg$parsePreRelease, PreReleaseSegment: peg$parsePreReleaseSegment, VersionNumber: peg$parseVersionNumber, Digit: peg$parseDigit, _: peg$parse_ }; +// @ts-ignore + var peg$startRuleFunction = peg$parseVersionRange; + +// @ts-ignore + var peg$c0 = "||"; + var peg$c1 = "&&"; + var peg$c2 = "("; + var peg$c3 = ")"; + var peg$c4 = ":"; + var peg$c5 = "#"; + var peg$c6 = "!"; + var peg$c7 = "*"; + var peg$c8 = ">="; + var peg$c9 = "<="; + var peg$c10 = ">"; + var peg$c11 = "<"; + var peg$c12 = "="; + var peg$c13 = "!="; + var peg$c14 = "^"; + var peg$c15 = "~"; + var peg$c16 = "."; + var peg$c17 = "-"; + + var peg$r0 = /^[a-z]/; + var peg$r1 = /^[a-zA-Z]/; + var peg$r2 = /^[0-9]/; + var peg$r3 = /^[ \t\n\r]/; + + var peg$e0 = peg$literalExpectation("||", false); + var peg$e1 = peg$literalExpectation("&&", false); + var peg$e2 = peg$literalExpectation("(", false); + var peg$e3 = peg$literalExpectation(")", false); + var peg$e4 = peg$literalExpectation(":", false); + var peg$e5 = peg$literalExpectation("#", false); + var peg$e6 = peg$literalExpectation("!", false); + var peg$e7 = peg$literalExpectation("*", false); + var peg$e8 = peg$literalExpectation(">=", false); + var peg$e9 = peg$literalExpectation("<=", false); + var peg$e10 = peg$literalExpectation(">", false); + var peg$e11 = peg$literalExpectation("<", false); + var peg$e12 = peg$literalExpectation("=", false); + var peg$e13 = peg$literalExpectation("!=", false); + var peg$e14 = peg$literalExpectation("^", false); + var peg$e15 = peg$literalExpectation("~", false); + var peg$e16 = peg$literalExpectation(".", false); + var peg$e17 = peg$classExpectation([["a", "z"]], false, false); + var peg$e18 = peg$classExpectation([["a", "z"], ["A", "Z"]], false, false); + var peg$e19 = peg$literalExpectation("-", false); + var peg$e20 = peg$classExpectation([["0", "9"]], false, false); + var peg$e21 = peg$otherExpectation("whitespace"); + var peg$e22 = peg$classExpectation([" ", "\t", "\n", "\r"], false, false); +// @ts-ignore + + var peg$f0 = function(expr) {// @ts-ignore + return { type: "Parens", expr } };// @ts-ignore + + var peg$f1 = function(operator, version) {// @ts-ignore + return { type: "Anchor", operator, version } };// @ts-ignore + + var peg$f2 = function(flavor, upstream, downstream) {// @ts-ignore + return { flavor: flavor || null, upstream, downstream: downstream ? downstream[1] : { number: [0], prerelease: [] } } };// @ts-ignore + + var peg$f3 = function(flavor) {// @ts-ignore + return { type: "Flavor", flavor: flavor } };// @ts-ignore + + var peg$f4 = function(value) {// @ts-ignore + return { type: "Not", value: value }};// @ts-ignore + + var peg$f5 = function() {// @ts-ignore + return { type: "Any" } };// @ts-ignore + + var peg$f6 = function() {// @ts-ignore + return { type: "None" } };// @ts-ignore + + var peg$f7 = function() {// @ts-ignore + return ">="; };// @ts-ignore + + var peg$f8 = function() {// @ts-ignore + return "<="; };// @ts-ignore + + var peg$f9 = function() {// @ts-ignore + return ">"; };// @ts-ignore + + var peg$f10 = function() {// @ts-ignore + return "<"; };// @ts-ignore + + var peg$f11 = function() {// @ts-ignore + return "="; };// @ts-ignore + + var peg$f12 = function() {// @ts-ignore + return "!="; };// @ts-ignore + + var peg$f13 = function() {// @ts-ignore + return "^"; };// @ts-ignore + + var peg$f14 = function() {// @ts-ignore + return "~"; };// @ts-ignore + + var peg$f15 = function(flavor, upstream, downstream) { +// @ts-ignore + return { flavor: flavor || null, upstream, downstream } + };// @ts-ignore + + var peg$f16 = function(expr) {// @ts-ignore + return { type: "Parens", expr } };// @ts-ignore + + var peg$f17 = function(operator, version) {// @ts-ignore + return { type: "Anchor", operator, version } };// @ts-ignore + + var peg$f18 = function(value) {// @ts-ignore + return { type: "Not", value: value }};// @ts-ignore + + var peg$f19 = function(major, minor, patch, revision) {// @ts-ignore + return revision };// @ts-ignore + + var peg$f20 = function(major, minor, patch, revision) { +// @ts-ignore return { - SyntaxError: peg$SyntaxError, - parse: peg$parse, +// @ts-ignore + flavor: null, +// @ts-ignore + upstream: { +// @ts-ignore + number: [major, minor, patch], +// @ts-ignore + prerelease: [], + }, +// @ts-ignore + downstream: { +// @ts-ignore + number: [revision || 0], +// @ts-ignore + prerelease: [], + }, } - })() + };// @ts-ignore + + var peg$f21 = function(flavor) {// @ts-ignore + return flavor };// @ts-ignore + + var peg$f22 = function() {// @ts-ignore + return text() };// @ts-ignore + + var peg$f23 = function() {// @ts-ignore + return text(); };// @ts-ignore + + var peg$f24 = function(number, prerelease) { +// @ts-ignore + return { +// @ts-ignore + number, +// @ts-ignore + prerelease: prerelease || [] + }; + };// @ts-ignore + + var peg$f25 = function(first, rest) { +// @ts-ignore + return [first].concat(rest.map(r => r[1])); + };// @ts-ignore + + var peg$f26 = function(segment) { +// @ts-ignore + return segment; + };// @ts-ignore + + var peg$f27 = function(first, rest) { +// @ts-ignore + return [first].concat(rest.map(r => r[1])); + };// @ts-ignore + + var peg$f28 = function() {// @ts-ignore + return parseInt(text(), 10); }; +// @ts-ignore + var peg$currPos = 0; +// @ts-ignore + var peg$savedPos = 0; +// @ts-ignore + var peg$posDetailsCache = [{ line: 1, column: 1 }]; +// @ts-ignore + var peg$maxFailPos = 0; +// @ts-ignore + var peg$maxFailExpected = []; +// @ts-ignore + var peg$silentFails = 0; + +// @ts-ignore + var peg$result; + +// @ts-ignore + if ("startRule" in options) { +// @ts-ignore + if (!(options.startRule in peg$startRuleFunctions)) { +// @ts-ignore + throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); + } + +// @ts-ignore + peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; + } + +// @ts-ignore + function text() { +// @ts-ignore + return input.substring(peg$savedPos, peg$currPos); + } + +// @ts-ignore + function offset() { +// @ts-ignore + return peg$savedPos; + } + +// @ts-ignore + function range() { +// @ts-ignore + return { +// @ts-ignore + source: peg$source, +// @ts-ignore + start: peg$savedPos, +// @ts-ignore + end: peg$currPos + }; + } + +// @ts-ignore + function location() { +// @ts-ignore + return peg$computeLocation(peg$savedPos, peg$currPos); + } + +// @ts-ignore + function expected(description, location) { +// @ts-ignore + location = location !== undefined +// @ts-ignore + ? location +// @ts-ignore + : peg$computeLocation(peg$savedPos, peg$currPos); + +// @ts-ignore + throw peg$buildStructuredError( +// @ts-ignore + [peg$otherExpectation(description)], +// @ts-ignore + input.substring(peg$savedPos, peg$currPos), +// @ts-ignore + location + ); + } + +// @ts-ignore + function error(message, location) { +// @ts-ignore + location = location !== undefined +// @ts-ignore + ? location +// @ts-ignore + : peg$computeLocation(peg$savedPos, peg$currPos); + +// @ts-ignore + throw peg$buildSimpleError(message, location); + } + +// @ts-ignore + function peg$literalExpectation(text, ignoreCase) { +// @ts-ignore + return { type: "literal", text: text, ignoreCase: ignoreCase }; + } + +// @ts-ignore + function peg$classExpectation(parts, inverted, ignoreCase) { +// @ts-ignore + return { type: "class", parts: parts, inverted: inverted, ignoreCase: ignoreCase }; + } + +// @ts-ignore + function peg$anyExpectation() { +// @ts-ignore + return { type: "any" }; + } + +// @ts-ignore + function peg$endExpectation() { +// @ts-ignore + return { type: "end" }; + } + +// @ts-ignore + function peg$otherExpectation(description) { +// @ts-ignore + return { type: "other", description: description }; + } + +// @ts-ignore + function peg$computePosDetails(pos) { +// @ts-ignore + var details = peg$posDetailsCache[pos]; +// @ts-ignore + var p; + +// @ts-ignore + if (details) { +// @ts-ignore + return details; +// @ts-ignore + } else { +// @ts-ignore + p = pos - 1; +// @ts-ignore + while (!peg$posDetailsCache[p]) { +// @ts-ignore + p--; + } + +// @ts-ignore + details = peg$posDetailsCache[p]; +// @ts-ignore + details = { +// @ts-ignore + line: details.line, +// @ts-ignore + column: details.column + }; + +// @ts-ignore + while (p < pos) { +// @ts-ignore + if (input.charCodeAt(p) === 10) { +// @ts-ignore + details.line++; +// @ts-ignore + details.column = 1; +// @ts-ignore + } else { +// @ts-ignore + details.column++; + } + +// @ts-ignore + p++; + } + +// @ts-ignore + peg$posDetailsCache[pos] = details; + +// @ts-ignore + return details; + } + } + +// @ts-ignore + function peg$computeLocation(startPos, endPos, offset) { +// @ts-ignore + var startPosDetails = peg$computePosDetails(startPos); +// @ts-ignore + var endPosDetails = peg$computePosDetails(endPos); + +// @ts-ignore + var res = { +// @ts-ignore + source: peg$source, +// @ts-ignore + start: { +// @ts-ignore + offset: startPos, +// @ts-ignore + line: startPosDetails.line, +// @ts-ignore + column: startPosDetails.column + }, +// @ts-ignore + end: { +// @ts-ignore + offset: endPos, +// @ts-ignore + line: endPosDetails.line, +// @ts-ignore + column: endPosDetails.column + } + }; +// @ts-ignore + if (offset && peg$source && (typeof peg$source.offset === "function")) { +// @ts-ignore + res.start = peg$source.offset(res.start); +// @ts-ignore + res.end = peg$source.offset(res.end); + } +// @ts-ignore + return res; + } + +// @ts-ignore + function peg$fail(expected) { +// @ts-ignore + if (peg$currPos < peg$maxFailPos) { return; } + +// @ts-ignore + if (peg$currPos > peg$maxFailPos) { +// @ts-ignore + peg$maxFailPos = peg$currPos; +// @ts-ignore + peg$maxFailExpected = []; + } + +// @ts-ignore + peg$maxFailExpected.push(expected); + } + +// @ts-ignore + function peg$buildSimpleError(message, location) { +// @ts-ignore + return new peg$SyntaxError(message, null, null, location); + } + +// @ts-ignore + function peg$buildStructuredError(expected, found, location) { +// @ts-ignore + return new peg$SyntaxError( +// @ts-ignore + peg$SyntaxError.buildMessage(expected, found), +// @ts-ignore + expected, +// @ts-ignore + found, +// @ts-ignore + location + ); + } + +// @ts-ignore + function // @ts-ignore +peg$parseVersionRange() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5, s6, s7; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseVersionRangeAtom(); +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = []; +// @ts-ignore + s3 = peg$currPos; +// @ts-ignore + s4 = peg$parse_(); +// @ts-ignore + s5 = peg$currPos; +// @ts-ignore + s6 = peg$parseOr(); +// @ts-ignore + if (s6 === peg$FAILED) { +// @ts-ignore + s6 = peg$parseAnd(); + } +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s7 = peg$parse_(); +// @ts-ignore + s6 = [s6, s7]; +// @ts-ignore + s5 = s6; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s5; +// @ts-ignore + s5 = peg$FAILED; + } +// @ts-ignore + if (s5 === peg$FAILED) { +// @ts-ignore + s5 = null; + } +// @ts-ignore + s6 = peg$parseVersionRangeAtom(); +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s4 = [s4, s5, s6]; +// @ts-ignore + s3 = s4; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + while (s3 !== peg$FAILED) { +// @ts-ignore + s2.push(s3); +// @ts-ignore + s3 = peg$currPos; +// @ts-ignore + s4 = peg$parse_(); +// @ts-ignore + s5 = peg$currPos; +// @ts-ignore + s6 = peg$parseOr(); +// @ts-ignore + if (s6 === peg$FAILED) { +// @ts-ignore + s6 = peg$parseAnd(); + } +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s7 = peg$parse_(); +// @ts-ignore + s6 = [s6, s7]; +// @ts-ignore + s5 = s6; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s5; +// @ts-ignore + s5 = peg$FAILED; + } +// @ts-ignore + if (s5 === peg$FAILED) { +// @ts-ignore + s5 = null; + } +// @ts-ignore + s6 = peg$parseVersionRangeAtom(); +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s4 = [s4, s5, s6]; +// @ts-ignore + s3 = s4; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } + } +// @ts-ignore + s1 = [s1, s2]; +// @ts-ignore + s0 = s1; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseOr() { +// @ts-ignore + var s0; + +// @ts-ignore + if (input.substr(peg$currPos, 2) === peg$c0) { +// @ts-ignore + s0 = peg$c0; +// @ts-ignore + peg$currPos += 2; +// @ts-ignore + } else { +// @ts-ignore + s0 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e0); } + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseAnd() { +// @ts-ignore + var s0; + +// @ts-ignore + if (input.substr(peg$currPos, 2) === peg$c1) { +// @ts-ignore + s0 = peg$c1; +// @ts-ignore + peg$currPos += 2; +// @ts-ignore + } else { +// @ts-ignore + s0 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e1); } + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseVersionRangeAtom() { +// @ts-ignore + var s0; + +// @ts-ignore + s0 = peg$parseParens(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseAnchor(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseNot(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseAny(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseNone(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseFlavorAtom(); + } + } + } + } + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseParens() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 40) { +// @ts-ignore + s1 = peg$c2; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e2); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parse_(); +// @ts-ignore + s3 = peg$parseVersionRange(); +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + s4 = peg$parse_(); +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 41) { +// @ts-ignore + s5 = peg$c3; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s5 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e3); } + } +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f0(s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseAnchor() { +// @ts-ignore + var s0, s1, s2, s3; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseCmpOp(); +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + s1 = null; + } +// @ts-ignore + s2 = peg$parse_(); +// @ts-ignore + s3 = peg$parseVersionSpec(); +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f1(s1, s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseVersionSpec() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseFlavor(); +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + s1 = null; + } +// @ts-ignore + s2 = peg$parseVersion(); +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + s3 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 58) { +// @ts-ignore + s4 = peg$c4; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s4 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e4); } + } +// @ts-ignore + if (s4 !== peg$FAILED) { +// @ts-ignore + s5 = peg$parseVersion(); +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s4 = [s4, s5]; +// @ts-ignore + s3 = s4; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + if (s3 === peg$FAILED) { +// @ts-ignore + s3 = null; + } +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f2(s1, s2, s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseFlavorAtom() { +// @ts-ignore + var s0, s1, s2; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 35) { +// @ts-ignore + s1 = peg$c5; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e5); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parseFlavorString(); +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f3(s2); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseNot() { +// @ts-ignore + var s0, s1, s2, s3; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 33) { +// @ts-ignore + s1 = peg$c6; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e6); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parse_(); +// @ts-ignore + s3 = peg$parseVersionRangeAtom(); +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f4(s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseAny() { +// @ts-ignore + var s0, s1; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 42) { +// @ts-ignore + s1 = peg$c7; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e7); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f5(); + } +// @ts-ignore + s0 = s1; + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseNone() { +// @ts-ignore + var s0, s1; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 33) { +// @ts-ignore + s1 = peg$c6; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e6); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f6(); + } +// @ts-ignore + s0 = s1; + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseCmpOp() { +// @ts-ignore + var s0, s1; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.substr(peg$currPos, 2) === peg$c8) { +// @ts-ignore + s1 = peg$c8; +// @ts-ignore + peg$currPos += 2; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e8); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f7(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.substr(peg$currPos, 2) === peg$c9) { +// @ts-ignore + s1 = peg$c9; +// @ts-ignore + peg$currPos += 2; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e9); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f8(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 62) { +// @ts-ignore + s1 = peg$c10; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e10); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f9(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 60) { +// @ts-ignore + s1 = peg$c11; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e11); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f10(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 61) { +// @ts-ignore + s1 = peg$c12; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e12); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f11(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.substr(peg$currPos, 2) === peg$c13) { +// @ts-ignore + s1 = peg$c13; +// @ts-ignore + peg$currPos += 2; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e13); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f12(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 94) { +// @ts-ignore + s1 = peg$c14; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e14); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f13(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 126) { +// @ts-ignore + s1 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f14(); + } +// @ts-ignore + s0 = s1; + } + } + } + } + } + } + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseExtendedVersion() { +// @ts-ignore + var s0, s1, s2, s3, s4; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseFlavor(); +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + s1 = null; + } +// @ts-ignore + s2 = peg$parseVersion(); +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 58) { +// @ts-ignore + s3 = peg$c4; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s3 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e4); } + } +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + s4 = peg$parseVersion(); +// @ts-ignore + if (s4 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f15(s1, s2, s4); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseEmverVersionRange() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5, s6, s7; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseEmverVersionRangeAtom(); +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = []; +// @ts-ignore + s3 = peg$currPos; +// @ts-ignore + s4 = peg$parse_(); +// @ts-ignore + s5 = peg$currPos; +// @ts-ignore + s6 = peg$parseOr(); +// @ts-ignore + if (s6 === peg$FAILED) { +// @ts-ignore + s6 = peg$parseAnd(); + } +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s7 = peg$parse_(); +// @ts-ignore + s6 = [s6, s7]; +// @ts-ignore + s5 = s6; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s5; +// @ts-ignore + s5 = peg$FAILED; + } +// @ts-ignore + if (s5 === peg$FAILED) { +// @ts-ignore + s5 = null; + } +// @ts-ignore + s6 = peg$parseEmverVersionRangeAtom(); +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s4 = [s4, s5, s6]; +// @ts-ignore + s3 = s4; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + while (s3 !== peg$FAILED) { +// @ts-ignore + s2.push(s3); +// @ts-ignore + s3 = peg$currPos; +// @ts-ignore + s4 = peg$parse_(); +// @ts-ignore + s5 = peg$currPos; +// @ts-ignore + s6 = peg$parseOr(); +// @ts-ignore + if (s6 === peg$FAILED) { +// @ts-ignore + s6 = peg$parseAnd(); + } +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s7 = peg$parse_(); +// @ts-ignore + s6 = [s6, s7]; +// @ts-ignore + s5 = s6; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s5; +// @ts-ignore + s5 = peg$FAILED; + } +// @ts-ignore + if (s5 === peg$FAILED) { +// @ts-ignore + s5 = null; + } +// @ts-ignore + s6 = peg$parseEmverVersionRangeAtom(); +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s4 = [s4, s5, s6]; +// @ts-ignore + s3 = s4; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } + } +// @ts-ignore + s1 = [s1, s2]; +// @ts-ignore + s0 = s1; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseEmverVersionRangeAtom() { +// @ts-ignore + var s0; + +// @ts-ignore + s0 = peg$parseEmverParens(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseEmverAnchor(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseEmverNot(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseAny(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseNone(); + } + } + } + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseEmverParens() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 40) { +// @ts-ignore + s1 = peg$c2; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e2); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parse_(); +// @ts-ignore + s3 = peg$parseEmverVersionRange(); +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + s4 = peg$parse_(); +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 41) { +// @ts-ignore + s5 = peg$c3; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s5 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e3); } + } +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f16(s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseEmverAnchor() { +// @ts-ignore + var s0, s1, s2, s3; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseCmpOp(); +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + s1 = null; + } +// @ts-ignore + s2 = peg$parse_(); +// @ts-ignore + s3 = peg$parseEmver(); +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f17(s1, s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseEmverNot() { +// @ts-ignore + var s0, s1, s2, s3; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 33) { +// @ts-ignore + s1 = peg$c6; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e6); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parse_(); +// @ts-ignore + s3 = peg$parseEmverVersionRangeAtom(); +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f18(s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseEmver() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5, s6, s7, s8; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseDigit(); +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s2 = peg$c16; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e16); } + } +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + s3 = peg$parseDigit(); +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s4 = peg$c16; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s4 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e16); } + } +// @ts-ignore + if (s4 !== peg$FAILED) { +// @ts-ignore + s5 = peg$parseDigit(); +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s6 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s7 = peg$c16; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s7 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e16); } + } +// @ts-ignore + if (s7 !== peg$FAILED) { +// @ts-ignore + s8 = peg$parseDigit(); +// @ts-ignore + if (s8 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s6; +// @ts-ignore + s6 = peg$f19(s1, s3, s5, s8); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s6; +// @ts-ignore + s6 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s6; +// @ts-ignore + s6 = peg$FAILED; + } +// @ts-ignore + if (s6 === peg$FAILED) { +// @ts-ignore + s6 = null; + } +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f20(s1, s3, s5, s6); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseFlavor() { +// @ts-ignore + var s0, s1, s2, s3; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 35) { +// @ts-ignore + s1 = peg$c5; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e5); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parseFlavorString(); +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 58) { +// @ts-ignore + s3 = peg$c4; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s3 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e4); } + } +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f21(s2); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseFlavorString() { +// @ts-ignore + var s0, s1, s2; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = []; +// @ts-ignore + if (peg$r0.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e17); } + } +// @ts-ignore + while (s2 !== peg$FAILED) { +// @ts-ignore + s1.push(s2); +// @ts-ignore + if (peg$r0.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e17); } + } + } +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f22(); +// @ts-ignore + s0 = s1; + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseString() { +// @ts-ignore + var s0, s1, s2; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = []; +// @ts-ignore + if (peg$r1.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e18); } + } +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + while (s2 !== peg$FAILED) { +// @ts-ignore + s1.push(s2); +// @ts-ignore + if (peg$r1.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e18); } + } + } +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f23(); + } +// @ts-ignore + s0 = s1; + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseVersion() { +// @ts-ignore + var s0, s1, s2; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseVersionNumber(); +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parsePreRelease(); +// @ts-ignore + if (s2 === peg$FAILED) { +// @ts-ignore + s2 = null; + } +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f24(s1, s2); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parsePreRelease() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5, s6; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 45) { +// @ts-ignore + s1 = peg$c17; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e19); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parsePreReleaseSegment(); +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + s3 = []; +// @ts-ignore + s4 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s5 = peg$c16; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s5 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e16); } + } +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s6 = peg$parsePreReleaseSegment(); +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s5 = [s5, s6]; +// @ts-ignore + s4 = s5; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s4; +// @ts-ignore + s4 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s4; +// @ts-ignore + s4 = peg$FAILED; + } +// @ts-ignore + while (s4 !== peg$FAILED) { +// @ts-ignore + s3.push(s4); +// @ts-ignore + s4 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s5 = peg$c16; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s5 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e16); } + } +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s6 = peg$parsePreReleaseSegment(); +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s5 = [s5, s6]; +// @ts-ignore + s4 = s5; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s4; +// @ts-ignore + s4 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s4; +// @ts-ignore + s4 = peg$FAILED; + } + } +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f25(s2, s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parsePreReleaseSegment() { +// @ts-ignore + var s0, s1, s2; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s1 = peg$c16; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e16); } + } +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + s1 = null; + } +// @ts-ignore + s2 = peg$parseDigit(); +// @ts-ignore + if (s2 === peg$FAILED) { +// @ts-ignore + s2 = peg$parseString(); + } +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f26(s2); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseVersionNumber() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseDigit(); +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = []; +// @ts-ignore + s3 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s4 = peg$c16; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s4 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e16); } + } +// @ts-ignore + if (s4 !== peg$FAILED) { +// @ts-ignore + s5 = peg$parseDigit(); +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s4 = [s4, s5]; +// @ts-ignore + s3 = s4; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + while (s3 !== peg$FAILED) { +// @ts-ignore + s2.push(s3); +// @ts-ignore + s3 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s4 = peg$c16; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s4 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e16); } + } +// @ts-ignore + if (s4 !== peg$FAILED) { +// @ts-ignore + s5 = peg$parseDigit(); +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s4 = [s4, s5]; +// @ts-ignore + s3 = s4; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } + } +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f27(s1, s2); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseDigit() { +// @ts-ignore + var s0, s1, s2; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = []; +// @ts-ignore + if (peg$r2.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e20); } + } +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + while (s2 !== peg$FAILED) { +// @ts-ignore + s1.push(s2); +// @ts-ignore + if (peg$r2.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e20); } + } + } +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f28(); + } +// @ts-ignore + s0 = s1; + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parse_() { +// @ts-ignore + var s0, s1; + +// @ts-ignore + peg$silentFails++; +// @ts-ignore + s0 = []; +// @ts-ignore + if (peg$r3.test(input.charAt(peg$currPos))) { +// @ts-ignore + s1 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e22); } + } +// @ts-ignore + while (s1 !== peg$FAILED) { +// @ts-ignore + s0.push(s1); +// @ts-ignore + if (peg$r3.test(input.charAt(peg$currPos))) { +// @ts-ignore + s1 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e22); } + } + } +// @ts-ignore + peg$silentFails--; +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e21); } + +// @ts-ignore + return s0; + } + +// @ts-ignore + peg$result = peg$startRuleFunction(); + +// @ts-ignore + if (peg$result !== peg$FAILED && peg$currPos === input.length) { +// @ts-ignore + return peg$result; +// @ts-ignore + } else { +// @ts-ignore + if (peg$result !== peg$FAILED && peg$currPos < input.length) { +// @ts-ignore + peg$fail(peg$endExpectation()); + } + +// @ts-ignore + throw peg$buildStructuredError( +// @ts-ignore + peg$maxFailExpected, +// @ts-ignore + peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null, +// @ts-ignore + peg$maxFailPos < input.length +// @ts-ignore + ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1) +// @ts-ignore + : peg$computeLocation(peg$maxFailPos, peg$maxFailPos) + ); + } +} + +// @ts-ignore + return { + SyntaxError: peg$SyntaxError, + parse: peg$parse + }; +})() export interface FilePosition { - offset: number - line: number - column: number + offset: number; + line: number; + column: number; } export interface FileRange { - start: FilePosition - end: FilePosition - source: string + start: FilePosition; + end: FilePosition; + source: string; } export interface LiteralExpectation { - type: 'literal' - text: string - ignoreCase: boolean + type: "literal"; + text: string; + ignoreCase: boolean; } export interface ClassParts extends Array {} export interface ClassExpectation { - type: 'class' - parts: ClassParts - inverted: boolean - ignoreCase: boolean + type: "class"; + parts: ClassParts; + inverted: boolean; + ignoreCase: boolean; } export interface AnyExpectation { - type: 'any' + type: "any"; } export interface EndExpectation { - type: 'end' + type: "end"; } export interface OtherExpectation { - type: 'other' - description: string + type: "other"; + description: string; } -export type Expectation = - | LiteralExpectation - | ClassExpectation - | AnyExpectation - | EndExpectation - | OtherExpectation +export type Expectation = LiteralExpectation | ClassExpectation | AnyExpectation | EndExpectation | OtherExpectation; declare class _PeggySyntaxError extends Error { - public static buildMessage( - expected: Expectation[], - found: string | null, - ): string - public message: string - public expected: Expectation[] - public found: string | null - public location: FileRange - public name: string - constructor( - message: string, - expected: Expectation[], - found: string | null, - location: FileRange, - ) - format( - sources: { - source?: any - text: string - }[], - ): string + public static buildMessage(expected: Expectation[], found: string | null): string; + public message: string; + public expected: Expectation[]; + public found: string | null; + public location: FileRange; + public name: string; + constructor(message: string, expected: Expectation[], found: string | null, location: FileRange); + format(sources: { + source?: any; + text: string; + }[]): string; } export interface TraceEvent { - type: string - rule: string - result?: any - location: FileRange -} + type: string; + rule: string; + result?: any; + location: FileRange; + } declare class _DefaultTracer { - private indentLevel: number - public trace(event: TraceEvent): void + private indentLevel: number; + public trace(event: TraceEvent): void; } -peggyParser.SyntaxError.prototype.name = 'PeggySyntaxError' +peggyParser.SyntaxError.prototype.name = "PeggySyntaxError"; export interface ParseOptions { - filename?: string - startRule?: - | 'VersionRange' - | 'Or' - | 'And' - | 'VersionRangeAtom' - | 'Parens' - | 'Anchor' - | 'VersionSpec' - | 'FlavorAtom' - | 'Not' - | 'Any' - | 'None' - | 'CmpOp' - | 'ExtendedVersion' - | 'EmverVersionRange' - | 'EmverVersionRangeAtom' - | 'EmverParens' - | 'EmverAnchor' - | 'EmverNot' - | 'Emver' - | 'Flavor' - | 'FlavorString' - | 'String' - | 'Version' - | 'PreRelease' - | 'PreReleaseSegment' - | 'VersionNumber' - | 'Digit' - | '_' - tracer?: any - [key: string]: any + filename?: string; + startRule?: "VersionRange" | "Or" | "And" | "VersionRangeAtom" | "Parens" | "Anchor" | "VersionSpec" | "FlavorAtom" | "Not" | "Any" | "None" | "CmpOp" | "ExtendedVersion" | "EmverVersionRange" | "EmverVersionRangeAtom" | "EmverParens" | "EmverAnchor" | "EmverNot" | "Emver" | "Flavor" | "FlavorString" | "String" | "Version" | "PreRelease" | "PreReleaseSegment" | "VersionNumber" | "Digit" | "_"; + tracer?: any; + [key: string]: any; } export type ParseFunction = ( - input: string, - options?: Options, -) => Options extends { startRule: infer StartRule } - ? StartRule extends 'VersionRange' - ? VersionRange - : StartRule extends 'Or' - ? Or - : StartRule extends 'And' - ? And - : StartRule extends 'VersionRangeAtom' - ? VersionRangeAtom - : StartRule extends 'Parens' - ? Parens - : StartRule extends 'Anchor' - ? Anchor - : StartRule extends 'VersionSpec' - ? VersionSpec - : StartRule extends 'FlavorAtom' - ? FlavorAtom - : StartRule extends 'Not' - ? Not - : StartRule extends 'Any' - ? Any - : StartRule extends 'None' - ? None - : StartRule extends 'CmpOp' - ? CmpOp - : StartRule extends 'ExtendedVersion' - ? ExtendedVersion - : StartRule extends 'EmverVersionRange' - ? EmverVersionRange - : StartRule extends 'EmverVersionRangeAtom' - ? EmverVersionRangeAtom - : StartRule extends 'EmverParens' - ? EmverParens - : StartRule extends 'EmverAnchor' - ? EmverAnchor - : StartRule extends 'EmverNot' - ? EmverNot - : StartRule extends 'Emver' - ? Emver - : StartRule extends 'Flavor' - ? Flavor - : StartRule extends 'FlavorString' - ? FlavorString - : StartRule extends 'String' - ? String_1 - : StartRule extends 'Version' - ? Version - : StartRule extends 'PreRelease' - ? PreRelease - : StartRule extends 'PreReleaseSegment' - ? PreReleaseSegment - : StartRule extends 'VersionNumber' - ? VersionNumber - : StartRule extends 'Digit' - ? Digit - : StartRule extends '_' - ? _ - : VersionRange - : VersionRange -export const parse: ParseFunction = peggyParser.parse + input: string, + options?: Options + ) => Options extends { startRule: infer StartRule } ? + StartRule extends "VersionRange" ? VersionRange : + StartRule extends "Or" ? Or : + StartRule extends "And" ? And : + StartRule extends "VersionRangeAtom" ? VersionRangeAtom : + StartRule extends "Parens" ? Parens : + StartRule extends "Anchor" ? Anchor : + StartRule extends "VersionSpec" ? VersionSpec : + StartRule extends "FlavorAtom" ? FlavorAtom : + StartRule extends "Not" ? Not : + StartRule extends "Any" ? Any : + StartRule extends "None" ? None : + StartRule extends "CmpOp" ? CmpOp : + StartRule extends "ExtendedVersion" ? ExtendedVersion : + StartRule extends "EmverVersionRange" ? EmverVersionRange : + StartRule extends "EmverVersionRangeAtom" ? EmverVersionRangeAtom : + StartRule extends "EmverParens" ? EmverParens : + StartRule extends "EmverAnchor" ? EmverAnchor : + StartRule extends "EmverNot" ? EmverNot : + StartRule extends "Emver" ? Emver : + StartRule extends "Flavor" ? Flavor : + StartRule extends "FlavorString" ? FlavorString : + StartRule extends "String" ? String_1 : + StartRule extends "Version" ? Version : + StartRule extends "PreRelease" ? PreRelease : + StartRule extends "PreReleaseSegment" ? PreReleaseSegment : + StartRule extends "VersionNumber" ? VersionNumber : + StartRule extends "Digit" ? Digit : + StartRule extends "_" ? _ : VersionRange + : VersionRange; +export const parse: ParseFunction = peggyParser.parse; -export const PeggySyntaxError = - peggyParser.SyntaxError as typeof _PeggySyntaxError +export const PeggySyntaxError = peggyParser.SyntaxError as typeof _PeggySyntaxError; -export type PeggySyntaxError = _PeggySyntaxError +export type PeggySyntaxError = _PeggySyntaxError; // These types were autogenerated by ts-pegjs export type VersionRange = [ VersionRangeAtom, - [_, [Or | And, _] | null, VersionRangeAtom][], -] -export type Or = '||' -export type And = '&&' -export type VersionRangeAtom = Parens | Anchor | Not | Any | None | FlavorAtom -export type Parens = { type: 'Parens'; expr: VersionRange } + [_, [Or | And, _] | null, VersionRangeAtom][] +]; +export type Or = "||"; +export type And = "&&"; +export type VersionRangeAtom = Parens | Anchor | Not | Any | None | FlavorAtom; +export type Parens = { type: "Parens"; expr: VersionRange }; export type Anchor = { - type: 'Anchor' - operator: CmpOp | null - version: VersionSpec -} + type: "Anchor"; + operator: CmpOp | null; + version: VersionSpec; +}; export type VersionSpec = { - flavor: NonNullable | null - upstream: Version - downstream: any -} -export type FlavorAtom = { type: 'Flavor'; flavor: FlavorString } -export type Not = { type: 'Not'; value: VersionRangeAtom } -export type Any = { type: 'Any' } -export type None = { type: 'None' } -export type CmpOp = '>=' | '<=' | '>' | '<' | '=' | '!=' | '^' | '~' + flavor: NonNullable | null; + upstream: Version; + downstream: any; +}; +export type FlavorAtom = { type: "Flavor"; flavor: FlavorString }; +export type Not = { type: "Not"; value: VersionRangeAtom }; +export type Any = { type: "Any" }; +export type None = { type: "None" }; +export type CmpOp = ">=" | "<=" | ">" | "<" | "=" | "!=" | "^" | "~"; export type ExtendedVersion = { - flavor: NonNullable | null - upstream: Version - downstream: Version -} + flavor: NonNullable | null; + upstream: Version; + downstream: Version; +}; export type EmverVersionRange = [ EmverVersionRangeAtom, - [_, [Or | And, _] | null, EmverVersionRangeAtom][], -] + [_, [Or | And, _] | null, EmverVersionRangeAtom][] +]; export type EmverVersionRangeAtom = | EmverParens | EmverAnchor | EmverNot | Any - | None -export type EmverParens = { type: 'Parens'; expr: EmverVersionRange } + | None; +export type EmverParens = { type: "Parens"; expr: EmverVersionRange }; export type EmverAnchor = { - type: 'Anchor' - operator: CmpOp | null - version: Emver -} -export type EmverNot = { type: 'Not'; value: EmverVersionRangeAtom } + type: "Anchor"; + operator: CmpOp | null; + version: Emver; +}; +export type EmverNot = { type: "Not"; value: EmverVersionRangeAtom }; export type Emver = { - flavor: null - upstream: { number: [Digit, Digit, Digit]; prerelease: [] } - downstream: { number: [0 | NonNullable]; prerelease: [] } -} -export type Flavor = FlavorString -export type FlavorString = string -export type String_1 = string + flavor: null; + upstream: { number: [Digit, Digit, Digit]; prerelease: [] }; + downstream: { number: [0 | NonNullable]; prerelease: [] }; +}; +export type Flavor = FlavorString; +export type FlavorString = string; +export type String_1 = string; export type Version = { - number: VersionNumber - prerelease: never[] | NonNullable -} -export type PreRelease = PreReleaseSegment[] -export type PreReleaseSegment = Digit | String_1 -export type VersionNumber = Digit[] -export type Digit = number -export type _ = string[] + number: VersionNumber; + prerelease: never[] | NonNullable; +}; +export type PreRelease = PreReleaseSegment[]; +export type PreReleaseSegment = Digit | String_1; +export type VersionNumber = Digit[]; +export type Digit = number; +export type _ = string[]; diff --git a/sdk/base/lib/osBindings/ErrorData.ts b/sdk/base/lib/osBindings/ErrorData.ts index 3485b2f8b..fc2fa1e9a 100644 --- a/sdk/base/lib/osBindings/ErrorData.ts +++ b/sdk/base/lib/osBindings/ErrorData.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ErrorData = { details: string; debug: string } +export type ErrorData = { details: string; debug: string; info: unknown } diff --git a/sdk/base/lib/osBindings/Host.ts b/sdk/base/lib/osBindings/Host.ts index e34af2ae8..1cbe0b84e 100644 --- a/sdk/base/lib/osBindings/Host.ts +++ b/sdk/base/lib/osBindings/Host.ts @@ -5,7 +5,6 @@ import type { PublicDomainConfig } from './PublicDomainConfig' export type Host = { bindings: { [key: number]: BindInfo } - onions: string[] publicDomains: { [key: string]: PublicDomainConfig } privateDomains: Array /** diff --git a/sdk/base/lib/osBindings/HostnameInfo.ts b/sdk/base/lib/osBindings/HostnameInfo.ts index f2bb5e226..5865c6381 100644 --- a/sdk/base/lib/osBindings/HostnameInfo.ts +++ b/sdk/base/lib/osBindings/HostnameInfo.ts @@ -1,8 +1,10 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { GatewayInfo } from './GatewayInfo' import type { IpHostname } from './IpHostname' -import type { OnionHostname } from './OnionHostname' -export type HostnameInfo = - | { kind: 'ip'; gateway: GatewayInfo; public: boolean; hostname: IpHostname } - | { kind: 'onion'; hostname: OnionHostname } +export type HostnameInfo = { + kind: 'ip' + gateway: GatewayInfo + public: boolean + hostname: IpHostname +} diff --git a/sdk/base/lib/osBindings/OnionHostname.ts b/sdk/base/lib/osBindings/OnionHostname.ts deleted file mode 100644 index 0bea8245e..000000000 --- a/sdk/base/lib/osBindings/OnionHostname.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type OnionHostname = { - value: string - port: number | null - sslPort: number | null -} diff --git a/sdk/base/lib/osBindings/index.ts b/sdk/base/lib/osBindings/index.ts index 4d9049856..3f02322d8 100644 --- a/sdk/base/lib/osBindings/index.ts +++ b/sdk/base/lib/osBindings/index.ts @@ -149,7 +149,6 @@ export { NetInfo } from './NetInfo' export { NetworkInfo } from './NetworkInfo' export { NetworkInterfaceInfo } from './NetworkInterfaceInfo' export { NetworkInterfaceType } from './NetworkInterfaceType' -export { OnionHostname } from './OnionHostname' export { OsIndex } from './OsIndex' export { OsVersionInfoMap } from './OsVersionInfoMap' export { OsVersionInfo } from './OsVersionInfo' diff --git a/sdk/base/lib/util/getServiceInterface.ts b/sdk/base/lib/util/getServiceInterface.ts index 17186b7d7..d0f5fe6d5 100644 --- a/sdk/base/lib/util/getServiceInterface.ts +++ b/sdk/base/lib/util/getServiceInterface.ts @@ -20,7 +20,6 @@ export const getHostname = (url: string): Hostname | null => { } type FilterKinds = - | 'onion' | 'mdns' | 'domain' | 'ip' @@ -42,27 +41,25 @@ type VisibilityFilter = V extends 'public' | (HostnameInfo & { public: false }) | VisibilityFilter> : never -type KindFilter = K extends 'onion' - ? (HostnameInfo & { kind: 'onion' }) | KindFilter> - : K extends 'mdns' +type KindFilter = K extends 'mdns' + ? + | (HostnameInfo & { kind: 'ip'; hostname: { kind: 'local' } }) + | KindFilter> + : K extends 'domain' ? - | (HostnameInfo & { kind: 'ip'; hostname: { kind: 'local' } }) - | KindFilter> - : K extends 'domain' + | (HostnameInfo & { kind: 'ip'; hostname: { kind: 'domain' } }) + | KindFilter> + : K extends 'ipv4' ? - | (HostnameInfo & { kind: 'ip'; hostname: { kind: 'domain' } }) - | KindFilter> - : K extends 'ipv4' + | (HostnameInfo & { kind: 'ip'; hostname: { kind: 'ipv4' } }) + | KindFilter> + : K extends 'ipv6' ? - | (HostnameInfo & { kind: 'ip'; hostname: { kind: 'ipv4' } }) - | KindFilter> - : K extends 'ipv6' - ? - | (HostnameInfo & { kind: 'ip'; hostname: { kind: 'ipv6' } }) - | KindFilter> - : K extends 'ip' - ? KindFilter | 'ipv4' | 'ipv6'> - : never + | (HostnameInfo & { kind: 'ip'; hostname: { kind: 'ipv6' } }) + | KindFilter> + : K extends 'ip' + ? KindFilter | 'ipv4' | 'ipv6'> + : never type FilterReturnTy = F extends { visibility: infer V extends 'public' | 'private' @@ -90,10 +87,6 @@ const nonLocalFilter = { const publicFilter = { visibility: 'public', } as const -const onionFilter = { - kind: 'onion', -} as const - type Formats = 'hostname-info' | 'urlstring' | 'url' type FormatReturnTy< F extends Filter, @@ -124,7 +117,6 @@ export type Filled = { nonLocal: Filled public: Filled - onion: Filled } export type FilledAddressInfo = AddressInfo & Filled export type ServiceInterfaceFilled = { @@ -162,9 +154,7 @@ export const addressHostToUrl = ( scheme in knownProtocols && port === knownProtocols[scheme as keyof typeof knownProtocols].defaultPort let hostname - if (host.kind === 'onion') { - hostname = host.hostname.value - } else if (host.kind === 'ip') { + if (host.kind === 'ip') { if (host.hostname.kind === 'domain') { hostname = host.hostname.value } else if (host.hostname.kind === 'ipv6') { @@ -201,13 +191,9 @@ function filterRec( hostnames = hostnames.filter((h) => invert !== pred(h)) } if (filter.visibility === 'public') - hostnames = hostnames.filter( - (h) => invert !== (h.kind === 'onion' || h.public), - ) + hostnames = hostnames.filter((h) => invert !== h.public) if (filter.visibility === 'private') - hostnames = hostnames.filter( - (h) => invert !== (h.kind !== 'onion' && !h.public), - ) + hostnames = hostnames.filter((h) => invert !== !h.public) if (filter.kind) { const kind = new Set( Array.isArray(filter.kind) ? filter.kind : [filter.kind], @@ -219,10 +205,7 @@ function filterRec( hostnames = hostnames.filter( (h) => invert !== - ((kind.has('onion') && h.kind === 'onion') || - (kind.has('mdns') && - h.kind === 'ip' && - h.hostname.kind === 'local') || + ((kind.has('mdns') && h.kind === 'ip' && h.hostname.kind === 'local') || (kind.has('domain') && h.kind === 'ip' && h.hostname.kind === 'domain') || @@ -266,11 +249,6 @@ export const filledAddress = ( filterRec(hostnames, publicFilter, false), ), ) - const getOnion = once(() => - filledAddressFromHostnames( - filterRec(hostnames, onionFilter, false), - ), - ) return { ...addressInfo, hostnames, @@ -294,9 +272,6 @@ export const filledAddress = ( get public(): Filled { return getPublic() }, - get onion(): Filled { - return getOnion() - }, } } diff --git a/sdk/base/lib/util/patterns.ts b/sdk/base/lib/util/patterns.ts index 59f7a863b..b1f54c44d 100644 --- a/sdk/base/lib/util/patterns.ts +++ b/sdk/base/lib/util/patterns.ts @@ -21,11 +21,6 @@ export const localHostname: Pattern = { description: 'Must be a valid ".local" hostname', } -export const torHostname: Pattern = { - regex: regexes.torHostname.matches(), - description: 'Must be a valid Tor (".onion") hostname', -} - export const url: Pattern = { regex: regexes.url.matches(), description: 'Must be a valid URL', @@ -36,11 +31,6 @@ export const localUrl: Pattern = { description: 'Must be a valid ".local" URL', } -export const torUrl: Pattern = { - regex: regexes.torUrl.matches(), - description: 'Must be a valid Tor (".onion") URL', -} - export const ascii: Pattern = { regex: regexes.ascii.matches(), description: diff --git a/sdk/base/lib/util/regexes.ts b/sdk/base/lib/util/regexes.ts index 65213a7b3..c5a78b2bb 100644 --- a/sdk/base/lib/util/regexes.ts +++ b/sdk/base/lib/util/regexes.ts @@ -39,10 +39,6 @@ export const localHostname = new ComposableRegex( /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local/, ) -export const torHostname = new ComposableRegex( - /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.onion/, -) - // https://ihateregex.io/expr/url/ export const url = new ComposableRegex( /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/, @@ -52,10 +48,6 @@ export const localUrl = new ComposableRegex( /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/, ) -export const torUrl = new ComposableRegex( - /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.onion\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/, -) - // https://ihateregex.io/expr/ascii/ export const ascii = new ComposableRegex(/[ -~]*/) diff --git a/sdk/package/scripts/oldSpecToBuilder.ts b/sdk/package/scripts/oldSpecToBuilder.ts index c8a275d57..22d377083 100644 --- a/sdk/package/scripts/oldSpecToBuilder.ts +++ b/sdk/package/scripts/oldSpecToBuilder.ts @@ -1,9 +1,9 @@ -import * as fs from "fs" +import * as fs from 'fs' // https://stackoverflow.com/questions/2970525/converting-any-string-into-camel-case export function camelCase(value: string) { return value - .replace(/([\(\)\[\]])/g, "") + .replace(/([\(\)\[\]])/g, '') .replace(/^([A-Z])|[\s-_](\w)/g, function (match, p1, p2, offset) { if (p2) return p2.toUpperCase() return p1.toLowerCase() @@ -23,12 +23,12 @@ export async function oldSpecToBuilder( } function isString(x: unknown): x is string { - return typeof x === "string" + return typeof x === 'string' } export default async function makeFileContentFromOld( inputData: Promise | any, - { StartSdk = "start-sdk", nested = true } = {}, + { StartSdk = 'start-sdk', nested = true } = {}, ) { const outputLines: string[] = [] outputLines.push(` @@ -37,16 +37,16 @@ const {InputSpec, List, Value, Variants} = sdk `) const data = await inputData - const namedConsts = new Set(["InputSpec", "Value", "List"]) - const inputSpecName = newConst("inputSpecSpec", convertInputSpec(data)) + const namedConsts = new Set(['InputSpec', 'Value', 'List']) + const inputSpecName = newConst('inputSpecSpec', convertInputSpec(data)) outputLines.push(`export type InputSpecSpec = typeof ${inputSpecName}._TYPE;`) - return outputLines.join("\n") + return outputLines.join('\n') function newConst(key: string, data: string, type?: string) { const variableName = getNextConstName(camelCase(key)) outputLines.push( - `export const ${variableName}${!type ? "" : `: ${type}`} = ${data};`, + `export const ${variableName}${!type ? '' : `: ${type}`} = ${data};`, ) return variableName } @@ -55,7 +55,7 @@ const {InputSpec, List, Value, Variants} = sdk return newConst(key, data) } function convertInputSpecInner(data: any) { - let answer = "{" + let answer = '{' for (const [key, value] of Object.entries(data)) { const variableName = maybeNewConst(key, convertValueSpec(value)) @@ -69,7 +69,7 @@ const {InputSpec, List, Value, Variants} = sdk } function convertValueSpec(value: any): string { switch (value.type) { - case "string": { + case 'string': { if (value.textarea) { return `${rangeToTodoComment( value?.range, @@ -99,12 +99,12 @@ const {InputSpec, List, Value, Variants} = sdk warning: value.warning || null, masked: value.masked || false, placeholder: value.placeholder || null, - inputmode: "text", + inputmode: 'text', patterns: value.pattern ? [ { regex: value.pattern, - description: value["pattern-description"], + description: value['pattern-description'], }, ] : [], @@ -115,7 +115,7 @@ const {InputSpec, List, Value, Variants} = sdk 2, )})` } - case "number": { + case 'number': { return `${rangeToTodoComment( value?.range, )}Value.number(${JSON.stringify( @@ -136,7 +136,7 @@ const {InputSpec, List, Value, Variants} = sdk 2, )})` } - case "boolean": { + case 'boolean': { return `Value.toggle(${JSON.stringify( { name: value.name || null, @@ -148,15 +148,15 @@ const {InputSpec, List, Value, Variants} = sdk 2, )})` } - case "enum": { + case 'enum': { const allValueNames = new Set([ - ...(value?.["values"] || []), - ...Object.keys(value?.["value-names"] || {}), + ...(value?.['values'] || []), + ...Object.keys(value?.['value-names'] || {}), ]) const values = Object.fromEntries( Array.from(allValueNames) .filter(isString) - .map((key) => [key, value?.spec?.["value-names"]?.[key] || key]), + .map((key) => [key, value?.spec?.['value-names']?.[key] || key]), ) return `Value.select(${JSON.stringify( { @@ -170,9 +170,9 @@ const {InputSpec, List, Value, Variants} = sdk 2, )} as const)` } - case "object": { + case 'object': { const specName = maybeNewConst( - value.name + "_spec", + value.name + '_spec', convertInputSpec(value.spec), ) return `Value.object({ @@ -180,10 +180,10 @@ const {InputSpec, List, Value, Variants} = sdk description: ${JSON.stringify(value.description || null)}, }, ${specName})` } - case "union": { + case 'union': { const variants = maybeNewConst( - value.name + "_variants", - convertVariants(value.variants, value.tag["variant-names"] || {}), + value.name + '_variants', + convertVariants(value.variants, value.tag['variant-names'] || {}), ) return `Value.union({ @@ -194,18 +194,18 @@ const {InputSpec, List, Value, Variants} = sdk variants: ${variants}, })` } - case "list": { - if (value.subtype === "enum") { + case 'list': { + if (value.subtype === 'enum') { const allValueNames = new Set([ - ...(value?.spec?.["values"] || []), - ...Object.keys(value?.spec?.["value-names"] || {}), + ...(value?.spec?.['values'] || []), + ...Object.keys(value?.spec?.['value-names'] || {}), ]) const values = Object.fromEntries( Array.from(allValueNames) .filter(isString) .map((key: string) => [ key, - value?.spec?.["value-names"]?.[key] ?? key, + value?.spec?.['value-names']?.[key] ?? key, ]), ) return `Value.multiselect(${JSON.stringify( @@ -222,10 +222,10 @@ const {InputSpec, List, Value, Variants} = sdk 2, )})` } - const list = maybeNewConst(value.name + "_list", convertList(value)) + const list = maybeNewConst(value.name + '_list', convertList(value)) return `Value.list(${list})` } - case "pointer": { + case 'pointer': { return `/* TODO deal with point removed point "${value.name}" */null as any` } } @@ -234,7 +234,7 @@ const {InputSpec, List, Value, Variants} = sdk function convertList(value: any) { switch (value.subtype) { - case "string": { + case 'string': { return `${rangeToTodoComment(value?.range)}List.text(${JSON.stringify( { name: value.name || null, @@ -253,7 +253,7 @@ const {InputSpec, List, Value, Variants} = sdk ? [ { regex: value.spec.pattern, - description: value?.spec?.["pattern-description"], + description: value?.spec?.['pattern-description'], }, ] : [], @@ -281,12 +281,12 @@ const {InputSpec, List, Value, Variants} = sdk // placeholder: value?.spec?.placeholder || null, // })})` // } - case "enum": { - return "/* error!! list.enum */" + case 'enum': { + return '/* error!! list.enum */' } - case "object": { + case 'object': { const specName = maybeNewConst( - value.name + "_spec", + value.name + '_spec', convertInputSpec(value.spec.spec), ) return `${rangeToTodoComment(value?.range)}List.obj({ @@ -297,20 +297,20 @@ const {InputSpec, List, Value, Variants} = sdk description: ${JSON.stringify(value.description || null)}, }, { spec: ${specName}, - displayAs: ${JSON.stringify(value?.spec?.["display-as"] || null)}, - uniqueBy: ${JSON.stringify(value?.spec?.["unique-by"] || null)}, + displayAs: ${JSON.stringify(value?.spec?.['display-as'] || null)}, + uniqueBy: ${JSON.stringify(value?.spec?.['unique-by'] || null)}, })` } - case "union": { + case 'union': { const variants = maybeNewConst( - value.name + "_variants", + value.name + '_variants', convertVariants( value.spec.variants, - value.spec["variant-names"] || {}, + value.spec['variant-names'] || {}, ), ) const unionValueName = maybeNewConst( - value.name + "_union", + value.name + '_union', `${rangeToTodoComment(value?.range)} Value.union({ name: ${JSON.stringify(value?.spec?.tag?.name || null)}, @@ -324,7 +324,7 @@ const {InputSpec, List, Value, Variants} = sdk `, ) const listInputSpec = maybeNewConst( - value.name + "_list_inputSpec", + value.name + '_list_inputSpec', ` InputSpec.of({ "union": ${unionValueName} @@ -340,8 +340,8 @@ const {InputSpec, List, Value, Variants} = sdk warning: ${JSON.stringify(value.warning || null)}, }, { spec: ${listInputSpec}, - displayAs: ${JSON.stringify(value?.spec?.["display-as"] || null)}, - uniqueBy: ${JSON.stringify(value?.spec?.["unique-by"] || null)}, + displayAs: ${JSON.stringify(value?.spec?.['display-as'] || null)}, + uniqueBy: ${JSON.stringify(value?.spec?.['unique-by'] || null)}, })` } } @@ -352,7 +352,7 @@ const {InputSpec, List, Value, Variants} = sdk variants: Record, variantNames: Record, ): string { - let answer = "Variants.of({" + let answer = 'Variants.of({' for (const [key, value] of Object.entries(variants)) { const variantSpec = maybeNewConst(key, convertInputSpec(value)) answer += `"${key}": {name: "${ @@ -373,7 +373,7 @@ const {InputSpec, List, Value, Variants} = sdk } function rangeToTodoComment(range: string | undefined) { - if (!range) return "" + if (!range) return '' return `/* TODO: Convert range for this value (${range})*/` } diff --git a/web/projects/shared/src/types/workspace-config.ts b/web/projects/shared/src/types/workspace-config.ts index ebc678ccd..a00d66050 100644 --- a/web/projects/shared/src/types/workspace-config.ts +++ b/web/projects/shared/src/types/workspace-config.ts @@ -1,5 +1,4 @@ export type AccessType = - | 'tor' | 'mdns' | 'localhost' | 'ipv4' diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.component.ts index a1deaf339..1284e1396 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.component.ts @@ -2,7 +2,6 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core' import { tuiButtonOptionsProvider } from '@taiga-ui/core' import { MappedServiceInterface } from './interface.service' import { InterfaceGatewaysComponent } from './gateways.component' -import { InterfaceTorDomainsComponent } from './tor-domains.component' import { PublicDomainsComponent } from './public-domains/pd.component' import { InterfacePrivateDomainsComponent } from './private-domains.component' import { InterfaceAddressesComponent } from './addresses/addresses.component' @@ -16,7 +15,6 @@ import { InterfaceAddressesComponent } from './addresses/addresses.component' [publicDomains]="value()?.publicDomains" [addSsl]="value()?.addSsl || false" > -

@@ -52,7 +50,6 @@ import { InterfaceAddressesComponent } from './addresses/addresses.component' providers: [tuiButtonOptionsProvider({ size: 'xs' })], imports: [ InterfaceGatewaysComponent, - InterfaceTorDomainsComponent, PublicDomainsComponent, InterfacePrivateDomainsComponent, InterfaceAddressesComponent, diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts index 65e3055bb..218e4d7f3 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts @@ -27,14 +27,6 @@ function cmpWithRankedPredicates( return 0 } -type TorAddress = AddressWithInfo & { info: { kind: 'onion' } } -function filterTor(a: AddressWithInfo): a is TorAddress { - return a.info.kind === 'onion' -} -function cmpTor(a: TorAddress, b: TorAddress): -1 | 0 | 1 { - return cmpWithRankedPredicates(a, b, [x => !x.showSsl]) -} - type LanAddress = AddressWithInfo & { info: { kind: 'ip'; public: false } } function filterLan(a: AddressWithInfo): a is LanAddress { return a.info.kind === 'ip' && !a.info.public @@ -171,7 +163,6 @@ export class InterfaceService { }, ) - const torAddrs = allAddressesWithInfo.filter(filterTor).sort(cmpTor) const lanAddrs = allAddressesWithInfo .filter(filterLan) .sort((a, b) => cmpLan(host, a, b)) @@ -188,7 +179,6 @@ export class InterfaceService { clearnetAddrs[0], lanAddrs[0], vpnAddrs[0], - torAddrs[0], ] .filter(a => !!a) .reduce((acc, x) => { @@ -214,9 +204,8 @@ export class InterfaceService { kind: 'domain', visibility: 'public', }) - const tor = addresses.filter({ kind: 'onion' }) const wanIp = addresses.filter({ kind: 'ipv4', visibility: 'public' }) - const bestPublic = [publicDomains, tor, wanIp].flatMap(h => + const bestPublic = [publicDomains, wanIp].flatMap(h => h.format('urlstring'), )[0] const privateDomains = addresses.filter({ @@ -254,9 +243,6 @@ export class InterfaceService { .format('urlstring')[0] onLan = true break - case 'tor': - matching = tor.format('urlstring')[0] - break case 'mdns': matching = mdns.format('urlstring')[0] onLan = true @@ -302,31 +288,7 @@ export class InterfaceService { "Requires trusting your server's Root CA", ) - // ** Tor ** - if (info.kind === 'onion') { - access = null - gatewayName = null - type = 'Tor' - bullets = [ - this.i18n.transform('Connections can be slow or unreliable at times'), - this.i18n.transform( - 'Public if you share the address publicly, otherwise private', - ), - this.i18n.transform('Requires using a Tor-enabled device or browser'), - ] - // Tor (SSL) - if (showSsl) { - bullets = [rootCaRequired, ...bullets] - // Tor (NON-SSL) - } else { - bullets.unshift( - this.i18n.transform( - 'Ideal for anonymous, censorship-resistant hosting and remote access', - ), - ) - } - // ** Not Tor ** - } else { + { const port = info.hostname.sslPort || info.hostname.port gatewayName = info.gateway.name @@ -479,7 +441,6 @@ export class InterfaceService { export type MappedServiceInterface = T.ServiceInterface & { gateways: InterfaceGateway[] - torDomains: string[] publicDomains: PublicDomain[] privateDomains: string[] addresses: { diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/tor-domains.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/tor-domains.component.ts deleted file mode 100644 index 9fb233423..000000000 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/tor-domains.component.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - inject, - input, -} from '@angular/core' -import { - DialogService, - DocsLinkDirective, - ErrorService, - i18nPipe, - LoadingService, -} from '@start9labs/shared' -import { ISB, utils } from '@start9labs/start-sdk' -import { TuiButton, TuiTitle } from '@taiga-ui/core' -import { TuiSkeleton } from '@taiga-ui/kit' -import { TuiCell } from '@taiga-ui/layout' -import { filter } from 'rxjs' -import { - FormComponent, - FormContext, -} from 'src/app/routes/portal/components/form.component' -import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { FormDialogService } from 'src/app/services/form-dialog.service' -import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' - -import { InterfaceComponent } from './interface.component' - -type OnionForm = { - key: string -} - -@Component({ - selector: 'section[torDomains]', - template: ` -
- Tor Domains - - {{ 'Documentation' | i18n }} - - -
- @for (domain of torDomains(); track domain) { -
- {{ domain }} - -
- } @empty { - @if (torDomains()) { - - {{ 'No Tor domains' | i18n }} - - } @else { - @for (_ of [0, 1]; track $index) { - - } - } - } - `, - styles: ` - :host { - grid-column: span 6; - overflow-wrap: break-word; - } - `, - host: { class: 'g-card' }, - imports: [ - TuiCell, - TuiTitle, - TuiButton, - PlaceholderComponent, - i18nPipe, - DocsLinkDirective, - TuiSkeleton, - ], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class InterfaceTorDomainsComponent { - private readonly dialog = inject(DialogService) - private readonly formDialog = inject(FormDialogService) - private readonly loader = inject(LoadingService) - private readonly errorService = inject(ErrorService) - private readonly api = inject(ApiService) - private readonly interface = inject(InterfaceComponent) - private readonly i18n = inject(i18nPipe) - - readonly torDomains = input.required() - - async remove(onion: string) { - this.dialog - .openConfirm({ label: 'Are you sure?', size: 's' }) - .pipe(filter(Boolean)) - .subscribe(async () => { - const loader = this.loader.open('Removing').subscribe() - const params = { onion } - - try { - if (this.interface.packageId()) { - await this.api.pkgRemoveOnion({ - ...params, - package: this.interface.packageId(), - host: this.interface.value()?.addressInfo.hostId || '', - }) - } else { - await this.api.serverRemoveOnion(params) - } - return true - } catch (e: any) { - this.errorService.handleError(e) - return false - } finally { - loader.unsubscribe() - } - }) - } - - async add() { - this.formDialog.open>(FormComponent, { - label: 'New Tor domain', - data: { - spec: await configBuilderToSpec( - ISB.InputSpec.of({ - key: ISB.Value.text({ - name: this.i18n.transform('Private Key (optional)')!, - description: this.i18n.transform( - 'Optionally provide a base64-encoded ed25519 private key for generating the Tor V3 (.onion) domain. If not provided, a random key will be generated.', - ), - required: false, - default: null, - patterns: [utils.Patterns.base64], - }), - }), - ), - buttons: [ - { - text: this.i18n.transform('Save')!, - handler: async value => this.save(value.key), - }, - ], - }, - }) - } - - private async save(key?: string): Promise { - const loader = this.loader.open('Saving').subscribe() - - try { - const onion = key - ? await this.api.addTorKey({ key }) - : await this.api.generateTorKey({}) - - if (this.interface.packageId()) { - await this.api.pkgAddOnion({ - onion, - package: this.interface.packageId(), - host: this.interface.value()?.addressInfo.hostId || '', - }) - } else { - await this.api.serverAddOnion({ onion }) - } - return true - } catch (e: any) { - this.errorService.handleError(e) - return false - } finally { - loader.unsubscribe() - } - } -} diff --git a/web/projects/ui/src/app/routes/portal/routes/logs/logs.routes.ts b/web/projects/ui/src/app/routes/portal/routes/logs/logs.routes.ts index 281683fcd..c42623ea9 100644 --- a/web/projects/ui/src/app/routes/portal/routes/logs/logs.routes.ts +++ b/web/projects/ui/src/app/routes/portal/routes/logs/logs.routes.ts @@ -13,10 +13,6 @@ export const ROUTES: Routes = [ path: 'os', loadComponent: () => import('./routes/os.component'), }, - { - path: 'tor', - loadComponent: () => import('./routes/tor.component'), - }, ] export default ROUTES diff --git a/web/projects/ui/src/app/routes/portal/routes/logs/routes/outlet.component.ts b/web/projects/ui/src/app/routes/portal/routes/logs/routes/outlet.component.ts index 97d3f69cd..524a997da 100644 --- a/web/projects/ui/src/app/routes/portal/routes/logs/routes/outlet.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/logs/routes/outlet.component.ts @@ -79,12 +79,6 @@ export default class SystemLogsComponent { subtitle: 'Raw, unfiltered operating system logs', icon: '@tui.square-dashed-bottom-code', }, - { - link: 'tor', - title: 'Tor Logs', - subtitle: 'Diagnostics for the Tor daemon on this server', - icon: '@tui.target', - }, { link: 'kernel', title: 'Kernel Logs', diff --git a/web/projects/ui/src/app/routes/portal/routes/logs/routes/tor.component.ts b/web/projects/ui/src/app/routes/portal/routes/logs/routes/tor.component.ts deleted file mode 100644 index 45b711fa5..000000000 --- a/web/projects/ui/src/app/routes/portal/routes/logs/routes/tor.component.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ChangeDetectionStrategy, Component, inject } from '@angular/core' -import { i18nPipe } from '@start9labs/shared' -import { LogsComponent } from 'src/app/routes/portal/components/logs/logs.component' -import { LogsHeaderComponent } from 'src/app/routes/portal/routes/logs/components/header.component' -import { RR } from 'src/app/services/api/api.types' -import { ApiService } from 'src/app/services/api/embassy-api.service' - -@Component({ - template: ` - - {{ 'Diagnostics for the Tor daemon on this server' | i18n }} - - - `, - styles: ` - :host { - padding: 1rem; - } - `, - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [LogsComponent, LogsHeaderComponent, i18nPipe], - host: { class: 'g-page' }, -}) -export default class SystemTorComponent { - private readonly api = inject(ApiService) - - protected readonly follow = (params: RR.FollowServerLogsReq) => - this.api.followTorLogs(params) - - protected readonly fetch = (params: RR.GetServerLogsReq) => - this.api.getTorLogs(params) -} diff --git a/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts index 2b42dacc9..0fa1b4766 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts @@ -139,7 +139,6 @@ export default class ServiceInterfaceRoute { : !binding?.net.privateDisabled.includes(g.id)) ?? false, ...g, })) || [], - torDomains: host.onions, publicDomains: getPublicDomains(host.publicDomains, gateways), privateDomains: host.privateDomains, addSsl: !!binding?.options.addSsl, diff --git a/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.component.ts b/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.component.ts index 8f2055787..ebf7b2587 100644 --- a/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.component.ts @@ -13,7 +13,6 @@ import { TuiFiles, tuiInputFilesOptionsProvider, } from '@taiga-ui/kit' -import { ConfigService } from 'src/app/services/config.service' import { TitleDirective } from 'src/app/services/title.service' import { SideloadPackageComponent } from './package.component' @@ -55,11 +54,6 @@ import { MarketplacePkgSideload, validateS9pk } from './sideload.utils'

{{ 'Upload .s9pk package file' | i18n }}

- @if (isTor) { -

- {{ 'Warning: package upload will be slow over Tor.' | i18n }} -

- }
} @@ -92,8 +86,6 @@ import { MarketplacePkgSideload, validateS9pk } from './sideload.utils' ], }) export default class SideloadComponent { - readonly isTor = inject(ConfigService).accessType === 'tor' - file: File | null = null readonly package = signal(null) readonly error = signal(null) diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts index aaff817d0..cdf664e86 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts @@ -47,13 +47,11 @@ import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { PatchDB } from 'patch-db-client' import { filter } from 'rxjs' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ConfigService } from 'src/app/services/config.service' import { OSService } from 'src/app/services/os.service' import { DataModel } from 'src/app/services/patch-db/data-model' import { TitleDirective } from 'src/app/services/title.service' import { SnekDirective } from './snek.directive' import { UPDATE } from './update.component' -import { SystemWipeComponent } from './wipe.component' import { KeyboardSelectComponent } from './keyboard-select.component' @Component({ @@ -66,7 +64,7 @@ import { KeyboardSelectComponent } from './keyboard-select.component' @if (server(); as server) {
- + {{ 'Software Update' | i18n }} @@ -178,18 +176,6 @@ import { KeyboardSelectComponent } from './keyboard-select.component' }
-
- - - {{ 'Restart Tor' | i18n }} - - {{ 'Restart the Tor daemon on your server' | i18n }} - - - -
@if (count > 4) {
@@ -283,12 +269,10 @@ export default class SystemGeneralComponent { private readonly errorService = inject(ErrorService) private readonly patch = inject>(PatchDB) private readonly api = inject(ApiService) - private readonly isTor = inject(ConfigService).accessType === 'tor' private readonly dialog = inject(DialogService) private readonly i18n = inject(i18nPipe) private readonly injector = inject(INJECTOR) - wipe = false count = 0 readonly server = toSignal(this.patch.watch$('serverInfo')) @@ -392,24 +376,6 @@ export default class SystemGeneralComponent { }) } - onTorRestart() { - this.wipe = false - this.dialog - .openConfirm({ - label: this.isTor ? 'Warning' : 'Confirm', - data: { - content: new PolymorpheusComponent( - SystemWipeComponent, - this.injector, - ), - yes: 'Restart', - no: 'Cancel', - }, - }) - .pipe(filter(Boolean)) - .subscribe(() => this.resetTor(this.wipe)) - } - async onRepair() { this.dialog .openConfirm({ @@ -532,19 +498,6 @@ export default class SystemGeneralComponent { .subscribe(() => this.restart()) } - private async resetTor(wipeState: boolean) { - const loader = this.loader.open().subscribe() - - try { - await this.api.resetTor({ wipeState, reason: 'User triggered' }) - this.dialog.openAlert('Tor restart in progress').subscribe() - } catch (e: any) { - this.errorService.handleError(e) - } finally { - loader.unsubscribe() - } - } - private update() { this.dialogs .open(UPDATE, { diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/wipe.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/wipe.component.ts deleted file mode 100644 index 0456cbed3..000000000 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/wipe.component.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ChangeDetectionStrategy, Component, inject } from '@angular/core' -import { FormsModule } from '@angular/forms' -import { TuiLabel } from '@taiga-ui/core' -import { TuiCheckbox } from '@taiga-ui/kit' -import { ConfigService } from 'src/app/services/config.service' -import SystemGeneralComponent from './general.component' -import { i18nPipe } from '@start9labs/shared' - -@Component({ - template: ` - @if (isTor) { -

- {{ - 'You are currently connected over Tor. If you restart the Tor daemon, you will lose connectivity until it comes back online.' - | i18n - }} -

- } -

- {{ - 'Optionally wipe state to forcibly acquire new guard nodes. It is recommended to try without wiping state first.' - | i18n - }} -

- - `, - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [TuiLabel, FormsModule, TuiCheckbox, i18nPipe], -}) -export class SystemWipeComponent { - readonly isTor = inject(ConfigService).accessType === 'tor' - readonly component = inject(SystemGeneralComponent) -} diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/startos-ui/startos-ui.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/startos-ui/startos-ui.component.ts index 524eb92dc..08ba78d94 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/startos-ui/startos-ui.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/startos-ui/startos-ui.component.ts @@ -100,7 +100,6 @@ export default class StartOsUiComponent { : !binding?.net.privateDisabled.includes(g.id)) ?? false, ...g, })), - torDomains: network.host.onions, publicDomains: getPublicDomains(network.host.publicDomains, gateways), privateDomains: network.host.privateDomains, addSsl: true, diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index 4a30d43f6..a5280f5f4 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -2138,7 +2138,6 @@ export namespace Mock { }, publicDomains: {}, privateDomains: [], - onions: [], hostnameInfo: { 80: [ { @@ -2209,14 +2208,6 @@ export namespace Mock { sslPort: 1234, }, }, - { - kind: 'onion', - hostname: { - value: 'bitcoin-p2p.onion', - port: 80, - sslPort: 443, - }, - }, ], }, }, @@ -2239,7 +2230,6 @@ export namespace Mock { }, publicDomains: {}, privateDomains: [], - onions: [], hostnameInfo: { 8332: [], }, @@ -2263,7 +2253,6 @@ export namespace Mock { }, publicDomains: {}, privateDomains: [], - onions: [], hostnameInfo: { 8333: [], }, diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index 565bca8e4..85abac31a 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -79,14 +79,14 @@ export namespace RR { uptime: number // seconds } - export type GetServerLogsReq = FetchLogsReq // server.logs & server.kernel-logs & net.tor.logs + export type GetServerLogsReq = FetchLogsReq // server.logs & server.kernel-logs export type GetServerLogsRes = FetchLogsRes export type FollowServerLogsReq = { limit?: number // (optional) default is 50. Ignored if cursor provided boot?: number | string | null // (optional) number is offset (0: current, -1 prev, +1 first), string is a specific boot id, null is all. Default is undefined cursor?: string // the last known log. Websocket will return all logs since this log - } // server.logs.follow & server.kernel-logs.follow & net.tor.follow-logs + } // server.logs.follow & server.kernel-logs.follow export type FollowServerLogsRes = { startCursor: string guid: string @@ -120,12 +120,6 @@ export namespace RR { } // net.dns.query export type QueryDnsRes = string | null - export type ResetTorReq = { - wipeState: boolean - reason: string - } // net.tor.reset - export type ResetTorRes = null - export type SetKeyboardReq = FullKeyboard // server.set-keyboard export type SetKeyboardRes = null @@ -287,13 +281,6 @@ export namespace RR { } export type RemoveAcmeRes = null - export type AddTorKeyReq = { - // net.tor.key.add - key: string - } - export type GenerateTorKeyReq = {} // net.tor.key.generate - export type AddTorKeyRes = string // onion address *with* .onion suffix - export type ServerBindingToggleGatewayReq = { // server.host.binding.set-gateway-enabled gateway: T.GatewayId @@ -302,15 +289,6 @@ export namespace RR { } export type ServerBindingToggleGatewayRes = null - export type ServerAddOnionReq = { - // server.host.address.onion.add - onion: string // address *with* .onion suffix - } - export type AddOnionRes = null - - export type ServerRemoveOnionReq = ServerAddOnionReq // server.host.address.onion.remove - export type RemoveOnionRes = null - export type OsUiAddPublicDomainReq = { // server.host.address.domain.public.add fqdn: string // FQDN @@ -348,13 +326,6 @@ export namespace RR { } export type PkgBindingToggleGatewayRes = null - export type PkgAddOnionReq = ServerAddOnionReq & { - // package.host.address.onion.add - package: T.PackageId // string - host: T.HostId // string - } - export type PkgRemoveOnionReq = PkgAddOnionReq // package.host.address.onion.remove - export type PkgAddPublicDomainReq = OsUiAddPublicDomainReq & { // package.host.address.domain.public.add package: T.PackageId // string diff --git a/web/projects/ui/src/app/services/api/embassy-api.service.ts b/web/projects/ui/src/app/services/api/embassy-api.service.ts index 41d918ef0..c55dc8441 100644 --- a/web/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-api.service.ts @@ -81,8 +81,6 @@ export abstract class ApiService { params: RR.GetServerLogsReq, ): Promise - abstract getTorLogs(params: RR.GetServerLogsReq): Promise - abstract getKernelLogs( params: RR.GetServerLogsReq, ): Promise @@ -91,10 +89,6 @@ export abstract class ApiService { params: RR.FollowServerLogsReq, ): Promise - abstract followTorLogs( - params: RR.FollowServerLogsReq, - ): Promise - abstract followKernelLogs( params: RR.FollowServerLogsReq, ): Promise @@ -125,8 +119,6 @@ export abstract class ApiService { abstract queryDns(params: RR.QueryDnsReq): Promise - abstract resetTor(params: RR.ResetTorReq): Promise - // smtp abstract setSmtp(params: RR.SetSMTPReq): Promise @@ -344,22 +336,10 @@ export abstract class ApiService { abstract removeAcme(params: RR.RemoveAcmeReq): Promise - abstract addTorKey(params: RR.AddTorKeyReq): Promise - - abstract generateTorKey( - params: RR.GenerateTorKeyReq, - ): Promise - abstract serverBindingToggleGateway( params: RR.ServerBindingToggleGatewayReq, ): Promise - abstract serverAddOnion(params: RR.ServerAddOnionReq): Promise - - abstract serverRemoveOnion( - params: RR.ServerRemoveOnionReq, - ): Promise - abstract osUiAddPublicDomain( params: RR.OsUiAddPublicDomainReq, ): Promise @@ -380,12 +360,6 @@ export abstract class ApiService { params: RR.PkgBindingToggleGatewayReq, ): Promise - abstract pkgAddOnion(params: RR.PkgAddOnionReq): Promise - - abstract pkgRemoveOnion( - params: RR.PkgRemoveOnionReq, - ): Promise - abstract pkgAddPublicDomain( params: RR.PkgAddPublicDomainReq, ): Promise diff --git a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts index 47890f047..00524e2ef 100644 --- a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -195,10 +195,6 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'server.logs', params }) } - async getTorLogs(params: RR.GetServerLogsReq): Promise { - return this.rpcRequest({ method: 'net.tor.logs', params }) - } - async getKernelLogs( params: RR.GetServerLogsReq, ): Promise { @@ -211,12 +207,6 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'server.logs.follow', params }) } - async followTorLogs( - params: RR.FollowServerLogsReq, - ): Promise { - return this.rpcRequest({ method: 'net.tor.logs.follow', params }) - } - async followKernelLogs( params: RR.FollowServerLogsReq, ): Promise { @@ -278,10 +268,6 @@ export class LiveApiService extends ApiService { }) } - async resetTor(params: RR.ResetTorReq): Promise { - return this.rpcRequest({ method: 'net.tor.reset', params }) - } - // marketplace URLs async checkOSUpdate( @@ -621,20 +607,6 @@ export class LiveApiService extends ApiService { }) } - async addTorKey(params: RR.AddTorKeyReq): Promise { - return this.rpcRequest({ - method: 'net.tor.key.add', - params, - }) - } - - async generateTorKey(params: RR.GenerateTorKeyReq): Promise { - return this.rpcRequest({ - method: 'net.tor.key.generate', - params, - }) - } - async serverBindingToggleGateway( params: RR.ServerBindingToggleGatewayReq, ): Promise { @@ -644,22 +616,6 @@ export class LiveApiService extends ApiService { }) } - async serverAddOnion(params: RR.ServerAddOnionReq): Promise { - return this.rpcRequest({ - method: 'server.host.address.onion.add', - params, - }) - } - - async serverRemoveOnion( - params: RR.ServerRemoveOnionReq, - ): Promise { - return this.rpcRequest({ - method: 'server.host.address.onion.remove', - params, - }) - } - async osUiAddPublicDomain( params: RR.OsUiAddPublicDomainReq, ): Promise { @@ -705,22 +661,6 @@ export class LiveApiService extends ApiService { }) } - async pkgAddOnion(params: RR.PkgAddOnionReq): Promise { - return this.rpcRequest({ - method: 'package.host.address.onion.add', - params, - }) - } - - async pkgRemoveOnion( - params: RR.PkgRemoveOnionReq, - ): Promise { - return this.rpcRequest({ - method: 'package.host.address.onion.remove', - params, - }) - } - async pkgAddPublicDomain( params: RR.PkgAddPublicDomainReq, ): Promise { diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index c2f8d953b..e91a732e5 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -281,17 +281,6 @@ export class MockApiService extends ApiService { } } - async getTorLogs(params: RR.GetServerLogsReq): Promise { - await pauseFor(2000) - const entries = this.randomLogs(params.limit) - - return { - entries, - startCursor: 'start-cursor', - endCursor: 'end-cursor', - } - } - async getKernelLogs( params: RR.GetServerLogsReq, ): Promise { @@ -315,16 +304,6 @@ export class MockApiService extends ApiService { } } - async followTorLogs( - params: RR.FollowServerLogsReq, - ): Promise { - await pauseFor(2000) - return { - startCursor: 'start-cursor', - guid: 'logs-guid', - } - } - async followKernelLogs( params: RR.FollowServerLogsReq, ): Promise { @@ -504,11 +483,6 @@ export class MockApiService extends ApiService { return null } - async resetTor(params: RR.ResetTorReq): Promise { - await pauseFor(2000) - return null - } - // marketplace URLs async checkOSUpdate( @@ -1374,16 +1348,6 @@ export class MockApiService extends ApiService { return null } - async addTorKey(params: RR.AddTorKeyReq): Promise { - await pauseFor(2000) - return 'vanityabcdefghijklmnop.onion' - } - - async generateTorKey(params: RR.GenerateTorKeyReq): Promise { - await pauseFor(2000) - return 'abcdefghijklmnopqrstuv.onion' - } - async serverBindingToggleGateway( params: RR.ServerBindingToggleGatewayReq, ): Promise { @@ -1401,53 +1365,6 @@ export class MockApiService extends ApiService { return null } - async serverAddOnion(params: RR.ServerAddOnionReq): Promise { - await pauseFor(2000) - - const patch: Operation[] = [ - { - op: PatchOp.ADD, - path: `/serverInfo/host/onions/0`, - value: params.onion, - }, - { - op: PatchOp.ADD, - path: `/serverInfo/host/hostnameInfo/80/0`, - value: { - kind: 'onion', - hostname: { - port: 80, - sslPort: 443, - value: params.onion, - }, - }, - }, - ] - this.mockRevision(patch) - - return null - } - - async serverRemoveOnion( - params: RR.ServerRemoveOnionReq, - ): Promise { - await pauseFor(2000) - - const patch: RemoveOperation[] = [ - { - op: PatchOp.REMOVE, - path: `/serverInfo/host/onions/0`, - }, - { - op: PatchOp.REMOVE, - path: `/serverInfo/host/hostnameInfo/80/-1`, - }, - ] - this.mockRevision(patch) - - return null - } - async osUiAddPublicDomain( params: RR.OsUiAddPublicDomainReq, ): Promise { @@ -1574,53 +1491,6 @@ export class MockApiService extends ApiService { return null } - async pkgAddOnion(params: RR.PkgAddOnionReq): Promise { - await pauseFor(2000) - - const patch: Operation[] = [ - { - op: PatchOp.ADD, - path: `/packageData/${params.package}/hosts/${params.host}/onions/0`, - value: params.onion, - }, - { - op: PatchOp.ADD, - path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`, - value: { - kind: 'onion', - hostname: { - port: 80, - sslPort: 443, - value: params.onion, - }, - }, - }, - ] - this.mockRevision(patch) - - return null - } - - async pkgRemoveOnion( - params: RR.PkgRemoveOnionReq, - ): Promise { - await pauseFor(2000) - - const patch: RemoveOperation[] = [ - { - op: PatchOp.REMOVE, - path: `/packageData/${params.package}/hosts/${params.host}/onions/0`, - }, - { - op: PatchOp.REMOVE, - path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`, - }, - ] - this.mockRevision(patch) - - return null - } - async pkgAddPublicDomain( params: RR.PkgAddPublicDomainReq, ): Promise { diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index b7da8d5b9..6effc3827 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -54,7 +54,6 @@ export const mockPatchData: DataModel = { }, publicDomains: {}, privateDomains: [], - onions: ['myveryownspecialtoraddress'], hostnameInfo: { 80: [ { @@ -125,14 +124,6 @@ export const mockPatchData: DataModel = { sslPort: 443, }, }, - { - kind: 'onion', - hostname: { - value: 'myveryownspecialtoraddress.onion', - port: 80, - sslPort: 443, - }, - }, ], }, }, @@ -524,7 +515,6 @@ export const mockPatchData: DataModel = { }, publicDomains: {}, privateDomains: [], - onions: [], hostnameInfo: { 80: [ { @@ -595,14 +585,6 @@ export const mockPatchData: DataModel = { sslPort: 1234, }, }, - { - kind: 'onion', - hostname: { - value: 'bitcoin-p2p.onion', - port: 80, - sslPort: 443, - }, - }, ], }, }, @@ -625,7 +607,6 @@ export const mockPatchData: DataModel = { }, publicDomains: {}, privateDomains: [], - onions: [], hostnameInfo: { 8332: [], }, @@ -649,7 +630,6 @@ export const mockPatchData: DataModel = { }, publicDomains: {}, privateDomains: [], - onions: [], hostnameInfo: { 8333: [], }, diff --git a/web/projects/ui/src/app/services/config.service.ts b/web/projects/ui/src/app/services/config.service.ts index 3e9097b54..4e56e8e46 100644 --- a/web/projects/ui/src/app/services/config.service.ts +++ b/web/projects/ui/src/app/services/config.service.ts @@ -32,7 +32,6 @@ export class ConfigService { private getAccessType = utils.once(() => { if (useMocks) return mocks.maskAs if (this.hostname === 'localhost') return 'localhost' - if (this.hostname.endsWith('.onion')) return 'tor' if (this.hostname.endsWith('.local')) return 'mdns' let ip = null try { @@ -51,7 +50,7 @@ export class ConfigService { } isLanHttp(): boolean { - return !this.isHttps() && !['localhost', 'tor'].includes(this.accessType) + return !this.isHttps() && this.accessType !== 'localhost' } isHttps(): boolean { From 8204074bdfbdedff39de1dcb82f9f7a86925a267 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Tue, 10 Feb 2026 13:38:12 -0700 Subject: [PATCH 06/17] chore: flatten HostnameInfo from enum to struct HostnameInfo only had one variant (Ip) after removing Tor. Flatten it into a plain struct with fields gateway, public, hostname. Remove all kind === 'ip' type guards and narrowing across SDK, frontend, and container runtime. Update DB migration to strip the kind field. --- .../DockerProcedureContainer.ts | 2 +- .../Systems/SystemForEmbassy/index.ts | 8 +--- core/src/net/net_controller.rs | 12 +++--- core/src/net/service_interface.rs | 16 +++----- core/src/version/v0_4_0_alpha_20.rs | 13 +++++-- sdk/base/lib/osBindings/HostnameInfo.ts | 1 - sdk/base/lib/util/getServiceInterface.ts | 37 ++++++++----------- .../interfaces/interface.service.ts | 22 ++++------- 8 files changed, 47 insertions(+), 64 deletions(-) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts index 029483212..b6fd6f033 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts @@ -93,7 +93,7 @@ export class DockerProcedureContainer extends Drop { )?.hostnameInfo || {}, ) .flatMap((h) => h) - .flatMap((h) => (h.kind === "onion" ? [h.hostname.value] : [])), + .map((h) => h.hostname.value), ).values(), ] const certChain = await effects.getSslCertificate({ diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index c2f84c19f..82dc037f9 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -1244,12 +1244,8 @@ async function updateConfig( ? "" : catchFn( () => - (specValue.target === "lan-address" - ? filled.addressInfo!.filter({ kind: "mdns" }) || - filled.addressInfo!.onion - : filled.addressInfo!.onion || - filled.addressInfo!.filter({ kind: "mdns" }) - ).hostnames[0].hostname.value, + filled.addressInfo!.filter({ kind: "mdns" })! + .hostnames[0].hostname.value, ) || "" mutConfigValue[key] = url } diff --git a/core/src/net/net_controller.rs b/core/src/net/net_controller.rs index b1c6e0a6a..06cc087b5 100644 --- a/core/src/net/net_controller.rs +++ b/core/src/net/net_controller.rs @@ -481,7 +481,7 @@ impl NetServiceData { i.device_type != Some(NetworkInterfaceType::Wireguard) }) { - bind_hostname_info.push(HostnameInfo::Ip { + bind_hostname_info.push(HostnameInfo { gateway: gateway.clone(), public: false, hostname: IpHostname::Local { @@ -514,7 +514,7 @@ impl NetServiceData { .as_ref() .map_or(false, |ssl| ssl.preferred_external_port == 443) { - bind_hostname_info.push(HostnameInfo::Ip { + bind_hostname_info.push(HostnameInfo { gateway: gateway.clone(), public, hostname: IpHostname::Domain { @@ -524,7 +524,7 @@ impl NetServiceData { }, }); } else { - bind_hostname_info.push(HostnameInfo::Ip { + bind_hostname_info.push(HostnameInfo { gateway: gateway.clone(), public, hostname: IpHostname::Domain { @@ -540,7 +540,7 @@ impl NetServiceData { if let Some(ip_info) = &info.ip_info { let public = info.public(); if let Some(wan_ip) = ip_info.wan_ip { - bind_hostname_info.push(HostnameInfo::Ip { + bind_hostname_info.push(HostnameInfo { gateway: gateway.clone(), public: true, hostname: IpHostname::Ipv4 { @@ -554,7 +554,7 @@ impl NetServiceData { match ipnet { IpNet::V4(net) => { if !public { - bind_hostname_info.push(HostnameInfo::Ip { + bind_hostname_info.push(HostnameInfo { gateway: gateway.clone(), public, hostname: IpHostname::Ipv4 { @@ -566,7 +566,7 @@ impl NetServiceData { } } IpNet::V6(net) => { - bind_hostname_info.push(HostnameInfo::Ip { + bind_hostname_info.push(HostnameInfo { gateway: gateway.clone(), public: public && !ipv6_is_local(net.addr()), hostname: IpHostname::Ipv6 { diff --git a/core/src/net/service_interface.rs b/core/src/net/service_interface.rs index ca960e569..6d04ae891 100644 --- a/core/src/net/service_interface.rs +++ b/core/src/net/service_interface.rs @@ -9,20 +9,14 @@ use crate::{GatewayId, HostId, ServiceInterfaceId}; #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] -#[serde(rename_all_fields = "camelCase")] -#[serde(tag = "kind")] -pub enum HostnameInfo { - Ip { - gateway: GatewayInfo, - public: bool, - hostname: IpHostname, - }, +pub struct HostnameInfo { + pub gateway: GatewayInfo, + pub public: bool, + pub hostname: IpHostname, } impl HostnameInfo { pub fn to_san_hostname(&self) -> InternedString { - match self { - Self::Ip { hostname, .. } => hostname.to_san_hostname(), - } + self.hostname.to_san_hostname() } } diff --git a/core/src/version/v0_4_0_alpha_20.rs b/core/src/version/v0_4_0_alpha_20.rs index 6cb902bde..3320af322 100644 --- a/core/src/version/v0_4_0_alpha_20.rs +++ b/core/src/version/v0_4_0_alpha_20.rs @@ -58,7 +58,7 @@ impl VersionT for Version { } // Remove onion entries from hostnameInfo in server host - remove_onion_hostname_info( + migrate_hostname_info( db.get_mut("public") .and_then(|p| p.get_mut("serverInfo")) .and_then(|s| s.get_mut("network")) @@ -74,7 +74,7 @@ impl VersionT for Version { for (_, package) in packages.iter_mut() { if let Some(hosts) = package.get_mut("hosts").and_then(|h| h.as_object_mut()) { for (_, host) in hosts.iter_mut() { - remove_onion_hostname_info(Some(host)); + migrate_hostname_info(Some(host)); } } } @@ -96,14 +96,21 @@ impl VersionT for Version { } } -fn remove_onion_hostname_info(host: Option<&mut Value>) { +fn migrate_hostname_info(host: Option<&mut Value>) { if let Some(hostname_info) = host .and_then(|h| h.get_mut("hostnameInfo")) .and_then(|h| h.as_object_mut()) { for (_, infos) in hostname_info.iter_mut() { if let Some(arr) = infos.as_array_mut() { + // Remove onion entries arr.retain(|info| info.get("kind").and_then(|k| k.as_str()) != Some("onion")); + // Strip "kind" field from remaining entries (HostnameInfo flattened from enum to struct) + for info in arr.iter_mut() { + if let Some(obj) = info.as_object_mut() { + obj.remove("kind"); + } + } } } } diff --git a/sdk/base/lib/osBindings/HostnameInfo.ts b/sdk/base/lib/osBindings/HostnameInfo.ts index 5865c6381..4d80dd43f 100644 --- a/sdk/base/lib/osBindings/HostnameInfo.ts +++ b/sdk/base/lib/osBindings/HostnameInfo.ts @@ -3,7 +3,6 @@ import type { GatewayInfo } from './GatewayInfo' import type { IpHostname } from './IpHostname' export type HostnameInfo = { - kind: 'ip' gateway: GatewayInfo public: boolean hostname: IpHostname diff --git a/sdk/base/lib/util/getServiceInterface.ts b/sdk/base/lib/util/getServiceInterface.ts index d0f5fe6d5..cc80e3aec 100644 --- a/sdk/base/lib/util/getServiceInterface.ts +++ b/sdk/base/lib/util/getServiceInterface.ts @@ -43,19 +43,19 @@ type VisibilityFilter = V extends 'public' : never type KindFilter = K extends 'mdns' ? - | (HostnameInfo & { kind: 'ip'; hostname: { kind: 'local' } }) + | (HostnameInfo & { hostname: { kind: 'local' } }) | KindFilter> : K extends 'domain' ? - | (HostnameInfo & { kind: 'ip'; hostname: { kind: 'domain' } }) + | (HostnameInfo & { hostname: { kind: 'domain' } }) | KindFilter> : K extends 'ipv4' ? - | (HostnameInfo & { kind: 'ip'; hostname: { kind: 'ipv4' } }) + | (HostnameInfo & { hostname: { kind: 'ipv4' } }) | KindFilter> : K extends 'ipv6' ? - | (HostnameInfo & { kind: 'ip'; hostname: { kind: 'ipv6' } }) + | (HostnameInfo & { hostname: { kind: 'ipv6' } }) | KindFilter> : K extends 'ip' ? KindFilter | 'ipv4' | 'ipv6'> @@ -154,16 +154,14 @@ export const addressHostToUrl = ( scheme in knownProtocols && port === knownProtocols[scheme as keyof typeof knownProtocols].defaultPort let hostname - if (host.kind === 'ip') { - if (host.hostname.kind === 'domain') { - hostname = host.hostname.value - } else if (host.hostname.kind === 'ipv6') { - hostname = IPV6_LINK_LOCAL.contains(host.hostname.value) - ? `[${host.hostname.value}%${host.hostname.scopeId}]` - : `[${host.hostname.value}]` - } else { - hostname = host.hostname.value - } + if (host.hostname.kind === 'domain') { + hostname = host.hostname.value + } else if (host.hostname.kind === 'ipv6') { + hostname = IPV6_LINK_LOCAL.contains(host.hostname.value) + ? `[${host.hostname.value}%${host.hostname.scopeId}]` + : `[${host.hostname.value}]` + } else { + hostname = host.hostname.value } return `${scheme ? `${scheme}://` : ''}${ username ? `${username}@` : '' @@ -205,16 +203,13 @@ function filterRec( hostnames = hostnames.filter( (h) => invert !== - ((kind.has('mdns') && h.kind === 'ip' && h.hostname.kind === 'local') || - (kind.has('domain') && - h.kind === 'ip' && - h.hostname.kind === 'domain') || - (kind.has('ipv4') && h.kind === 'ip' && h.hostname.kind === 'ipv4') || - (kind.has('ipv6') && h.kind === 'ip' && h.hostname.kind === 'ipv6') || + ((kind.has('mdns') && h.hostname.kind === 'local') || + (kind.has('domain') && h.hostname.kind === 'domain') || + (kind.has('ipv4') && h.hostname.kind === 'ipv4') || + (kind.has('ipv6') && h.hostname.kind === 'ipv6') || (kind.has('localhost') && ['localhost', '127.0.0.1', '::1'].includes(h.hostname.value)) || (kind.has('link-local') && - h.kind === 'ip' && h.hostname.kind === 'ipv6' && IPV6_LINK_LOCAL.contains(IpAddress.parse(h.hostname.value)))), ) diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts index 218e4d7f3..21bc013b7 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts @@ -27,9 +27,9 @@ function cmpWithRankedPredicates( return 0 } -type LanAddress = AddressWithInfo & { info: { kind: 'ip'; public: false } } +type LanAddress = AddressWithInfo & { info: { public: false } } function filterLan(a: AddressWithInfo): a is LanAddress { - return a.info.kind === 'ip' && !a.info.public + return !a.info.public } function cmpLan(host: T.Host, a: LanAddress, b: LanAddress): -1 | 0 | 1 { return cmpWithRankedPredicates(a, b, [ @@ -45,15 +45,12 @@ function cmpLan(host: T.Host, a: LanAddress, b: LanAddress): -1 | 0 | 1 { type VpnAddress = AddressWithInfo & { info: { - kind: 'ip' public: false hostname: { kind: 'ipv4' | 'ipv6' | 'domain' } } } function filterVpn(a: AddressWithInfo): a is VpnAddress { - return ( - a.info.kind === 'ip' && !a.info.public && a.info.hostname.kind !== 'local' - ) + return !a.info.public && a.info.hostname.kind !== 'local' } function cmpVpn(host: T.Host, a: VpnAddress, b: VpnAddress): -1 | 0 | 1 { return cmpWithRankedPredicates(a, b, [ @@ -68,13 +65,12 @@ function cmpVpn(host: T.Host, a: VpnAddress, b: VpnAddress): -1 | 0 | 1 { type ClearnetAddress = AddressWithInfo & { info: { - kind: 'ip' public: true hostname: { kind: 'ipv4' | 'ipv6' | 'domain' } } } function filterClearnet(a: AddressWithInfo): a is ClearnetAddress { - return a.info.kind === 'ip' && a.info.public + return a.info.public } function cmpClearnet( host: T.Host, @@ -134,10 +130,7 @@ export class InterfaceService { h, ) const info = h - const gateway = - h.kind === 'ip' - ? gateways.find(g => h.gateway.id === g.id) - : undefined + const gateway = gateways.find(g => h.gateway.id === g.id) const res = [] if (url) { res.push({ @@ -266,10 +259,9 @@ export class InterfaceService { h => this.config.accessType === 'localhost' || !( - h.kind === 'ip' && - ((h.hostname.kind === 'ipv6' && + (h.hostname.kind === 'ipv6' && utils.IPV6_LINK_LOCAL.contains(h.hostname.value)) || - h.gateway.id === 'lo') + h.gateway.id === 'lo' ), ) || [] ) From e1915bf497f9f351f96d6c4e7d98d62ce72acb56 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Tue, 10 Feb 2026 13:38:40 -0700 Subject: [PATCH 07/17] chore: format RPCSpec.md markdown table --- container-runtime/RPCSpec.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/container-runtime/RPCSpec.md b/container-runtime/RPCSpec.md index 7c43467ba..57ff31348 100644 --- a/container-runtime/RPCSpec.md +++ b/container-runtime/RPCSpec.md @@ -139,8 +139,8 @@ Evaluate a script in the runtime context. Used for debugging. The `execute` and `sandbox` methods route to procedures based on the `procedure` path: -| Procedure | Description | -|-----------|-------------| -| `/backup/create` | Create a backup | +| Procedure | Description | +| -------------------------- | ---------------------------- | +| `/backup/create` | Create a backup | | `/actions/{name}/getInput` | Get input spec for an action | -| `/actions/{name}/run` | Run an action with input | +| `/actions/{name}/run` | Run an action with input | From 73274ef6e05e2afffb1528b05fcc5d432b8ad996 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Tue, 10 Feb 2026 14:45:50 -0700 Subject: [PATCH 08/17] docs: update TODO.md with DerivedAddressInfo design, remove completed tor task --- agents/TODO.md | 96 +++++++++++++++++++++----------------------------- 1 file changed, 41 insertions(+), 55 deletions(-) diff --git a/agents/TODO.md b/agents/TODO.md index e38dbdec4..34adf08c6 100644 --- a/agents/TODO.md +++ b/agents/TODO.md @@ -65,8 +65,26 @@ Pending tasks for AI agents. Remove items when completed. `InterfaceFilter` impl on `NetInfo` use these sets. This model is unintuitive because users think in terms of individual addresses, not gateways. - **New model**: Per-address enable/disable. Each computed address in `hostname_info` gets an `enabled` - field. Users toggle individual addresses on the View page (see Section 6). + **New model**: Per-address enable/disable using `DerivedAddressInfo` on `BindInfo`. Instead of + gateway-level toggles, users toggle individual addresses. The `hostnameInfo` field moves from `Host` + to `BindInfo.addresses` (as the computed `possible` set). + + **`DerivedAddressInfo` struct** (added to `BindInfo`): + + ```rust + pub struct DerivedAddressInfo { + /// User-controlled: private-gateway addresses the user has disabled + pub private_disabled: BTreeSet, + /// User-controlled: public-gateway addresses the user has enabled + pub public_enabled: BTreeSet, + /// COMPUTED by update(): all possible addresses for this binding + pub possible: BTreeSet, + } + ``` + + `DerivedAddressInfo::enabled()` returns `possible` filtered by the two sets: private addresses are + enabled by default (disabled if in `private_disabled`), public addresses are disabled by default + (enabled if in `public_enabled`). Requires `HostnameInfo` to derive `Ord` for `BTreeSet` usage. **How disabling works per address type**: @@ -86,24 +104,20 @@ Pending tasks for AI agents. Remove items when completed. entry** for that hostname. Since hostname-based routing uses SNI (SSL) or Host header (HTTP), removing the entry means the hostname simply doesn't resolve to a backend. No traffic reaches the service for that hostname. - - **Onion addresses**: Disabled by **not creating the Tor hidden service mapping**. The Tor - daemon won't advertise the onion:port, so no traffic arrives. **Backend changes**: - **Remove from `NetInfo`**: Delete the `private_disabled` and `public_enabled` fields entirely. - - **Add to `NetInfo`**: A `disabled` set containing identifiers for addresses the user has explicitly - disabled. The identifier must be stable across network changes — e.g., `(gateway_id, hostname_kind)` - for IP/Local addresses, `(gateway_id, domain_value)` for domain addresses, or `onion_value` for - onion addresses. Exact format TBD at implementation time. + `NetInfo` becomes just `{ assigned_port: Option, assigned_ssl_port: Option }`. + - **Add `addresses: DerivedAddressInfo` to `BindInfo`**: User-controlled sets (`private_disabled`, + `public_enabled`) are preserved across updates; `possible` is recomputed by `update()`. + - **Remove `hostname_info` from `Host`**: Computed addresses now live in `BindInfo.addresses.possible` + instead of being a top-level field on `Host` that was never persisted to the DB. - **Default behavior preserved**: Private-gateway addresses default to enabled, public-gateway - addresses default to disabled. An address is enabled if it's not in the `disabled` set AND either - (a) the gateway is private, or (b) the user has explicitly enabled it. - - **Add `enabled` field to `HostnameInfo`**: The computed `hostname_info` output should include - whether each address is enabled, derived from the `disabled` set during `NetServiceData::update()`. + addresses default to disabled, via the `enabled()` method on `DerivedAddressInfo`. - **Remove `set-gateway-enabled`** RPC endpoint from `binding.rs`. - **Remove `InterfaceFilter` impl for `NetInfo`**: The per-gateway filter logic is replaced by - per-address filtering in `update()`. + per-address filtering derived from `DerivedAddressInfo`. **New RPC endpoint** (`binding.rs`): @@ -114,12 +128,15 @@ Pending tasks for AI agents. Remove items when completed. ```ts interface BindingSetAddressEnabledParams { internalPort: number - addressId: AddressId // identifies a specific HostnameInfo entry - enabled: boolean + address: HostnameInfo // identifies the address directly (no separate AddressId type needed) + enabled: boolean | null // null = reset to default } ``` - Mutates `NetInfo.disabled` and syncs the host. Uses `sync_db` metadata. + Mutates `BindInfo.addresses.private_disabled` / `.public_enabled` based on the address's `public` + field. If `public == true` and enabled, add to `public_enabled`; if disabled, remove. If + `public == false` and enabled, remove from `private_disabled`; if disabled, add. Uses `sync_db` + metadata. This yields two RPC methods: - `server.host.binding.set-address-enabled` @@ -183,16 +200,15 @@ Pending tasks for AI agents. Remove items when completed. #### 6. Frontend: Interfaces Page Overhaul (View/Manage Split) The current interfaces page is a single page showing gateways (with toggle), addresses, public - domains, Tor domains, and private domains. It gets split into two pages: **View** and **Manage**. + domains, and private domains. It gets split into two pages: **View** and **Manage**. **SDK**: `preferredExternalPort` is already exposed. No additional SDK changes needed. ##### View Page Displays all computed addresses for the interface (from `hostname_info`) as a flat list. For each - address, show: URL, type (IPv4, IPv6, .local, domain, onion), access level (public/private), - gateway name, SSL indicator (especially relevant for onion addresses which may have both SSL and - non-SSL entries), enable/disable state, port forward info for public addresses, and a test button + address, show: URL, type (IPv4, IPv6, .local, domain), access level (public/private), + gateway name, SSL indicator, enable/disable state, port forward info for public addresses, and a test button for reachability (see Section 7). No gateway-level toggles. The old `gateways.component.ts` toggle UI is removed. @@ -202,7 +218,7 @@ Pending tasks for AI agents. Remove items when completed. ##### Manage Page - Simple CRUD interface for configuring which addresses exist. Three sections: + Simple CRUD interface for configuring which addresses exist. Two sections: - **Public domains**: Add/remove. Uses existing RPC endpoints: - `{server,package}.host.address.domain.public.add` @@ -210,9 +226,6 @@ Pending tasks for AI agents. Remove items when completed. - **Private domains**: Add/remove. Uses existing RPC endpoints: - `{server,package}.host.address.domain.private.add` - `{server,package}.host.address.domain.private.remove` - - **Onion addresses**: Add/remove. Uses existing RPC endpoints: - - `{server,package}.host.address.onion.add` - - `{server,package}.host.address.onion.remove` ##### Key Frontend Files to Modify @@ -220,7 +233,7 @@ Pending tasks for AI agents. Remove items when completed. |------|--------| | `web/projects/ui/src/app/routes/portal/components/interfaces/` | Overhaul: split into view/manage | | `web/projects/ui/src/app/routes/portal/components/interfaces/gateways.component.ts` | Remove (replaced by per-address toggles on View page) | - | `web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts` | Update `MappedServiceInterface` to use `enabled` field from `HostnameInfo` | + | `web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts` | Update `MappedServiceInterface` to compute enabled addresses from `DerivedAddressInfo` | | `web/projects/ui/src/app/routes/portal/components/interfaces/addresses/` | Refactor for View page with overflow menu (enable/disable) and test buttons | | `web/projects/ui/src/app/routes/portal/routes/services/services.routes.ts` | Add routes for view/manage sub-pages | | `web/projects/ui/src/app/routes/portal/routes/system/system.routes.ts` | Add routes for view/manage sub-pages | @@ -237,7 +250,7 @@ Pending tasks for AI agents. Remove items when completed. ```ts interface BindingTestAddressParams { internalPort: number - addressId: AddressId + address: HostnameInfo } ``` @@ -270,42 +283,15 @@ Pending tasks for AI agents. Remove items when completed. | File | Role | |------|------| | `core/src/net/forward.rs` | `AvailablePorts` — port pool allocation | - | `core/src/net/host/binding.rs` | `BindInfo`/`NetInfo` — remove gateway overrides, add per-address disable set, new RPC endpoints | + | `core/src/net/host/binding.rs` | `BindInfo`/`NetInfo`/`DerivedAddressInfo` — remove gateway overrides, add per-address enable/disable sets, new RPC endpoints | | `core/src/net/net_controller.rs:259` | `NetServiceData::update()` — compute `enabled` on `HostnameInfo`, vhost/forward/DNS reconciliation, 5443 hack removal | | `core/src/net/vhost.rs` | `VHostController` / `ProxyTarget` — source-IP gating for public/private | | `core/src/net/gateway.rs` | `InterfaceFilter` — remove `NetInfo` impl, simplify | - | `core/src/net/service_interface.rs` | `HostnameInfo` — add `enabled` field | + | `core/src/net/service_interface.rs` | `HostnameInfo` — add `Ord` derives for use in `BTreeSet` | | `core/src/net/host/address.rs` | Existing domain/onion CRUD endpoints (no changes needed) | | `sdk/base/lib/interfaces/Host.ts` | SDK `MultiHost.bindPort()` — no changes needed | | `core/src/db/model/public.rs` | Public DB model — port forward mapping | -- [ ] Remove Tor from StartOS core - @dr-bonez - - **Goal**: Remove all built-in Tor functionality from StartOS. Tor will now be provided by a service - running on StartOS rather than being integrated into the core OS. - - **Scope**: Remove the Arti-based Tor client, onion address management in the networking stack, Tor - hidden service creation in the vhost/net controller layers, and any Tor-specific configuration in the - database models. The core should no longer start, manage, or depend on a Tor daemon. - - **Key areas to modify**: - - | Area | Change | - |------|--------| - | `core/src/net/tor/` | Remove the Tor module entirely (Arti client, hidden service management) | - | `core/src/net/net_controller.rs` | Remove Tor hidden service creation/teardown in `update()` | - | `core/src/net/host/address.rs` | Remove onion address CRUD RPC endpoints (`address.onion.add`, `address.onion.remove`) | - | `core/src/net/host/binding.rs` | Remove onion-related fields from `NetInfo` | - | `core/src/net/service_interface.rs` | Remove onion-related variants from `HostnameInfo` | - | `core/src/db/model/` | Remove Tor/onion fields from public and private DB models | - | `sdk/base/lib/interfaces/Host.ts` | Remove onion-related types and options from the SDK | - | `web/projects/ui/` | Remove onion address UI from interfaces pages | - | `Cargo.toml` / dependencies | Remove Arti and related Tor crate dependencies | - - **Migration**: Existing onion address data in the database should be cleaned up during migration. - Services that previously relied on the OS-provided Tor integration will need to use the new Tor - service instead (service-level integration is out of scope for this task). - - [ ] Auto-configure port forwards via UPnP/NAT-PMP/PCP - @dr-bonez **Blocked by**: "Support preferred external ports besides 443" (must be implemented and tested From 4e638fb58e1dc0727f930e08309501e899dabd8f Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Tue, 10 Feb 2026 17:38:51 -0700 Subject: [PATCH 09/17] feat: implement preferred port allocation and per-address enable/disable - Add AvailablePorts::try_alloc() with SSL tracking (BTreeMap) - Add DerivedAddressInfo on BindInfo with private_disabled/public_enabled/possible sets - Add Bindings wrapper with Map impl for patchdb indexed access - Flatten HostAddress from single-variant enum to struct - Replace set-gateway-enabled RPC with set-address-enabled - Remove hostname_info from Host; computed addresses now in BindInfo.addresses.possible - Compute possible addresses inline in NetServiceData::update() - Update DB migration, SDK types, frontend, and container-runtime --- CLAUDE.md | 18 + agents/TODO.md | 123 +--- .../DockerProcedureContainer.ts | 13 +- .../Systems/SystemForEmbassy/index.ts | 4 +- core/src/db/model/public.rs | 54 +- core/src/net/forward.rs | 48 +- core/src/net/gateway.rs | 12 +- core/src/net/host/address.rs | 49 +- core/src/net/host/binding.rs | 193 ++++-- core/src/net/host/mod.rs | 11 +- core/src/net/net_controller.rs | 618 +++++++++--------- core/src/net/service_interface.rs | 6 +- core/src/net/tunnel.rs | 8 +- core/src/service/effects/net/ssl.rs | 8 +- core/src/version/v0_4_0_alpha_20.rs | 89 +-- sdk/base/lib/osBindings/BindInfo.ts | 8 +- ...s.ts => BindingSetAddressEnabledParams.ts} | 5 +- sdk/base/lib/osBindings/Bindings.ts | 4 + sdk/base/lib/osBindings/DerivedAddressInfo.ts | 17 + sdk/base/lib/osBindings/Host.ts | 9 +- sdk/base/lib/osBindings/NetInfo.ts | 3 - sdk/base/lib/osBindings/index.ts | 4 +- sdk/base/lib/util/getServiceInterface.ts | 19 +- .../interfaces/gateways.component.ts | 30 +- .../interfaces/interface.service.ts | 29 +- .../services/routes/interface.component.ts | 4 +- .../routes/startos-ui/startos-ui.component.ts | 4 +- .../ui/src/app/services/api/api.fixures.ts | 156 +++-- .../ui/src/app/services/api/api.types.ts | 18 +- .../app/services/api/embassy-api.service.ts | 12 +- .../services/api/embassy-live-api.service.ts | 16 +- .../services/api/embassy-mock-api.service.ts | 60 +- .../ui/src/app/services/api/mock-patch.ts | 296 ++++----- 33 files changed, 996 insertions(+), 952 deletions(-) rename sdk/base/lib/osBindings/{BindingGatewaySetEnabledParams.ts => BindingSetAddressEnabledParams.ts} (58%) create mode 100644 sdk/base/lib/osBindings/Bindings.ts create mode 100644 sdk/base/lib/osBindings/DerivedAddressInfo.ts diff --git a/CLAUDE.md b/CLAUDE.md index 22d94db31..2ed0a6368 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,6 +29,24 @@ make update-startbox REMOTE=start9@ # Fastest iteration (binary + UI) make test-core # Run Rust tests ``` +### Verifying code changes + +When making changes across multiple layers (Rust, SDK, web, container-runtime), verify in this order: + +1. **Rust**: `cargo check -p start-os` — verifies core compiles +2. **TS bindings**: `make ts-bindings` — regenerates TypeScript types from Rust `#[ts(export)]` structs + - Runs `./core/build/build-ts.sh` to export ts-rs types to `core/bindings/` + - Syncs `core/bindings/` → `sdk/base/lib/osBindings/` via rsync + - If you manually edit files in `sdk/base/lib/osBindings/`, you must still rebuild the SDK (step 3) +3. **SDK bundle**: `cd sdk && make baseDist dist` — compiles SDK source into packages + - `baseDist/` is consumed by `/web` (via `@start9labs/start-sdk-base`) + - `dist/` is consumed by `/container-runtime` (via `@start9labs/start-sdk`) + - Web and container-runtime reference the **built** SDK, not source files +4. **Web type check**: `cd web && npm run check` — type-checks all Angular projects +5. **Container runtime type check**: `cd container-runtime && npm run check` — type-checks the runtime + +**Important**: Editing `sdk/base/lib/osBindings/*.ts` alone is NOT sufficient — you must rebuild the SDK bundle (step 3) before web/container-runtime can see the changes. + ## Architecture ### Core (`/core`) diff --git a/agents/TODO.md b/agents/TODO.md index 34adf08c6..cabae12fb 100644 --- a/agents/TODO.md +++ b/agents/TODO.md @@ -33,114 +33,39 @@ Pending tasks for AI agents. Remove items when completed. `preferred_external_port: 443` won't get 443 as its `assigned_ssl_port` (it's taken), but it CAN still have domain vhost entries on port 443 — SNI routes by hostname. - #### 1. Preferred Port Allocation for Ownership (`forward.rs`, `binding.rs`) + #### 1. Preferred Port Allocation for Ownership ✅ DONE - Expand `AvailablePorts` to support trying a preferred port before falling back to the dynamic range: + `AvailablePorts::try_alloc(port) -> Option` added to `forward.rs`. `BindInfo::new()` and + `BindInfo::update()` attempt the preferred port first, falling back to dynamic-range allocation. - - Add `try_alloc(port) -> Option`: Attempts to exclusively allocate a specific port. Returns - `None` if the port is already allocated or restricted. - - Enforce the restricted port list (currently noted in `vhost.rs:89`: `<=1024, >=32768, 5355, 5432, - 9050, 6010, 9051, 5353`) — skip the preferred port if restricted, except for ports the OS itself - uses (80, 443). - - No SSL-vs-non-SSL distinction or refcounting needed at this layer — ownership is always exclusive. - SSL port sharing for domains is handled entirely by the VHostController via SNI. + #### 2. Per-Address Enable/Disable ✅ DONE - Modify `BindInfo::new()` and `BindInfo::update()` to attempt the preferred port first: + Gateway-level `private_disabled`/`public_enabled` on `NetInfo` replaced with per-address + `DerivedAddressInfo` on `BindInfo`. `hostname_info` removed from `Host` — computed addresses now + live in `BindInfo.addresses.possible`. - ``` - assigned_ssl_port = try_alloc(ssl.preferred_external_port) - .unwrap_or(dynamic_pool.alloc()) - assigned_port = try_alloc(options.preferred_external_port) - .unwrap_or(dynamic_pool.alloc()) - ``` - - After this change, `assigned_ssl_port` may match the preferred port if it was available, or fall back - to the dynamic range as before. - - #### 2. Per-Address Enable/Disable (replaces gateway overrides) - - **Current model being removed**: `NetInfo` has `private_disabled: OrdSet` and - `public_enabled: OrdSet` — gateway-level toggles where private gateways are enabled by - default and public gateways are disabled by default. The `set-gateway-enabled` RPC endpoint and the - `InterfaceFilter` impl on `NetInfo` use these sets. This model is unintuitive because users think in - terms of individual addresses, not gateways. - - **New model**: Per-address enable/disable using `DerivedAddressInfo` on `BindInfo`. Instead of - gateway-level toggles, users toggle individual addresses. The `hostnameInfo` field moves from `Host` - to `BindInfo.addresses` (as the computed `possible` set). - - **`DerivedAddressInfo` struct** (added to `BindInfo`): + **`DerivedAddressInfo` struct** (on `BindInfo`): ```rust pub struct DerivedAddressInfo { - /// User-controlled: private-gateway addresses the user has disabled pub private_disabled: BTreeSet, - /// User-controlled: public-gateway addresses the user has enabled pub public_enabled: BTreeSet, - /// COMPUTED by update(): all possible addresses for this binding - pub possible: BTreeSet, + pub possible: BTreeSet, // COMPUTED by update() } ``` - `DerivedAddressInfo::enabled()` returns `possible` filtered by the two sets: private addresses are - enabled by default (disabled if in `private_disabled`), public addresses are disabled by default - (enabled if in `public_enabled`). Requires `HostnameInfo` to derive `Ord` for `BTreeSet` usage. + `DerivedAddressInfo::enabled()` returns `possible` filtered by the two sets. `HostnameInfo` derives + `Ord` for `BTreeSet` usage. `AddressFilter` (implementing `InterfaceFilter`) derives enabled + gateway set from `DerivedAddressInfo` for vhost/forward filtering. - **How disabling works per address type**: + **RPC endpoint**: `set-gateway-enabled` replaced with `set-address-enabled` (on both + `server.host.binding` and `package.host.binding`). - The enforcement mechanism varies by address type because different addresses are reached through - different network paths: + **How disabling works per address type** (enforcement deferred to Section 3): - - **WAN IP:port** (public gateway IP addresses): Disabled via **source-IP gating** in the vhost - layer (Section 3). Public and private traffic share the same port listener, so we can't just - remove the vhost entry — that would also block private traffic. Instead, the vhost target is - tagged with which source-IP classes it accepts. When a WAN IP address is disabled, the vhost - target rejects connections whose source IP matches the gateway (i.e., NAT'd internet traffic) - or falls outside the gateway's LAN subnets. LAN traffic to the same port is unaffected. - - **LAN IP:port** (private gateway IP addresses): Also enforced via **source-IP gating**. When - disabled, the vhost target rejects connections from LAN subnets on that gateway. This is the - inverse of the WAN case — same mechanism, different source-IP class. + - **WAN/LAN IP:port**: Will be enforced via **source-IP gating** in the vhost layer (Section 3). - **Hostname-based addresses** (`.local`, domains): Disabled by **not creating the vhost/SNI - entry** for that hostname. Since hostname-based routing uses SNI (SSL) or Host header (HTTP), - removing the entry means the hostname simply doesn't resolve to a backend. No traffic reaches - the service for that hostname. - - **Backend changes**: - - - **Remove from `NetInfo`**: Delete the `private_disabled` and `public_enabled` fields entirely. - `NetInfo` becomes just `{ assigned_port: Option, assigned_ssl_port: Option }`. - - **Add `addresses: DerivedAddressInfo` to `BindInfo`**: User-controlled sets (`private_disabled`, - `public_enabled`) are preserved across updates; `possible` is recomputed by `update()`. - - **Remove `hostname_info` from `Host`**: Computed addresses now live in `BindInfo.addresses.possible` - instead of being a top-level field on `Host` that was never persisted to the DB. - - **Default behavior preserved**: Private-gateway addresses default to enabled, public-gateway - addresses default to disabled, via the `enabled()` method on `DerivedAddressInfo`. - - **Remove `set-gateway-enabled`** RPC endpoint from `binding.rs`. - - **Remove `InterfaceFilter` impl for `NetInfo`**: The per-gateway filter logic is replaced by - per-address filtering derived from `DerivedAddressInfo`. - - **New RPC endpoint** (`binding.rs`): - - Following the existing `HostApiKind` pattern, replace `set-gateway-enabled` with: - - - **`set-address-enabled`** — Toggle an individual address on or off. - - ```ts - interface BindingSetAddressEnabledParams { - internalPort: number - address: HostnameInfo // identifies the address directly (no separate AddressId type needed) - enabled: boolean | null // null = reset to default - } - ``` - - Mutates `BindInfo.addresses.private_disabled` / `.public_enabled` based on the address's `public` - field. If `public == true` and enabled, add to `public_enabled`; if disabled, remove. If - `public == false` and enabled, remove from `private_disabled`; if disabled, add. Uses `sync_db` - metadata. - - This yields two RPC methods: - - `server.host.binding.set-address-enabled` - - `package.host.binding.set-address-enabled` + entry** for that hostname. #### 3. Eliminate the Port 5443 Hack: Source-IP-Based WAN Blocking (`vhost.rs`, `net_controller.rs`) @@ -206,7 +131,7 @@ Pending tasks for AI agents. Remove items when completed. ##### View Page - Displays all computed addresses for the interface (from `hostname_info`) as a flat list. For each + Displays all computed addresses for the interface (from `BindInfo.addresses`) as a flat list. For each address, show: URL, type (IPv4, IPv6, .local, domain), access level (public/private), gateway name, SSL indicator, enable/disable state, port forward info for public addresses, and a test button for reachability (see Section 7). @@ -282,13 +207,13 @@ Pending tasks for AI agents. Remove items when completed. | File | Role | |------|------| - | `core/src/net/forward.rs` | `AvailablePorts` — port pool allocation | - | `core/src/net/host/binding.rs` | `BindInfo`/`NetInfo`/`DerivedAddressInfo` — remove gateway overrides, add per-address enable/disable sets, new RPC endpoints | - | `core/src/net/net_controller.rs:259` | `NetServiceData::update()` — compute `enabled` on `HostnameInfo`, vhost/forward/DNS reconciliation, 5443 hack removal | + | `core/src/net/forward.rs` | `AvailablePorts` — port pool allocation, `try_alloc()` for preferred ports | + | `core/src/net/host/binding.rs` | `Bindings` (Map wrapper for patchdb), `BindInfo`/`NetInfo`/`DerivedAddressInfo`/`AddressFilter` — per-address enable/disable, `set-address-enabled` RPC | + | `core/src/net/net_controller.rs:259` | `NetServiceData::update()` — computes `DerivedAddressInfo.possible`, vhost/forward/DNS reconciliation, 5443 hack removal | | `core/src/net/vhost.rs` | `VHostController` / `ProxyTarget` — source-IP gating for public/private | - | `core/src/net/gateway.rs` | `InterfaceFilter` — remove `NetInfo` impl, simplify | - | `core/src/net/service_interface.rs` | `HostnameInfo` — add `Ord` derives for use in `BTreeSet` | - | `core/src/net/host/address.rs` | Existing domain/onion CRUD endpoints (no changes needed) | + | `core/src/net/gateway.rs` | `InterfaceFilter` trait and filter types (`AddressFilter`, `PublicFilter`, etc.) | + | `core/src/net/service_interface.rs` | `HostnameInfo` — derives `Ord` for `BTreeSet` usage | + | `core/src/net/host/address.rs` | `HostAddress` (flattened struct), domain CRUD endpoints | | `sdk/base/lib/interfaces/Host.ts` | SDK `MultiHost.bindPort()` — no changes needed | | `core/src/db/model/public.rs` | Public DB model — port forward mapping | diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts index b6fd6f033..1a8975317 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts @@ -82,17 +82,14 @@ export class DockerProcedureContainer extends Drop { }), ) } else if (volumeMount.type === "certificate") { + const hostInfo = await effects.getHostInfo({ + hostId: volumeMount["interface-id"], + }) const hostnames = [ `${packageId}.embassy`, ...new Set( - Object.values( - ( - await effects.getHostInfo({ - hostId: volumeMount["interface-id"], - }) - )?.hostnameInfo || {}, - ) - .flatMap((h) => h) + Object.values(hostInfo?.bindings || {}) + .flatMap((b) => b.addresses.possible) .map((h) => h.hostname.value), ).values(), ] diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index 82dc037f9..cb7585ac9 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -1244,8 +1244,8 @@ async function updateConfig( ? "" : catchFn( () => - filled.addressInfo!.filter({ kind: "mdns" })! - .hostnames[0].hostname.value, + filled.addressInfo!.filter({ kind: "mdns" })!.hostnames[0] + .hostname.value, ) || "" mutConfigValue[key] = url } diff --git a/core/src/db/model/public.rs b/core/src/db/model/public.rs index d038577a6..cd2dc2169 100644 --- a/core/src/db/model/public.rs +++ b/core/src/db/model/public.rs @@ -20,7 +20,7 @@ use crate::db::model::Database; use crate::db::model::package::AllPackageData; use crate::net::acme::AcmeProvider; use crate::net::host::Host; -use crate::net::host::binding::{AddSslOptions, BindInfo, BindOptions, NetInfo}; +use crate::net::host::binding::{AddSslOptions, BindInfo, BindOptions, Bindings, DerivedAddressInfo, NetInfo}; use crate::net::utils::ipv6_is_local; use crate::net::vhost::AlpnInfo; use crate::prelude::*; @@ -63,35 +63,35 @@ impl Public { post_init_migration_todos: BTreeMap::new(), network: NetworkInfo { host: Host { - bindings: [( - 80, - BindInfo { - enabled: false, - options: BindOptions { - preferred_external_port: 80, - add_ssl: Some(AddSslOptions { - preferred_external_port: 443, - add_x_forwarded_headers: false, - alpn: Some(AlpnInfo::Specified(vec![ - MaybeUtf8String("h2".into()), - MaybeUtf8String("http/1.1".into()), - ])), - }), - secure: None, + bindings: Bindings( + [( + 80, + BindInfo { + enabled: false, + options: BindOptions { + preferred_external_port: 80, + add_ssl: Some(AddSslOptions { + preferred_external_port: 443, + add_x_forwarded_headers: false, + alpn: Some(AlpnInfo::Specified(vec![ + MaybeUtf8String("h2".into()), + MaybeUtf8String("http/1.1".into()), + ])), + }), + secure: None, + }, + net: NetInfo { + assigned_port: None, + assigned_ssl_port: Some(443), + }, + addresses: DerivedAddressInfo::default(), }, - net: NetInfo { - assigned_port: None, - assigned_ssl_port: Some(443), - private_disabled: OrdSet::new(), - public_enabled: OrdSet::new(), - }, - }, - )] - .into_iter() - .collect(), + )] + .into_iter() + .collect(), + ), public_domains: BTreeMap::new(), private_domains: BTreeSet::new(), - hostname_info: BTreeMap::new(), }, wifi: WifiInfo { enabled: true, diff --git a/core/src/net/forward.rs b/core/src/net/forward.rs index b18ed7f1b..7cf5e7985 100644 --- a/core/src/net/forward.rs +++ b/core/src/net/forward.rs @@ -4,8 +4,8 @@ use std::sync::{Arc, Weak}; use std::time::Duration; use futures::channel::oneshot; -use id_pool::IdPool; use iddqd::{IdOrdItem, IdOrdMap}; +use rand::Rng; use imbl::OrdMap; use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async}; use serde::{Deserialize, Serialize}; @@ -23,25 +23,49 @@ use crate::util::serde::{HandlerExtSerde, display_serializable}; use crate::util::sync::Watch; pub const START9_BRIDGE_IFACE: &str = "lxcbr0"; -pub const FIRST_DYNAMIC_PRIVATE_PORT: u16 = 49152; +const EPHEMERAL_PORT_START: u16 = 49152; +// vhost.rs:89 — not allowed: <=1024, >=32768, 5355, 5432, 9050, 6010, 9051, 5353 +const RESTRICTED_PORTS: &[u16] = &[5353, 5355, 5432, 6010, 9050, 9051]; + +fn is_restricted(port: u16) -> bool { + port <= 1024 || RESTRICTED_PORTS.contains(&port) +} #[derive(Debug, Deserialize, Serialize)] -pub struct AvailablePorts(IdPool); +pub struct AvailablePorts(BTreeMap); impl AvailablePorts { pub fn new() -> Self { - Self(IdPool::new_ranged(FIRST_DYNAMIC_PRIVATE_PORT..u16::MAX)) + Self(BTreeMap::new()) } - pub fn alloc(&mut self) -> Result { - self.0.request_id().ok_or_else(|| { - Error::new( - eyre!("{}", t!("net.forward.no-dynamic-ports-available")), - ErrorKind::Network, - ) - }) + pub fn alloc(&mut self, ssl: bool) -> Result { + let mut rng = rand::rng(); + for _ in 0..1000 { + let port = rng.random_range(EPHEMERAL_PORT_START..u16::MAX); + if !self.0.contains_key(&port) { + self.0.insert(port, ssl); + return Ok(port); + } + } + Err(Error::new( + eyre!("{}", t!("net.forward.no-dynamic-ports-available")), + ErrorKind::Network, + )) + } + /// Try to allocate a specific port. Returns Some(port) if available, None if taken/restricted. + pub fn try_alloc(&mut self, port: u16, ssl: bool) -> Option { + if is_restricted(port) || self.0.contains_key(&port) { + return None; + } + self.0.insert(port, ssl); + Some(port) + } + /// Returns whether a given allocated port is SSL. + pub fn is_ssl(&self, port: u16) -> bool { + self.0.get(&port).copied().unwrap_or(false) } pub fn free(&mut self, ports: impl IntoIterator) { for port in ports { - self.0.return_id(port).unwrap_or_default(); + self.0.remove(&port); } } } diff --git a/core/src/net/gateway.rs b/core/src/net/gateway.rs index 6079efd76..688892f85 100644 --- a/core/src/net/gateway.rs +++ b/core/src/net/gateway.rs @@ -1685,19 +1685,9 @@ where #[test] fn test_filter() { - use crate::net::host::binding::NetInfo; let wg1 = "wg1".parse::().unwrap(); assert!(!InterfaceFilter::filter( - &AndFilter( - NetInfo { - private_disabled: [wg1.clone()].into_iter().collect(), - public_enabled: Default::default(), - assigned_port: None, - assigned_ssl_port: None, - }, - AndFilter(IdFilter(wg1.clone()), PublicFilter { public: false }), - ) - .into_dyn(), + &AndFilter(IdFilter(wg1.clone()), PublicFilter { public: false }).into_dyn(), &wg1, &NetworkInterfaceInfo { name: None, diff --git a/core/src/net/host/address.rs b/core/src/net/host/address.rs index 59f74fdb8..33c32073b 100644 --- a/core/src/net/host/address.rs +++ b/core/src/net/host/address.rs @@ -16,15 +16,11 @@ use crate::prelude::*; use crate::util::serde::{HandlerExtSerde, display_serializable}; #[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -#[serde(rename_all_fields = "camelCase")] -#[serde(tag = "kind")] -pub enum HostAddress { - Domain { - address: InternedString, - public: Option, - private: bool, - }, +#[serde(rename_all = "camelCase")] +pub struct HostAddress { + pub address: InternedString, + pub public: Option, + pub private: bool, } #[derive(Debug, Clone, Deserialize, Serialize, TS)] @@ -151,29 +147,18 @@ pub fn address_api() let mut table = Table::new(); table.add_row(row![bc => "ADDRESS", "PUBLIC", "ACME PROVIDER"]); - for address in &res { - match address { - HostAddress::Domain { - address, - public: Some(PublicDomainConfig { gateway, acme }), - private, - } => { - table.add_row(row![ - address, - &format!( - "{} ({gateway})", - if *private { "YES" } else { "ONLY" } - ), - acme.as_ref().map(|a| a.0.as_str()).unwrap_or("NONE") - ]); - } - HostAddress::Domain { - address, - public: None, - .. - } => { - table.add_row(row![address, &format!("NO"), "N/A"]); - } + for entry in &res { + if let Some(PublicDomainConfig { gateway, acme }) = &entry.public { + table.add_row(row![ + entry.address, + &format!( + "{} ({gateway})", + if entry.private { "YES" } else { "ONLY" } + ), + acme.as_ref().map(|a| a.0.as_str()).unwrap_or("NONE") + ]); + } else { + table.add_row(row![entry.address, &format!("NO"), "N/A"]); } } diff --git a/core/src/net/host/binding.rs b/core/src/net/host/binding.rs index 8862e2bda..3c78c4338 100644 --- a/core/src/net/host/binding.rs +++ b/core/src/net/host/binding.rs @@ -3,16 +3,17 @@ use std::str::FromStr; use clap::Parser; use clap::builder::ValueParserFactory; -use imbl::OrdSet; use rpc_toolkit::{Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async}; use serde::{Deserialize, Serialize}; use ts_rs::TS; use crate::context::{CliContext, RpcContext}; use crate::db::model::public::NetworkInterfaceInfo; +use crate::db::prelude::Map; use crate::net::forward::AvailablePorts; use crate::net::gateway::InterfaceFilter; use crate::net::host::HostApiKind; +use crate::net::service_interface::HostnameInfo; use crate::net::vhost::AlpnInfo; use crate::prelude::*; use crate::util::FromStrParser; @@ -45,51 +46,137 @@ impl FromStr for BindId { } } -#[derive(Debug, Deserialize, Serialize, TS)] +#[derive(Debug, Default, Clone, Deserialize, Serialize, TS, HasModel)] #[serde(rename_all = "camelCase")] #[ts(export)] +#[model = "Model"] +pub struct DerivedAddressInfo { + /// User-controlled: private-gateway addresses the user has disabled + pub private_disabled: BTreeSet, + /// User-controlled: public-gateway addresses the user has enabled + pub public_enabled: BTreeSet, + /// COMPUTED: NetServiceData::update — all possible addresses for this binding + pub possible: BTreeSet, +} + +impl DerivedAddressInfo { + /// Returns addresses that are currently enabled. + /// Private addresses are enabled by default (disabled if in private_disabled). + /// Public addresses are disabled by default (enabled if in public_enabled). + pub fn enabled(&self) -> BTreeSet<&HostnameInfo> { + self.possible + .iter() + .filter(|h| { + if h.public { + self.public_enabled.contains(h) + } else { + !self.private_disabled.contains(h) + } + }) + .collect() + } + + /// Derive a gateway-level InterfaceFilter from the enabled addresses. + /// A gateway passes the filter if it has any enabled address for this binding. + pub fn gateway_filter(&self) -> AddressFilter { + let enabled_gateways: BTreeSet = self + .enabled() + .into_iter() + .map(|h| h.gateway.id.clone()) + .collect(); + AddressFilter(enabled_gateways) + } +} + +/// Gateway-level filter derived from DerivedAddressInfo. +/// Passes if the gateway has at least one enabled address. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct AddressFilter(pub BTreeSet); +impl InterfaceFilter for AddressFilter { + fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool { + info.ip_info.is_some() && self.0.contains(id) + } +} + +#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] +#[model = "Model"] +#[ts(export)] +pub struct Bindings(pub BTreeMap); + +impl Map for Bindings { + type Key = u16; + type Value = BindInfo; + fn key_str(key: &Self::Key) -> Result, Error> { + Self::key_string(key) + } + fn key_string(key: &Self::Key) -> Result { + Ok(InternedString::from_display(key)) + } +} + +impl std::ops::Deref for Bindings { + type Target = BTreeMap; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for Bindings { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] pub struct BindInfo { pub enabled: bool, pub options: BindOptions, pub net: NetInfo, + pub addresses: DerivedAddressInfo, } #[derive(Clone, Debug, Deserialize, Serialize, TS, PartialEq, Eq, PartialOrd, Ord)] #[serde(rename_all = "camelCase")] #[ts(export)] pub struct NetInfo { - #[ts(as = "BTreeSet::")] - #[serde(default)] - pub private_disabled: OrdSet, - #[ts(as = "BTreeSet::")] - #[serde(default)] - pub public_enabled: OrdSet, pub assigned_port: Option, pub assigned_ssl_port: Option, } +impl InterfaceFilter for NetInfo { + fn filter(&self, _id: &GatewayId, info: &NetworkInterfaceInfo) -> bool { + info.ip_info.is_some() + } +} + impl BindInfo { pub fn new(available_ports: &mut AvailablePorts, options: BindOptions) -> Result { let mut assigned_port = None; let mut assigned_ssl_port = None; - if options.add_ssl.is_some() { - assigned_ssl_port = Some(available_ports.alloc()?); + if let Some(ssl) = &options.add_ssl { + assigned_ssl_port = available_ports + .try_alloc(ssl.preferred_external_port, true) + .or_else(|| Some(available_ports.alloc(true).ok()?)); } if options .secure .map_or(true, |s| !(s.ssl && options.add_ssl.is_some())) { - assigned_port = Some(available_ports.alloc()?); + assigned_port = available_ports + .try_alloc(options.preferred_external_port, false) + .or_else(|| Some(available_ports.alloc(false).ok()?)); } Ok(Self { enabled: true, options, net: NetInfo { - private_disabled: OrdSet::new(), - public_enabled: OrdSet::new(), assigned_port, assigned_ssl_port, }, + addresses: DerivedAddressInfo::default(), }) } pub fn update( @@ -97,7 +184,11 @@ impl BindInfo { available_ports: &mut AvailablePorts, options: BindOptions, ) -> Result { - let Self { net: mut lan, .. } = self; + let Self { + net: mut lan, + addresses, + .. + } = self; if options .secure .map_or(true, |s| !(s.ssl && options.add_ssl.is_some())) @@ -105,19 +196,26 @@ impl BindInfo { { lan.assigned_port = if let Some(port) = lan.assigned_port.take() { Some(port) + } else if let Some(port) = + available_ports.try_alloc(options.preferred_external_port, false) + { + Some(port) } else { - Some(available_ports.alloc()?) + Some(available_ports.alloc(false)?) }; } else { if let Some(port) = lan.assigned_port.take() { available_ports.free([port]); } } - if options.add_ssl.is_some() { + if let Some(ssl) = &options.add_ssl { lan.assigned_ssl_port = if let Some(port) = lan.assigned_ssl_port.take() { Some(port) + } else if let Some(port) = available_ports.try_alloc(ssl.preferred_external_port, true) + { + Some(port) } else { - Some(available_ports.alloc()?) + Some(available_ports.alloc(true)?) }; } else { if let Some(port) = lan.assigned_ssl_port.take() { @@ -128,22 +226,17 @@ impl BindInfo { enabled: true, options, net: lan, + addresses: DerivedAddressInfo { + private_disabled: addresses.private_disabled, + public_enabled: addresses.public_enabled, + possible: BTreeSet::new(), + }, }) } pub fn disable(&mut self) { self.enabled = false; } } -impl InterfaceFilter for NetInfo { - fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool { - info.ip_info.is_some() - && if info.public() { - self.public_enabled.contains(id) - } else { - !self.private_disabled.contains(id) - } - } -} #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, TS)] #[ts(export)] @@ -188,7 +281,7 @@ pub fn binding() let mut table = Table::new(); table.add_row(row![bc => "INTERNAL PORT", "ENABLED", "EXTERNAL PORT", "EXTERNAL SSL PORT"]); - for (internal, info) in res { + for (internal, info) in res.iter() { table.add_row(row![ internal, info.enabled, @@ -213,12 +306,12 @@ pub fn binding() .with_call_remote::(), ) .subcommand( - "set-gateway-enabled", - from_fn_async(set_gateway_enabled::) + "set-address-enabled", + from_fn_async(set_address_enabled::) .with_metadata("sync_db", Value::Bool(true)) .with_inherited(Kind::inheritance) .no_display() - .with_about("about.set-gateway-enabled-for-binding") + .with_about("about.set-address-enabled-for-binding") .with_call_remote::(), ) } @@ -227,7 +320,7 @@ pub async fn list_bindings( ctx: RpcContext, _: Empty, inheritance: Kind::Inheritance, -) -> Result, Error> { +) -> Result { Kind::host_for(&inheritance, &mut ctx.db.peek().await)? .as_bindings() .de() @@ -236,50 +329,44 @@ pub async fn list_bindings( #[derive(Deserialize, Serialize, Parser, TS)] #[serde(rename_all = "camelCase")] #[ts(export)] -pub struct BindingGatewaySetEnabledParams { +pub struct BindingSetAddressEnabledParams { #[arg(help = "help.arg.internal-port")] internal_port: u16, - #[arg(help = "help.arg.gateway-id")] - gateway: GatewayId, + #[arg(long, help = "help.arg.address")] + address: String, #[arg(long, help = "help.arg.binding-enabled")] enabled: Option, } -pub async fn set_gateway_enabled( +pub async fn set_address_enabled( ctx: RpcContext, - BindingGatewaySetEnabledParams { + BindingSetAddressEnabledParams { internal_port, - gateway, + address, enabled, - }: BindingGatewaySetEnabledParams, + }: BindingSetAddressEnabledParams, inheritance: Kind::Inheritance, ) -> Result<(), Error> { let enabled = enabled.unwrap_or(true); - let gateway_public = ctx - .net_controller - .net_iface - .watcher - .ip_info() - .get(&gateway) - .or_not_found(&gateway)? - .public(); + let address: HostnameInfo = + serde_json::from_str(&address).with_kind(ErrorKind::Deserialization)?; ctx.db .mutate(|db| { Kind::host_for(&inheritance, db)? .as_bindings_mut() .mutate(|b| { - let net = &mut b.get_mut(&internal_port).or_not_found(internal_port)?.net; - if gateway_public { + let bind = b.get_mut(&internal_port).or_not_found(internal_port)?; + if address.public { if enabled { - net.public_enabled.insert(gateway); + bind.addresses.public_enabled.insert(address.clone()); } else { - net.public_enabled.remove(&gateway); + bind.addresses.public_enabled.remove(&address); } } else { if enabled { - net.private_disabled.remove(&gateway); + bind.addresses.private_disabled.remove(&address); } else { - net.private_disabled.insert(gateway); + bind.addresses.private_disabled.insert(address.clone()); } } Ok(()) diff --git a/core/src/net/host/mod.rs b/core/src/net/host/mod.rs index 6fdd65d92..ef06313dd 100644 --- a/core/src/net/host/mod.rs +++ b/core/src/net/host/mod.rs @@ -13,8 +13,7 @@ use crate::context::RpcContext; use crate::db::model::DatabaseModel; use crate::net::forward::AvailablePorts; use crate::net::host::address::{HostAddress, PublicDomainConfig, address_api}; -use crate::net::host::binding::{BindInfo, BindOptions, binding}; -use crate::net::service_interface::HostnameInfo; +use crate::net::host::binding::{BindInfo, BindOptions, Bindings, binding}; use crate::prelude::*; use crate::{HostId, PackageId}; @@ -26,11 +25,9 @@ pub mod binding; #[model = "Model"] #[ts(export)] pub struct Host { - pub bindings: BTreeMap, + pub bindings: Bindings, pub public_domains: BTreeMap, pub private_domains: BTreeSet, - /// COMPUTED: NetService::update - pub hostname_info: BTreeMap>, // internal port -> Hostnames } impl AsRef for Host { @@ -45,7 +42,7 @@ impl Host { pub fn addresses<'a>(&'a self) -> impl Iterator + 'a { self.public_domains .iter() - .map(|(address, config)| HostAddress::Domain { + .map(|(address, config)| HostAddress { address: address.clone(), public: Some(config.clone()), private: self.private_domains.contains(address), @@ -54,7 +51,7 @@ impl Host { self.private_domains .iter() .filter(|a| !self.public_domains.contains_key(*a)) - .map(|address| HostAddress::Domain { + .map(|address| HostAddress { address: address.clone(), public: None, private: true, diff --git a/core/src/net/net_controller.rs b/core/src/net/net_controller.rs index 06cc087b5..56c30ed4e 100644 --- a/core/src/net/net_controller.rs +++ b/core/src/net/net_controller.rs @@ -18,8 +18,8 @@ use crate::hostname::Hostname; use crate::net::dns::DnsController; use crate::net::forward::{InterfacePortForwardController, START9_BRIDGE_IFACE, add_iptables_rule}; use crate::net::gateway::{ - AndFilter, DynInterfaceFilter, IdFilter, InterfaceFilter, NetworkInterfaceController, OrFilter, - PublicFilter, SecureFilter, + AndFilter, AnyFilter, DynInterfaceFilter, IdFilter, InterfaceFilter, + NetworkInterfaceController, OrFilter, PublicFilter, SecureFilter, }; use crate::net::host::address::HostAddress; use crate::net::host::binding::{AddSslOptions, BindId, BindOptions}; @@ -202,7 +202,7 @@ impl NetServiceData { .as_entries_mut()? { host.as_bindings_mut().mutate(|b| { - for (internal_port, info) in b { + for (internal_port, info) in b.iter_mut() { if !except.contains(&BindId { id: host_id.clone(), internal_port: *internal_port, @@ -233,7 +233,7 @@ impl NetServiceData { .as_network_mut() .as_host_mut(); host.as_bindings_mut().mutate(|b| { - for (internal_port, info) in b { + for (internal_port, info) in b.iter_mut() { if !except.contains(&BindId { id: HostId::default(), internal_port: *internal_port, @@ -251,11 +251,15 @@ impl NetServiceData { } } - async fn update(&mut self, ctrl: &NetController, id: HostId, host: Host) -> Result<(), Error> { + async fn update( + &mut self, + ctrl: &NetController, + id: HostId, + mut host: Host, + ) -> Result<(), Error> { let mut forwards: BTreeMap = BTreeMap::new(); let mut vhosts: BTreeMap<(Option, u16), ProxyTarget> = BTreeMap::new(); let mut private_dns: BTreeSet = BTreeSet::new(); - let mut hostname_info: BTreeMap> = BTreeMap::new(); let binds = self.binds.entry(id.clone()).or_default(); let peek = ctrl.db.peek().await; @@ -264,324 +268,327 @@ impl NetServiceData { let server_info = peek.as_public().as_server_info(); let net_ifaces = ctrl.net_iface.watcher.ip_info(); let hostname = server_info.as_hostname().de()?; - for (port, bind) in &host.bindings { + let host_addresses: Vec<_> = host.addresses().collect(); + for (port, bind) in host.bindings.iter_mut() { if !bind.enabled { continue; } - if bind.net.assigned_port.is_some() || bind.net.assigned_ssl_port.is_some() { - let mut hostnames = BTreeSet::new(); - if let Some(ssl) = &bind.options.add_ssl { - let external = bind - .net - .assigned_ssl_port - .or_not_found("assigned ssl port")?; - let addr = (self.ip, *port).into(); - let connect_ssl = if let Some(alpn) = ssl.alpn.clone() { - Err(alpn) + if bind.net.assigned_port.is_none() && bind.net.assigned_ssl_port.is_none() { + continue; + } + let mut hostnames = BTreeSet::new(); + let mut gw_filter = AnyFilter( + [PublicFilter { public: false }.into_dyn()] + .into_iter() + .chain( + bind.addresses + .public_enabled + .iter() + .map(|a| a.gateway.id.clone()) + .collect::>() + .into_iter() + .map(IdFilter) + .map(InterfaceFilter::into_dyn), + ) + .collect(), + ); + if let Some(ssl) = &bind.options.add_ssl { + let external = bind + .net + .assigned_ssl_port + .or_not_found("assigned ssl port")?; + let addr = (self.ip, *port).into(); + let connect_ssl = if let Some(alpn) = ssl.alpn.clone() { + Err(alpn) + } else { + if bind.options.secure.as_ref().map_or(false, |s| s.ssl) { + Ok(()) } else { - if bind.options.secure.as_ref().map_or(false, |s| s.ssl) { - Ok(()) - } else { - Err(AlpnInfo::Reflect) - } - }; - for hostname in ctrl.server_hostnames.iter().cloned() { - vhosts.insert( - (hostname, external), - ProxyTarget { - filter: bind.net.clone().into_dyn(), - acme: None, - addr, - add_x_forwarded_headers: ssl.add_x_forwarded_headers, - connect_ssl: connect_ssl - .clone() - .map(|_| ctrl.tls_client_config.clone()), - }, - ); + Err(AlpnInfo::Reflect) } - for address in host.addresses() { - match address { - HostAddress::Domain { - address, - public, - private, - } => { - if hostnames.insert(address.clone()) { - let address = Some(address.clone()); - if ssl.preferred_external_port == 443 { - if let Some(public) = &public { - vhosts.insert( - (address.clone(), 5443), - ProxyTarget { - filter: AndFilter( - bind.net.clone(), - AndFilter( - IdFilter(public.gateway.clone()), - PublicFilter { public: false }, - ), - ) - .into_dyn(), - acme: public.acme.clone(), - addr, - add_x_forwarded_headers: ssl - .add_x_forwarded_headers, - connect_ssl: connect_ssl - .clone() - .map(|_| ctrl.tls_client_config.clone()), - }, - ); - vhosts.insert( - (address.clone(), 443), - ProxyTarget { - filter: AndFilter( - bind.net.clone(), - if private { - OrFilter( - IdFilter(public.gateway.clone()), - PublicFilter { public: false }, - ) - .into_dyn() - } else { - AndFilter( - IdFilter(public.gateway.clone()), - PublicFilter { public: true }, - ) - .into_dyn() - }, - ) - .into_dyn(), - acme: public.acme.clone(), - addr, - add_x_forwarded_headers: ssl - .add_x_forwarded_headers, - connect_ssl: connect_ssl - .clone() - .map(|_| ctrl.tls_client_config.clone()), - }, - ); - } else { - vhosts.insert( - (address.clone(), 443), - ProxyTarget { - filter: AndFilter( - bind.net.clone(), - PublicFilter { public: false }, - ) - .into_dyn(), - acme: None, - addr, - add_x_forwarded_headers: ssl - .add_x_forwarded_headers, - connect_ssl: connect_ssl - .clone() - .map(|_| ctrl.tls_client_config.clone()), - }, - ); - } - } else { - if let Some(public) = public { - vhosts.insert( - (address.clone(), external), - ProxyTarget { - filter: AndFilter( - bind.net.clone(), - if private { - OrFilter( - IdFilter(public.gateway.clone()), - PublicFilter { public: false }, - ) - .into_dyn() - } else { - IdFilter(public.gateway.clone()) - .into_dyn() - }, - ) - .into_dyn(), - acme: public.acme.clone(), - addr, - add_x_forwarded_headers: ssl - .add_x_forwarded_headers, - connect_ssl: connect_ssl - .clone() - .map(|_| ctrl.tls_client_config.clone()), - }, - ); - } else { - vhosts.insert( - (address.clone(), external), - ProxyTarget { - filter: AndFilter( - bind.net.clone(), - PublicFilter { public: false }, - ) - .into_dyn(), - acme: None, - addr, - add_x_forwarded_headers: ssl - .add_x_forwarded_headers, - connect_ssl: connect_ssl - .clone() - .map(|_| ctrl.tls_client_config.clone()), - }, - ); - } - } - } - } - } - } - } - if bind - .options - .secure - .map_or(true, |s| !(s.ssl && bind.options.add_ssl.is_some())) - { - let external = bind.net.assigned_port.or_not_found("assigned lan port")?; - forwards.insert( - external, - ( - SocketAddrV4::new(self.ip, *port), - AndFilter( - SecureFilter { - secure: bind.options.secure.is_some(), - }, - bind.net.clone(), - ) - .into_dyn(), - ), + }; + for hostname in ctrl.server_hostnames.iter().cloned() { + vhosts.insert( + (hostname, external), + ProxyTarget { + filter: gw_filter.clone().into_dyn(), + acme: None, + addr, + add_x_forwarded_headers: ssl.add_x_forwarded_headers, + connect_ssl: connect_ssl + .clone() + .map(|_| ctrl.tls_client_config.clone()), + }, // TODO: allow public traffic? ); } - let mut bind_hostname_info: Vec = - hostname_info.remove(port).unwrap_or_default(); - for (gateway_id, info) in net_ifaces - .iter() - .filter(|(_, info)| { - info.ip_info.as_ref().map_or(false, |i| { - !matches!(i.device_type, Some(NetworkInterfaceType::Bridge)) - }) - }) - .filter(|(id, info)| bind.net.filter(id, info)) + for HostAddress { + address, + public, + private, + } in host_addresses.iter().cloned() { - let gateway = GatewayInfo { - id: gateway_id.clone(), - name: info - .name - .clone() - .or_else(|| info.ip_info.as_ref().map(|i| i.name.clone())) - .unwrap_or_else(|| gateway_id.clone().into()), - public: info.public(), - }; - let port = bind.net.assigned_port.filter(|_| { - bind.options.secure.map_or(false, |s| { - !(s.ssl && bind.options.add_ssl.is_some()) || info.secure() - }) - }); - if !info.public() - && info.ip_info.as_ref().map_or(false, |i| { - i.device_type != Some(NetworkInterfaceType::Wireguard) - }) - { - bind_hostname_info.push(HostnameInfo { - gateway: gateway.clone(), - public: false, - hostname: IpHostname::Local { - value: InternedString::from_display(&{ - let hostname = &hostname; - lazy_format!("{hostname}.local") - }), - port, - ssl_port: bind.net.assigned_ssl_port, - }, - }); - } - for address in host.addresses() { - if let HostAddress::Domain { - address, - public, - private, - } = address - { - if public.is_none() { - private_dns.insert(address.clone()); + if hostnames.insert(address.clone()) { + let address = Some(address.clone()); + if ssl.preferred_external_port == 443 { + if let Some(public) = &public { + vhosts.insert( + (address.clone(), 5443), + ProxyTarget { + filter: AndFilter( + bind.net.clone(), + AndFilter( + IdFilter(public.gateway.clone()), + PublicFilter { public: false }, + ), + ) + .into_dyn(), + acme: public.acme.clone(), + addr, + add_x_forwarded_headers: ssl.add_x_forwarded_headers, + connect_ssl: connect_ssl + .clone() + .map(|_| ctrl.tls_client_config.clone()), + }, + ); + vhosts.insert( + (address.clone(), 443), + ProxyTarget { + filter: AndFilter( + bind.net.clone(), + if private { + OrFilter( + IdFilter(public.gateway.clone()), + PublicFilter { public: false }, + ) + .into_dyn() + } else { + AndFilter( + IdFilter(public.gateway.clone()), + PublicFilter { public: true }, + ) + .into_dyn() + }, + ) + .into_dyn(), + acme: public.acme.clone(), + addr, + add_x_forwarded_headers: ssl.add_x_forwarded_headers, + connect_ssl: connect_ssl + .clone() + .map(|_| ctrl.tls_client_config.clone()), + }, + ); + } else { + vhosts.insert( + (address.clone(), 443), + ProxyTarget { + filter: AndFilter( + bind.net.clone(), + PublicFilter { public: false }, + ) + .into_dyn(), + acme: None, + addr, + add_x_forwarded_headers: ssl.add_x_forwarded_headers, + connect_ssl: connect_ssl + .clone() + .map(|_| ctrl.tls_client_config.clone()), + }, + ); } - let private = private && !info.public(); - let public = - public.as_ref().map_or(false, |p| &p.gateway == gateway_id); - if public || private { - if bind - .options - .add_ssl - .as_ref() - .map_or(false, |ssl| ssl.preferred_external_port == 443) - { - bind_hostname_info.push(HostnameInfo { - gateway: gateway.clone(), - public, - hostname: IpHostname::Domain { - value: address.clone(), - port: None, - ssl_port: Some(443), - }, - }); - } else { - bind_hostname_info.push(HostnameInfo { - gateway: gateway.clone(), - public, - hostname: IpHostname::Domain { - value: address.clone(), - port, - ssl_port: bind.net.assigned_ssl_port, - }, - }); - } + } else { + if let Some(public) = public { + vhosts.insert( + (address.clone(), external), + ProxyTarget { + filter: AndFilter( + bind.net.clone(), + if private { + OrFilter( + IdFilter(public.gateway.clone()), + PublicFilter { public: false }, + ) + .into_dyn() + } else { + IdFilter(public.gateway.clone()).into_dyn() + }, + ) + .into_dyn(), + acme: public.acme.clone(), + addr, + add_x_forwarded_headers: ssl.add_x_forwarded_headers, + connect_ssl: connect_ssl + .clone() + .map(|_| ctrl.tls_client_config.clone()), + }, + ); + } else { + vhosts.insert( + (address.clone(), external), + ProxyTarget { + filter: AndFilter( + bind.net.clone(), + PublicFilter { public: false }, + ) + .into_dyn(), + acme: None, + addr, + add_x_forwarded_headers: ssl.add_x_forwarded_headers, + connect_ssl: connect_ssl + .clone() + .map(|_| ctrl.tls_client_config.clone()), + }, + ); } } } - if let Some(ip_info) = &info.ip_info { - let public = info.public(); - if let Some(wan_ip) = ip_info.wan_ip { - bind_hostname_info.push(HostnameInfo { + } + } + if bind + .options + .secure + .map_or(true, |s| !(s.ssl && bind.options.add_ssl.is_some())) + { + let external = bind.net.assigned_port.or_not_found("assigned lan port")?; + forwards.insert( + external, + ( + SocketAddrV4::new(self.ip, *port), + AndFilter( + SecureFilter { + secure: bind.options.secure.is_some(), + }, + bind.net.clone(), + ) + .into_dyn(), + ), + ); + } + bind.addresses.possible.clear(); + for (gateway_id, info) in net_ifaces + .iter() + .filter(|(_, info)| { + info.ip_info.as_ref().map_or(false, |i| { + !matches!(i.device_type, Some(NetworkInterfaceType::Bridge)) + }) + }) + .filter(|(id, info)| bind.net.filter(id, info)) + { + let gateway = GatewayInfo { + id: gateway_id.clone(), + name: info + .name + .clone() + .or_else(|| info.ip_info.as_ref().map(|i| i.name.clone())) + .unwrap_or_else(|| gateway_id.clone().into()), + public: info.public(), + }; + let port = bind.net.assigned_port.filter(|_| { + bind.options.secure.map_or(false, |s| { + !(s.ssl && bind.options.add_ssl.is_some()) || info.secure() + }) + }); + if !info.public() + && info.ip_info.as_ref().map_or(false, |i| { + i.device_type != Some(NetworkInterfaceType::Wireguard) + }) + { + bind.addresses.possible.insert(HostnameInfo { + gateway: gateway.clone(), + public: false, + hostname: IpHostname::Local { + value: InternedString::from_display(&{ + let hostname = &hostname; + lazy_format!("{hostname}.local") + }), + port, + ssl_port: bind.net.assigned_ssl_port, + }, + }); + } + for HostAddress { + address, + public, + private, + } in host_addresses.iter().cloned() + { + if public.is_none() { + private_dns.insert(address.clone()); + } + let private = private && !info.public(); + let public = public.as_ref().map_or(false, |p| &p.gateway == gateway_id); + if public || private { + if bind + .options + .add_ssl + .as_ref() + .map_or(false, |ssl| ssl.preferred_external_port == 443) + { + bind.addresses.possible.insert(HostnameInfo { gateway: gateway.clone(), - public: true, - hostname: IpHostname::Ipv4 { - value: wan_ip, + public, + hostname: IpHostname::Domain { + value: address.clone(), + port: None, + ssl_port: Some(443), + }, + }); + } else { + bind.addresses.possible.insert(HostnameInfo { + gateway: gateway.clone(), + public, + hostname: IpHostname::Domain { + value: address.clone(), port, ssl_port: bind.net.assigned_ssl_port, }, }); } - for ipnet in &ip_info.subnets { - match ipnet { - IpNet::V4(net) => { - if !public { - bind_hostname_info.push(HostnameInfo { - gateway: gateway.clone(), - public, - hostname: IpHostname::Ipv4 { - value: net.addr(), - port, - ssl_port: bind.net.assigned_ssl_port, - }, - }); - } - } - IpNet::V6(net) => { - bind_hostname_info.push(HostnameInfo { + } + } + if let Some(ip_info) = &info.ip_info { + let public = info.public(); + if let Some(wan_ip) = ip_info.wan_ip { + bind.addresses.possible.insert(HostnameInfo { + gateway: gateway.clone(), + public: true, + hostname: IpHostname::Ipv4 { + value: wan_ip, + port, + ssl_port: bind.net.assigned_ssl_port, + }, + }); + } + for ipnet in &ip_info.subnets { + match ipnet { + IpNet::V4(net) => { + if !public { + bind.addresses.possible.insert(HostnameInfo { gateway: gateway.clone(), - public: public && !ipv6_is_local(net.addr()), - hostname: IpHostname::Ipv6 { + public, + hostname: IpHostname::Ipv4 { value: net.addr(), - scope_id: ip_info.scope_id, port, ssl_port: bind.net.assigned_ssl_port, }, }); } } + IpNet::V6(net) => { + bind.addresses.possible.insert(HostnameInfo { + gateway: gateway.clone(), + public: public && !ipv6_is_local(net.addr()), + hostname: IpHostname::Ipv6 { + value: net.addr(), + scope_id: ip_info.scope_id, + port, + ssl_port: bind.net.assigned_ssl_port, + }, + }); + } } } } - hostname_info.insert(*port, bind_hostname_info); } } @@ -673,9 +680,26 @@ impl NetServiceData { } ctrl.dns.gc_private_domains(&rm)?; + let res = ctrl + .db + .mutate(|db| { + let bindings = host_for(db, self.id.as_ref(), &id)?.as_bindings_mut(); + for (port, bind) in host.bindings.0 { + if let Some(b) = bindings.as_idx_mut(&port) { + b.as_addresses_mut() + .as_possible_mut() + .ser(&bind.addresses.possible)?; + } + } + Ok(()) + }) + .await; + res.result?; if let Some(pkg_id) = self.id.as_ref() { - if let Some(cbs) = ctrl.callbacks.get_host_info(&(pkg_id.clone(), id)) { - cbs.call(vector![]).await?; + if res.revision.is_some() { + if let Some(cbs) = ctrl.callbacks.get_host_info(&(pkg_id.clone(), id)) { + cbs.call(vector![]).await?; + } } } Ok(()) diff --git a/core/src/net/service_interface.rs b/core/src/net/service_interface.rs index 6d04ae891..1eae3ebca 100644 --- a/core/src/net/service_interface.rs +++ b/core/src/net/service_interface.rs @@ -6,7 +6,7 @@ use ts_rs::TS; use crate::{GatewayId, HostId, ServiceInterfaceId}; -#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] pub struct HostnameInfo { @@ -20,7 +20,7 @@ impl HostnameInfo { } } -#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] pub struct GatewayInfo { @@ -29,7 +29,7 @@ pub struct GatewayInfo { pub public: bool, } -#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] #[serde(rename_all_fields = "camelCase")] diff --git a/core/src/net/tunnel.rs b/core/src/net/tunnel.rs index f3b505850..8635475e1 100644 --- a/core/src/net/tunnel.rs +++ b/core/src/net/tunnel.rs @@ -175,8 +175,12 @@ pub async fn remove_tunnel( let host = host?; host.as_bindings_mut().mutate(|b| { Ok(b.values_mut().for_each(|v| { - v.net.private_disabled.remove(&id); - v.net.public_enabled.remove(&id); + v.addresses + .private_disabled + .retain(|h| h.gateway.id != id); + v.addresses + .public_enabled + .retain(|h| h.gateway.id != id); })) })?; } diff --git a/core/src/service/effects/net/ssl.rs b/core/src/service/effects/net/ssl.rs index 317fb50c8..7de40aed6 100644 --- a/core/src/service/effects/net/ssl.rs +++ b/core/src/service/effects/net/ssl.rs @@ -60,10 +60,10 @@ pub async fn get_ssl_certificate( .into_iter() .chain(m.as_private_domains().de()?) .chain( - m.as_hostname_info() + m.as_bindings() .de()? .values() - .flatten() + .flat_map(|b| b.addresses.possible.iter().cloned()) .map(|h| h.to_san_hostname()), ) .collect::>()) @@ -184,10 +184,10 @@ pub async fn get_ssl_key( .into_iter() .chain(m.as_private_domains().de()?) .chain( - m.as_hostname_info() + m.as_bindings() .de()? .values() - .flatten() + .flat_map(|b| b.addresses.possible.iter().cloned()) .map(|h| h.to_san_hostname()), ) .collect::>()) diff --git a/core/src/version/v0_4_0_alpha_20.rs b/core/src/version/v0_4_0_alpha_20.rs index 3320af322..3da7caf5c 100644 --- a/core/src/version/v0_4_0_alpha_20.rs +++ b/core/src/version/v0_4_0_alpha_20.rs @@ -57,29 +57,6 @@ impl VersionT for Version { } } - // Remove onion entries from hostnameInfo in server host - migrate_hostname_info( - db.get_mut("public") - .and_then(|p| p.get_mut("serverInfo")) - .and_then(|s| s.get_mut("network")) - .and_then(|n| n.get_mut("host")), - ); - - // Remove onion entries from hostnameInfo in all package hosts - if let Some(packages) = db - .get_mut("public") - .and_then(|p| p.get_mut("packageData")) - .and_then(|p| p.as_object_mut()) - { - for (_, package) in packages.iter_mut() { - if let Some(hosts) = package.get_mut("hosts").and_then(|h| h.as_object_mut()) { - for (_, host) in hosts.iter_mut() { - migrate_hostname_info(Some(host)); - } - } - } - } - // Remove onion store from private keyStore if let Some(key_store) = db .get_mut("private") @@ -89,6 +66,29 @@ impl VersionT for Version { key_store.remove("onion"); } + // Migrate server host: remove hostnameInfo, add addresses to bindings, clean net + migrate_host( + db.get_mut("public") + .and_then(|p| p.get_mut("serverInfo")) + .and_then(|s| s.get_mut("network")) + .and_then(|n| n.get_mut("host")), + ); + + // Migrate all package hosts + if let Some(packages) = db + .get_mut("public") + .and_then(|p| p.get_mut("packageData")) + .and_then(|p| p.as_object_mut()) + { + for (_, package) in packages.iter_mut() { + if let Some(hosts) = package.get_mut("hosts").and_then(|h| h.as_object_mut()) { + for (_, host) in hosts.iter_mut() { + migrate_host(Some(host)); + } + } + } + } + Ok(Value::Null) } fn down(self, _db: &mut Value) -> Result<(), Error> { @@ -96,20 +96,35 @@ impl VersionT for Version { } } -fn migrate_hostname_info(host: Option<&mut Value>) { - if let Some(hostname_info) = host - .and_then(|h| h.get_mut("hostnameInfo")) - .and_then(|h| h.as_object_mut()) - { - for (_, infos) in hostname_info.iter_mut() { - if let Some(arr) = infos.as_array_mut() { - // Remove onion entries - arr.retain(|info| info.get("kind").and_then(|k| k.as_str()) != Some("onion")); - // Strip "kind" field from remaining entries (HostnameInfo flattened from enum to struct) - for info in arr.iter_mut() { - if let Some(obj) = info.as_object_mut() { - obj.remove("kind"); - } +fn migrate_host(host: Option<&mut Value>) { + let Some(host) = host.and_then(|h| h.as_object_mut()) else { + return; + }; + + // Remove hostnameInfo from host + host.remove("hostnameInfo"); + + // For each binding: add "addresses" field, remove gateway-level fields from "net" + if let Some(bindings) = host.get_mut("bindings").and_then(|b| b.as_object_mut()) { + for (_, binding) in bindings.iter_mut() { + if let Some(binding_obj) = binding.as_object_mut() { + // Add addresses if not present + if !binding_obj.contains_key("addresses") { + binding_obj.insert( + "addresses".into(), + serde_json::json!({ + "privateDisabled": [], + "publicEnabled": [], + "possible": [] + }) + .into(), + ); + } + + // Remove gateway-level privateDisabled/publicEnabled from net + if let Some(net) = binding_obj.get_mut("net").and_then(|n| n.as_object_mut()) { + net.remove("privateDisabled"); + net.remove("publicEnabled"); } } } diff --git a/sdk/base/lib/osBindings/BindInfo.ts b/sdk/base/lib/osBindings/BindInfo.ts index eba1fe446..d83c6948f 100644 --- a/sdk/base/lib/osBindings/BindInfo.ts +++ b/sdk/base/lib/osBindings/BindInfo.ts @@ -1,5 +1,11 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { BindOptions } from './BindOptions' +import type { DerivedAddressInfo } from './DerivedAddressInfo' import type { NetInfo } from './NetInfo' -export type BindInfo = { enabled: boolean; options: BindOptions; net: NetInfo } +export type BindInfo = { + enabled: boolean + options: BindOptions + net: NetInfo + addresses: DerivedAddressInfo +} diff --git a/sdk/base/lib/osBindings/BindingGatewaySetEnabledParams.ts b/sdk/base/lib/osBindings/BindingSetAddressEnabledParams.ts similarity index 58% rename from sdk/base/lib/osBindings/BindingGatewaySetEnabledParams.ts rename to sdk/base/lib/osBindings/BindingSetAddressEnabledParams.ts index fb90cdaa7..2dfeff757 100644 --- a/sdk/base/lib/osBindings/BindingGatewaySetEnabledParams.ts +++ b/sdk/base/lib/osBindings/BindingSetAddressEnabledParams.ts @@ -1,8 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { GatewayId } from './GatewayId' -export type BindingGatewaySetEnabledParams = { +export type BindingSetAddressEnabledParams = { internalPort: number - gateway: GatewayId + address: string enabled: boolean | null } diff --git a/sdk/base/lib/osBindings/Bindings.ts b/sdk/base/lib/osBindings/Bindings.ts new file mode 100644 index 000000000..ef921868e --- /dev/null +++ b/sdk/base/lib/osBindings/Bindings.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { BindInfo } from './BindInfo' + +export type Bindings = { [key: number]: BindInfo } diff --git a/sdk/base/lib/osBindings/DerivedAddressInfo.ts b/sdk/base/lib/osBindings/DerivedAddressInfo.ts new file mode 100644 index 000000000..2c83191fd --- /dev/null +++ b/sdk/base/lib/osBindings/DerivedAddressInfo.ts @@ -0,0 +1,17 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HostnameInfo } from './HostnameInfo' + +export type DerivedAddressInfo = { + /** + * User-controlled: private-gateway addresses the user has disabled + */ + privateDisabled: Array + /** + * User-controlled: public-gateway addresses the user has enabled + */ + publicEnabled: Array + /** + * COMPUTED: NetServiceData::update — all possible addresses for this binding + */ + possible: Array +} diff --git a/sdk/base/lib/osBindings/Host.ts b/sdk/base/lib/osBindings/Host.ts index 1cbe0b84e..cecc24475 100644 --- a/sdk/base/lib/osBindings/Host.ts +++ b/sdk/base/lib/osBindings/Host.ts @@ -1,14 +1,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { BindInfo } from './BindInfo' -import type { HostnameInfo } from './HostnameInfo' +import type { Bindings } from './Bindings' import type { PublicDomainConfig } from './PublicDomainConfig' export type Host = { - bindings: { [key: number]: BindInfo } + bindings: Bindings publicDomains: { [key: string]: PublicDomainConfig } privateDomains: Array - /** - * COMPUTED: NetService::update - */ - hostnameInfo: { [key: number]: Array } } diff --git a/sdk/base/lib/osBindings/NetInfo.ts b/sdk/base/lib/osBindings/NetInfo.ts index 4483f81b8..90abe2cd8 100644 --- a/sdk/base/lib/osBindings/NetInfo.ts +++ b/sdk/base/lib/osBindings/NetInfo.ts @@ -1,9 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { GatewayId } from './GatewayId' export type NetInfo = { - privateDisabled: Array - publicEnabled: Array assignedPort: number | null assignedSslPort: number | null } diff --git a/sdk/base/lib/osBindings/index.ts b/sdk/base/lib/osBindings/index.ts index 3f02322d8..18cca9c9a 100644 --- a/sdk/base/lib/osBindings/index.ts +++ b/sdk/base/lib/osBindings/index.ts @@ -36,7 +36,8 @@ export { BackupTargetFS } from './BackupTargetFS' export { Base64 } from './Base64' export { BindId } from './BindId' export { BindInfo } from './BindInfo' -export { BindingGatewaySetEnabledParams } from './BindingGatewaySetEnabledParams' +export { BindingSetAddressEnabledParams } from './BindingSetAddressEnabledParams' +export { Bindings } from './Bindings' export { BindOptions } from './BindOptions' export { BindParams } from './BindParams' export { Blake3Commitment } from './Blake3Commitment' @@ -64,6 +65,7 @@ export { Dependencies } from './Dependencies' export { DependencyMetadata } from './DependencyMetadata' export { DependencyRequirement } from './DependencyRequirement' export { DepInfo } from './DepInfo' +export { DerivedAddressInfo } from './DerivedAddressInfo' export { Description } from './Description' export { DesiredStatus } from './DesiredStatus' export { DestroySubcontainerFsParams } from './DestroySubcontainerFsParams' diff --git a/sdk/base/lib/util/getServiceInterface.ts b/sdk/base/lib/util/getServiceInterface.ts index cc80e3aec..bc22e70c6 100644 --- a/sdk/base/lib/util/getServiceInterface.ts +++ b/sdk/base/lib/util/getServiceInterface.ts @@ -1,6 +1,12 @@ import { PackageId, ServiceInterfaceId, ServiceInterfaceType } from '../types' import { knownProtocols } from '../interfaces/Host' -import { AddressInfo, Host, Hostname, HostnameInfo } from '../types' +import { + AddressInfo, + DerivedAddressInfo, + Host, + Hostname, + HostnameInfo, +} from '../types' import { Effects } from '../Effects' import { DropGenerator, DropPromise } from './Drop' import { IpAddress, IPV6_LINK_LOCAL } from './ip' @@ -220,6 +226,14 @@ function filterRec( return hostnames } +function enabledAddresses(addr: DerivedAddressInfo): HostnameInfo[] { + return addr.possible.filter((h) => + h.public + ? addr.publicEnabled.some((e) => deepEqual(e, h)) + : !addr.privateDisabled.some((d) => deepEqual(d, h)), + ) +} + export const filledAddress = ( host: Host, addressInfo: AddressInfo, @@ -229,7 +243,8 @@ export const filledAddress = ( const u = toUrls(h) return [u.url, u.sslUrl].filter((u) => u !== null) } - const hostnames = host.hostnameInfo[addressInfo.internalPort] ?? [] + const binding = host.bindings[addressInfo.internalPort] + const hostnames = binding ? enabledAddresses(binding.addresses) : [] function filledAddressFromHostnames( hostnames: HostnameInfo[], diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/gateways.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/gateways.component.ts index 077fa87bd..1a3a4d632 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/gateways.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/gateways.component.ts @@ -83,32 +83,8 @@ export class InterfaceGatewaysComponent { readonly gateways = input.required() - async onToggle(gateway: InterfaceGateway) { - const addressInfo = this.interface.value()!.addressInfo - const pkgId = this.interface.packageId() - - const loader = this.loader.open().subscribe() - - try { - if (pkgId) { - await this.api.pkgBindingToggleGateway({ - gateway: gateway.id, - enabled: !gateway.enabled, - internalPort: addressInfo.internalPort, - host: addressInfo.hostId, - package: pkgId, - }) - } else { - await this.api.serverBindingToggleGateway({ - gateway: gateway.id, - enabled: !gateway.enabled, - internalPort: 80, - }) - } - } catch (e: any) { - this.errorService.handleError(e) - } finally { - loader.unsubscribe() - } + async onToggle(_gateway: InterfaceGateway) { + // TODO: Replace with per-address toggle UI (Section 6 frontend overhaul). + // Gateway-level toggle replaced by set-address-enabled RPC. } } diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts index 21bc013b7..6e05c02d9 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts @@ -252,18 +252,23 @@ export class InterfaceService { serviceInterface: T.ServiceInterface, host: T.Host, ): T.HostnameInfo[] { - let hostnameInfo = - host.hostnameInfo[serviceInterface.addressInfo.internalPort] - return ( - hostnameInfo?.filter( - h => - this.config.accessType === 'localhost' || - !( - (h.hostname.kind === 'ipv6' && - utils.IPV6_LINK_LOCAL.contains(h.hostname.value)) || - h.gateway.id === 'lo' - ), - ) || [] + const binding = + host.bindings[serviceInterface.addressInfo.internalPort] + if (!binding) return [] + const addr = binding.addresses + const enabled = addr.possible.filter(h => + h.public + ? addr.publicEnabled.some(e => utils.deepEqual(e, h)) + : !addr.privateDisabled.some(d => utils.deepEqual(d, h)), + ) + return enabled.filter( + h => + this.config.accessType === 'localhost' || + !( + (h.hostname.kind === 'ipv6' && + utils.IPV6_LINK_LOCAL.contains(h.hostname.value)) || + h.gateway.id === 'lo' + ), ) } diff --git a/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts index 0fa1b4766..03ec3d3ee 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts @@ -135,8 +135,8 @@ export default class ServiceInterfaceRoute { gateways.map(g => ({ enabled: (g.public - ? binding?.net.publicEnabled.includes(g.id) - : !binding?.net.privateDisabled.includes(g.id)) ?? false, + ? binding?.addresses.publicEnabled.some(a => a.gateway.id === g.id) + : !binding?.addresses.privateDisabled.some(a => a.gateway.id === g.id)) ?? false, ...g, })) || [], publicDomains: getPublicDomains(host.publicDomains, gateways), diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/startos-ui/startos-ui.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/startos-ui/startos-ui.component.ts index 08ba78d94..6d2246f98 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/startos-ui/startos-ui.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/startos-ui/startos-ui.component.ts @@ -96,8 +96,8 @@ export default class StartOsUiComponent { gateways: gateways.map(g => ({ enabled: (g.public - ? binding?.net.publicEnabled.includes(g.id) - : !binding?.net.privateDisabled.includes(g.id)) ?? false, + ? binding?.addresses.publicEnabled.some(a => a.gateway.id === g.id) + : !binding?.addresses.privateDisabled.some(a => a.gateway.id === g.id)) ?? false, ...g, })), publicDomains: getPublicDomains(network.host.publicDomains, gateways), diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index a5280f5f4..06f482e99 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -2126,8 +2126,74 @@ export namespace Mock { net: { assignedPort: 80, assignedSslPort: 443, - publicEnabled: [], + }, + addresses: { privateDisabled: [], + publicEnabled: [], + possible: [ + { + gateway: { id: 'eth0', name: 'Ethernet', public: false }, + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: null, + sslPort: 1234, + }, + }, + { + gateway: { id: 'wlan0', name: 'Wireless', public: false }, + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: null, + sslPort: 1234, + }, + }, + { + gateway: { id: 'wlan0', name: 'Wireless', public: false }, + public: false, + hostname: { + kind: 'ipv4', + value: '192.168.10.11', + port: null, + sslPort: 1234, + }, + }, + { + gateway: { id: 'wlan0', name: 'Wireless', public: false }, + public: false, + hostname: { + kind: 'ipv4', + value: '10.0.0.2', + port: null, + sslPort: 1234, + }, + }, + { + gateway: { id: 'eth0', name: 'Ethernet', public: false }, + public: false, + hostname: { + kind: 'ipv6', + value: '[fe80:cd00:0000:0cde:1257:0000:211e:72cd]', + scopeId: 2, + port: null, + sslPort: 1234, + }, + }, + { + gateway: { id: 'wlan0', name: 'Wireless', public: false }, + public: false, + hostname: { + kind: 'ipv6', + value: '[fe80:cd00:0000:0cde:1257:0000:211e:1234]', + scopeId: 3, + port: null, + sslPort: 1234, + }, + }, + ], }, options: { addSsl: null, @@ -2138,78 +2204,6 @@ export namespace Mock { }, publicDomains: {}, privateDomains: [], - hostnameInfo: { - 80: [ - { - kind: 'ip', - gateway: { id: 'eth0', name: 'Ethernet', public: false }, - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: null, - sslPort: 1234, - }, - }, - { - kind: 'ip', - gateway: { id: 'wlan0', name: 'Wireless', public: false }, - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: null, - sslPort: 1234, - }, - }, - { - kind: 'ip', - gateway: { id: 'wlan0', name: 'Wireless', public: false }, - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.10.11', - port: null, - sslPort: 1234, - }, - }, - { - kind: 'ip', - gateway: { id: 'wlan0', name: 'Wireless', public: false }, - public: false, - hostname: { - kind: 'ipv4', - value: '10.0.0.2', - port: null, - sslPort: 1234, - }, - }, - { - kind: 'ip', - gateway: { id: 'eth0', name: 'Ethernet', public: false }, - public: false, - hostname: { - kind: 'ipv6', - value: '[fe80:cd00:0000:0cde:1257:0000:211e:72cd]', - scopeId: 2, - port: null, - sslPort: 1234, - }, - }, - { - kind: 'ip', - gateway: { id: 'wlan0', name: 'Wireless', public: false }, - public: false, - hostname: { - kind: 'ipv6', - value: '[fe80:cd00:0000:0cde:1257:0000:211e:1234]', - scopeId: 3, - port: null, - sslPort: 1234, - }, - }, - ], - }, }, bcdefgh: { bindings: { @@ -2218,8 +2212,11 @@ export namespace Mock { net: { assignedPort: 8332, assignedSslPort: null, - publicEnabled: [], + }, + addresses: { privateDisabled: [], + publicEnabled: [], + possible: [], }, options: { addSsl: null, @@ -2230,9 +2227,6 @@ export namespace Mock { }, publicDomains: {}, privateDomains: [], - hostnameInfo: { - 8332: [], - }, }, cdefghi: { bindings: { @@ -2241,8 +2235,11 @@ export namespace Mock { net: { assignedPort: 8333, assignedSslPort: null, - publicEnabled: [], + }, + addresses: { privateDisabled: [], + publicEnabled: [], + possible: [], }, options: { addSsl: null, @@ -2253,9 +2250,6 @@ export namespace Mock { }, publicDomains: {}, privateDomains: [], - hostnameInfo: { - 8333: [], - }, }, }, storeExposedDependents: [], diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index 85abac31a..73d42ee68 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -281,13 +281,13 @@ export namespace RR { } export type RemoveAcmeRes = null - export type ServerBindingToggleGatewayReq = { - // server.host.binding.set-gateway-enabled - gateway: T.GatewayId + export type ServerBindingSetAddressEnabledReq = { + // server.host.binding.set-address-enabled internalPort: 80 - enabled: boolean + address: string // JSON-serialized HostnameInfo + enabled: boolean | null // null = reset to default } - export type ServerBindingToggleGatewayRes = null + export type ServerBindingSetAddressEnabledRes = null export type OsUiAddPublicDomainReq = { // server.host.address.domain.public.add @@ -315,16 +315,16 @@ export namespace RR { } export type OsUiRemovePrivateDomainRes = null - export type PkgBindingToggleGatewayReq = Omit< - ServerBindingToggleGatewayReq, + export type PkgBindingSetAddressEnabledReq = Omit< + ServerBindingSetAddressEnabledReq, 'internalPort' > & { - // package.host.binding.set-gateway-enabled + // package.host.binding.set-address-enabled internalPort: number package: T.PackageId // string host: T.HostId // string } - export type PkgBindingToggleGatewayRes = null + export type PkgBindingSetAddressEnabledRes = null export type PkgAddPublicDomainReq = OsUiAddPublicDomainReq & { // package.host.address.domain.public.add diff --git a/web/projects/ui/src/app/services/api/embassy-api.service.ts b/web/projects/ui/src/app/services/api/embassy-api.service.ts index c55dc8441..ee79644f8 100644 --- a/web/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-api.service.ts @@ -336,9 +336,9 @@ export abstract class ApiService { abstract removeAcme(params: RR.RemoveAcmeReq): Promise - abstract serverBindingToggleGateway( - params: RR.ServerBindingToggleGatewayReq, - ): Promise + abstract serverBindingSetAddressEnabled( + params: RR.ServerBindingSetAddressEnabledReq, + ): Promise abstract osUiAddPublicDomain( params: RR.OsUiAddPublicDomainReq, @@ -356,9 +356,9 @@ export abstract class ApiService { params: RR.OsUiRemovePrivateDomainReq, ): Promise - abstract pkgBindingToggleGateway( - params: RR.PkgBindingToggleGatewayReq, - ): Promise + abstract pkgBindingSetAddressEnabled( + params: RR.PkgBindingSetAddressEnabledReq, + ): Promise abstract pkgAddPublicDomain( params: RR.PkgAddPublicDomainReq, diff --git a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts index 00524e2ef..ad7085c40 100644 --- a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -607,11 +607,11 @@ export class LiveApiService extends ApiService { }) } - async serverBindingToggleGateway( - params: RR.ServerBindingToggleGatewayReq, - ): Promise { + async serverBindingSetAddressEnabled( + params: RR.ServerBindingSetAddressEnabledReq, + ): Promise { return this.rpcRequest({ - method: 'server.host.binding.set-gateway-enabled', + method: 'server.host.binding.set-address-enabled', params, }) } @@ -652,11 +652,11 @@ export class LiveApiService extends ApiService { }) } - async pkgBindingToggleGateway( - params: RR.PkgBindingToggleGatewayReq, - ): Promise { + async pkgBindingSetAddressEnabled( + params: RR.PkgBindingSetAddressEnabledReq, + ): Promise { return this.rpcRequest({ - method: 'package.host.binding.set-gateway-enabled', + method: 'package.host.binding.set-address-enabled', params, }) } diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index e91a732e5..3c3e7a1d7 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -1348,20 +1348,12 @@ export class MockApiService extends ApiService { return null } - async serverBindingToggleGateway( - params: RR.ServerBindingToggleGatewayReq, - ): Promise { + async serverBindingSetAddressEnabled( + params: RR.ServerBindingSetAddressEnabledReq, + ): Promise { await pauseFor(2000) - const patch = [ - { - op: PatchOp.REPLACE, - path: `/serverInfo/network/host/bindings/${params.internalPort}/net/publicEnabled`, - value: params.enabled ? [params.gateway] : [], - }, - ] - this.mockRevision(patch) - + // Mock: no-op since address enable/disable modifies DerivedAddressInfo sets return null } @@ -1380,10 +1372,9 @@ export class MockApiService extends ApiService { }, { op: PatchOp.ADD, - path: `/serverInfo/host/hostnameInfo/80/0`, + path: `/serverInfo/network/host/bindings/80/addresses/possible/0`, value: { - kind: 'ip', - gatewayId: 'eth0', + gateway: { id: 'eth0', name: 'Ethernet', public: false }, public: true, hostname: { kind: 'domain', @@ -1412,7 +1403,7 @@ export class MockApiService extends ApiService { }, { op: PatchOp.REMOVE, - path: `/serverInfo/host/hostnameInfo/80/0`, + path: `/serverInfo/network/host/bindings/80/addresses/possible/0`, }, ] this.mockRevision(patch) @@ -1433,10 +1424,9 @@ export class MockApiService extends ApiService { }, { op: PatchOp.ADD, - path: `/serverInfo/host/hostnameInfo/80/0`, + path: `/serverInfo/network/host/bindings/80/addresses/possible/0`, value: { - kind: 'ip', - gatewayId: 'eth0', + gateway: { id: 'eth0', name: 'Ethernet', public: false }, public: false, hostname: { kind: 'domain', @@ -1466,7 +1456,7 @@ export class MockApiService extends ApiService { }, { op: PatchOp.REMOVE, - path: `/serverInfo/host/hostnameInfo/80/0`, + path: `/serverInfo/network/host/bindings/80/addresses/possible/0`, }, ] this.mockRevision(patch) @@ -1474,20 +1464,12 @@ export class MockApiService extends ApiService { return null } - async pkgBindingToggleGateway( - params: RR.PkgBindingToggleGatewayReq, - ): Promise { + async pkgBindingSetAddressEnabled( + params: RR.PkgBindingSetAddressEnabledReq, + ): Promise { await pauseFor(2000) - const patch = [ - { - op: PatchOp.REPLACE, - path: `/packageData/${params.package}/hosts/${params.host}/bindings/${params.internalPort}/net/privateDisabled`, - value: params.enabled ? [] : [params.gateway], - }, - ] - this.mockRevision(patch) - + // Mock: no-op since address enable/disable modifies DerivedAddressInfo sets return null } @@ -1506,10 +1488,9 @@ export class MockApiService extends ApiService { }, { op: PatchOp.ADD, - path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`, + path: `/packageData/${params.package}/hosts/${params.host}/bindings/80/addresses/possible/0`, value: { - kind: 'ip', - gatewayId: 'eth0', + gateway: { id: 'eth0', name: 'Ethernet', public: false }, public: true, hostname: { kind: 'domain', @@ -1538,7 +1519,7 @@ export class MockApiService extends ApiService { }, { op: PatchOp.REMOVE, - path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`, + path: `/packageData/${params.package}/hosts/${params.host}/bindings/80/addresses/possible/0`, }, ] this.mockRevision(patch) @@ -1559,10 +1540,9 @@ export class MockApiService extends ApiService { }, { op: PatchOp.ADD, - path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`, + path: `/packageData/${params.package}/hosts/${params.host}/bindings/80/addresses/possible/0`, value: { - kind: 'ip', - gatewayId: 'eth0', + gateway: { id: 'eth0', name: 'Ethernet', public: false }, public: false, hostname: { kind: 'domain', @@ -1592,7 +1572,7 @@ export class MockApiService extends ApiService { }, { op: PatchOp.REMOVE, - path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`, + path: `/packageData/${params.package}/hosts/${params.host}/bindings/80/addresses/possible/0`, }, ] this.mockRevision(patch) diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 6effc3827..c1d0c793a 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -38,8 +38,74 @@ export const mockPatchData: DataModel = { net: { assignedPort: null, assignedSslPort: 443, - publicEnabled: [], + }, + addresses: { privateDisabled: [], + publicEnabled: [], + possible: [ + { + gateway: { id: 'eth0', name: 'Ethernet', public: false }, + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: null, + sslPort: 443, + }, + }, + { + gateway: { id: 'wlan0', name: 'Wireless', public: false }, + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: null, + sslPort: 443, + }, + }, + { + gateway: { id: 'eth0', name: 'Ethernet', public: false }, + public: false, + hostname: { + kind: 'ipv4', + value: '10.0.0.1', + port: null, + sslPort: 443, + }, + }, + { + gateway: { id: 'wlan0', name: 'Wireless', public: false }, + public: false, + hostname: { + kind: 'ipv4', + value: '10.0.0.2', + port: null, + sslPort: 443, + }, + }, + { + gateway: { id: 'eth0', name: 'Ethernet', public: false }, + public: false, + hostname: { + kind: 'ipv6', + value: 'fe80::cd00:0000:0cde:1257:0000:211e:72cd', + scopeId: 2, + port: null, + sslPort: 443, + }, + }, + { + gateway: { id: 'wlan0', name: 'Wireless', public: false }, + public: false, + hostname: { + kind: 'ipv6', + value: 'fe80::cd00:0000:0cde:1257:0000:211e:1234', + scopeId: 3, + port: null, + sslPort: 443, + }, + }, + ], }, options: { preferredExternalPort: 80, @@ -54,78 +120,6 @@ export const mockPatchData: DataModel = { }, publicDomains: {}, privateDomains: [], - hostnameInfo: { - 80: [ - { - kind: 'ip', - gateway: { id: 'eth0', name: 'Ethernet', public: false }, - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: null, - sslPort: 443, - }, - }, - { - kind: 'ip', - gateway: { id: 'wlan0', name: 'Wireless', public: false }, - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: null, - sslPort: 443, - }, - }, - { - kind: 'ip', - gateway: { id: 'eth0', name: 'Ethernet', public: false }, - public: false, - hostname: { - kind: 'ipv4', - value: '10.0.0.1', - port: null, - sslPort: 443, - }, - }, - { - kind: 'ip', - gateway: { id: 'wlan0', name: 'Wireless', public: false }, - public: false, - hostname: { - kind: 'ipv4', - value: '10.0.0.2', - port: null, - sslPort: 443, - }, - }, - { - kind: 'ip', - gateway: { id: 'eth0', name: 'Ethernet', public: false }, - public: false, - hostname: { - kind: 'ipv6', - value: 'fe80::cd00:0000:0cde:1257:0000:211e:72cd', - scopeId: 2, - port: null, - sslPort: 443, - }, - }, - { - kind: 'ip', - gateway: { id: 'wlan0', name: 'Wireless', public: false }, - public: false, - hostname: { - kind: 'ipv6', - value: 'fe80::cd00:0000:0cde:1257:0000:211e:1234', - scopeId: 3, - port: null, - sslPort: 443, - }, - }, - ], - }, }, gateways: { eth0: { @@ -503,8 +497,74 @@ export const mockPatchData: DataModel = { net: { assignedPort: 80, assignedSslPort: 443, - publicEnabled: [], + }, + addresses: { privateDisabled: [], + publicEnabled: [], + possible: [ + { + gateway: { id: 'eth0', name: 'Ethernet', public: false }, + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: null, + sslPort: 1234, + }, + }, + { + gateway: { id: 'wlan0', name: 'Wireless', public: false }, + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: null, + sslPort: 1234, + }, + }, + { + gateway: { id: 'eth0', name: 'Ethernet', public: false }, + public: false, + hostname: { + kind: 'ipv4', + value: '10.0.0.1', + port: null, + sslPort: 1234, + }, + }, + { + gateway: { id: 'wlan0', name: 'Wireless', public: false }, + public: false, + hostname: { + kind: 'ipv4', + value: '10.0.0.2', + port: null, + sslPort: 1234, + }, + }, + { + gateway: { id: 'eth0', name: 'Ethernet', public: false }, + public: false, + hostname: { + kind: 'ipv6', + value: 'fe80::cd00:0000:0cde:1257:0000:211e:72cd', + scopeId: 2, + port: null, + sslPort: 1234, + }, + }, + { + gateway: { id: 'wlan0', name: 'Wireless', public: false }, + public: false, + hostname: { + kind: 'ipv6', + value: 'fe80::cd00:0000:0cde:1257:0000:211e:1234', + scopeId: 3, + port: null, + sslPort: 1234, + }, + }, + ], }, options: { addSsl: null, @@ -515,78 +575,6 @@ export const mockPatchData: DataModel = { }, publicDomains: {}, privateDomains: [], - hostnameInfo: { - 80: [ - { - kind: 'ip', - gateway: { id: 'eth0', name: 'Ethernet', public: false }, - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: null, - sslPort: 1234, - }, - }, - { - kind: 'ip', - gateway: { id: 'wlan0', name: 'Wireless', public: false }, - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: null, - sslPort: 1234, - }, - }, - { - kind: 'ip', - gateway: { id: 'eth0', name: 'Ethernet', public: false }, - public: false, - hostname: { - kind: 'ipv4', - value: '10.0.0.1', - port: null, - sslPort: 1234, - }, - }, - { - kind: 'ip', - gateway: { id: 'wlan0', name: 'Wireless', public: false }, - public: false, - hostname: { - kind: 'ipv4', - value: '10.0.0.2', - port: null, - sslPort: 1234, - }, - }, - { - kind: 'ip', - gateway: { id: 'eth0', name: 'Ethernet', public: false }, - public: false, - hostname: { - kind: 'ipv6', - value: 'fe80::cd00:0000:0cde:1257:0000:211e:72cd', - scopeId: 2, - port: null, - sslPort: 1234, - }, - }, - { - kind: 'ip', - gateway: { id: 'wlan0', name: 'Wireless', public: false }, - public: false, - hostname: { - kind: 'ipv6', - value: 'fe80::cd00:0000:0cde:1257:0000:211e:1234', - scopeId: 3, - port: null, - sslPort: 1234, - }, - }, - ], - }, }, bcdefgh: { bindings: { @@ -595,8 +583,11 @@ export const mockPatchData: DataModel = { net: { assignedPort: 8332, assignedSslPort: null, - publicEnabled: [], + }, + addresses: { privateDisabled: [], + publicEnabled: [], + possible: [], }, options: { addSsl: null, @@ -607,9 +598,6 @@ export const mockPatchData: DataModel = { }, publicDomains: {}, privateDomains: [], - hostnameInfo: { - 8332: [], - }, }, cdefghi: { bindings: { @@ -618,8 +606,11 @@ export const mockPatchData: DataModel = { net: { assignedPort: 8333, assignedSslPort: null, - publicEnabled: [], + }, + addresses: { privateDisabled: [], + publicEnabled: [], + possible: [], }, options: { addSsl: null, @@ -630,9 +621,6 @@ export const mockPatchData: DataModel = { }, publicDomains: {}, privateDomains: [], - hostnameInfo: { - 8333: [], - }, }, }, storeExposedDependents: [], From 2a54625f431cc49d5675914669c985dc3ac57ada Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Wed, 11 Feb 2026 18:10:27 -0700 Subject: [PATCH 10/17] feat: replace InterfaceFilter with ForwardRequirements, add WildcardListener, complete alpha.20 bump - Replace DynInterfaceFilter with ForwardRequirements for per-IP forward precision with source-subnet iptables filtering for private forwards - Add WildcardListener (binds [::]:port) to replace the per-gateway NetworkInterfaceListener/SelfContainedNetworkInterfaceListener/ UpgradableListener infrastructure - Update forward-port script with src_subnet and excluded_src env vars - Remove unused filter types and listener infrastructure from gateway.rs - Add availablePorts migration (IdPool -> BTreeMap) to alpha.20 - Complete version bump to 0.4.0-alpha.20 in SDK and web --- build/lib/scripts/forward-port | 22 +- core/src/bins/start_init.rs | 6 +- core/src/bins/startd.rs | 6 +- core/src/bins/tunnel.rs | 9 +- core/src/context/rpc.rs | 6 +- core/src/context/setup.rs | 6 +- core/src/init.rs | 6 +- core/src/net/forward.rs | 206 ++++++---- core/src/net/gateway.rs | 566 ++++------------------------ core/src/net/host/binding.rs | 34 +- core/src/net/net_controller.rs | 401 +++++++++----------- core/src/net/vhost.rs | 248 ++++++++++-- core/src/net/web_server.rs | 22 -- core/src/tunnel/api.rs | 2 +- core/src/tunnel/context.rs | 2 +- core/src/version/v0_4_0_alpha_20.rs | 60 +++ sdk/package/lib/StartSdk.ts | 2 +- web/package-lock.json | 4 +- web/package.json | 2 +- 19 files changed, 714 insertions(+), 896 deletions(-) diff --git a/build/lib/scripts/forward-port b/build/lib/scripts/forward-port index 705c1e6a7..31f42a39e 100755 --- a/build/lib/scripts/forward-port +++ b/build/lib/scripts/forward-port @@ -5,7 +5,7 @@ if [ -z "$sip" ] || [ -z "$dip" ] || [ -z "$dprefix" ] || [ -z "$sport" ] || [ - exit 1 fi -NAME="F$(echo "$sip:$sport -> $dip/$dprefix:$dport" | sha256sum | head -c 15)" +NAME="F$(echo "$sip:$sport -> $dip/$dprefix:$dport ${src_subnet:-any} ${excluded_src:-none}" | sha256sum | head -c 15)" for kind in INPUT FORWARD ACCEPT; do if ! iptables -C $kind -j "${NAME}_${kind}" 2> /dev/null; then @@ -36,8 +36,22 @@ if [ "$UNDO" = 1 ]; then fi # DNAT: rewrite destination for incoming packets (external traffic) -iptables -t nat -A ${NAME}_PREROUTING -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport" -iptables -t nat -A ${NAME}_PREROUTING -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport" +# When src_subnet is set, only forward traffic from that subnet (private forwards) +# excluded_src: comma-separated gateway/router IPs to reject (they may masquerade internet traffic) +if [ -n "$src_subnet" ]; then + if [ -n "$excluded_src" ]; then + IFS=',' read -ra EXCLUDED <<< "$excluded_src" + for excl in "${EXCLUDED[@]}"; do + iptables -t nat -A ${NAME}_PREROUTING -s "$excl" -d "$sip" -p tcp --dport "$sport" -j RETURN + iptables -t nat -A ${NAME}_PREROUTING -s "$excl" -d "$sip" -p udp --dport "$sport" -j RETURN + done + fi + iptables -t nat -A ${NAME}_PREROUTING -s "$src_subnet" -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport" + iptables -t nat -A ${NAME}_PREROUTING -s "$src_subnet" -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport" +else + iptables -t nat -A ${NAME}_PREROUTING -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport" + iptables -t nat -A ${NAME}_PREROUTING -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport" +fi # DNAT: rewrite destination for locally-originated packets (hairpin from host itself) iptables -t nat -A ${NAME}_OUTPUT -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport" @@ -52,4 +66,4 @@ iptables -t nat -A ${NAME}_POSTROUTING -d "$dip" -p udp --dport "$dport" -j MASQ iptables -A ${NAME}_FORWARD -d $dip -p tcp --dport $dport -m state --state NEW -j ACCEPT iptables -A ${NAME}_FORWARD -d $dip -p udp --dport $dport -m state --state NEW -j ACCEPT -exit $err \ No newline at end of file +exit $err diff --git a/core/src/bins/start_init.rs b/core/src/bins/start_init.rs index 48e65f5af..5c53a6e0c 100644 --- a/core/src/bins/start_init.rs +++ b/core/src/bins/start_init.rs @@ -9,7 +9,7 @@ use crate::disk::fsck::RepairStrategy; use crate::disk::main::DEFAULT_PASSWORD; use crate::firmware::{check_for_firmware_update, update_firmware}; use crate::init::{InitPhases, STANDBY_MODE_PATH}; -use crate::net::gateway::UpgradableListener; +use crate::net::gateway::WildcardListener; use crate::net::web_server::WebServer; use crate::prelude::*; use crate::progress::FullProgressTracker; @@ -19,7 +19,7 @@ use crate::{DATA_DIR, PLATFORM}; #[instrument(skip_all)] async fn setup_or_init( - server: &mut WebServer, + server: &mut WebServer, config: &ServerConfig, ) -> Result, Error> { if let Some(firmware) = check_for_firmware_update() @@ -204,7 +204,7 @@ async fn setup_or_init( #[instrument(skip_all)] pub async fn main( - server: &mut WebServer, + server: &mut WebServer, config: &ServerConfig, ) -> Result, Error> { if &*PLATFORM == "raspberrypi" && tokio::fs::metadata(STANDBY_MODE_PATH).await.is_ok() { diff --git a/core/src/bins/startd.rs b/core/src/bins/startd.rs index f4a7784f4..b88f622e5 100644 --- a/core/src/bins/startd.rs +++ b/core/src/bins/startd.rs @@ -12,7 +12,7 @@ use tracing::instrument; use crate::context::config::ServerConfig; use crate::context::rpc::InitRpcContextPhases; use crate::context::{DiagnosticContext, InitContext, RpcContext}; -use crate::net::gateway::{BindTcp, SelfContainedNetworkInterfaceListener, UpgradableListener}; +use crate::net::gateway::WildcardListener; use crate::net::static_server::refresher; use crate::net::web_server::{Acceptor, WebServer}; use crate::prelude::*; @@ -23,7 +23,7 @@ use crate::util::logger::LOGGER; #[instrument(skip_all)] async fn inner_main( - server: &mut WebServer, + server: &mut WebServer, config: &ServerConfig, ) -> Result, Error> { let rpc_ctx = if !tokio::fs::metadata("/run/startos/initialized") @@ -148,7 +148,7 @@ pub fn main(args: impl IntoIterator) { .expect(&t!("bins.startd.failed-to-initialize-runtime")); let res = rt.block_on(async { let mut server = WebServer::new( - Acceptor::bind_upgradable(SelfContainedNetworkInterfaceListener::bind(BindTcp, 80)), + Acceptor::new(WildcardListener::new(80)?), refresher(), ); match inner_main(&mut server, &config).await { diff --git a/core/src/bins/tunnel.rs b/core/src/bins/tunnel.rs index 97fb818ea..57615c00c 100644 --- a/core/src/bins/tunnel.rs +++ b/core/src/bins/tunnel.rs @@ -13,7 +13,7 @@ use visit_rs::Visit; use crate::context::CliContext; use crate::context::config::ClientConfig; -use crate::net::gateway::{Bind, BindTcp}; +use tokio::net::TcpListener; use crate::net::tls::TlsListener; use crate::net::web_server::{Accept, Acceptor, MetadataVisitor, WebServer}; use crate::prelude::*; @@ -57,7 +57,12 @@ async fn inner_main(config: &TunnelConfig) -> Result<(), Error> { if !a.contains_key(&key) { match (|| { Ok::<_, Error>(TlsListener::new( - BindTcp.bind(addr)?, + TcpListener::from_std( + mio::net::TcpListener::bind(addr) + .with_kind(ErrorKind::Network)? + .into(), + ) + .with_kind(ErrorKind::Network)?, TunnelCertHandler { db: https_db.clone(), crypto_provider: Arc::new(tokio_rustls::rustls::crypto::ring::default_provider()), diff --git a/core/src/context/rpc.rs b/core/src/context/rpc.rs index a59d60236..6350672f1 100644 --- a/core/src/context/rpc.rs +++ b/core/src/context/rpc.rs @@ -34,7 +34,7 @@ use crate::disk::mount::guard::MountGuard; use crate::init::{InitResult, check_time_is_synchronized}; use crate::install::PKG_ARCHIVE_DIR; use crate::lxc::LxcManager; -use crate::net::gateway::UpgradableListener; +use crate::net::gateway::WildcardListener; use crate::net::net_controller::{NetController, NetService}; use crate::net::socks::DEFAULT_SOCKS_LISTEN; use crate::net::utils::{find_eth_iface, find_wifi_iface}; @@ -132,7 +132,7 @@ pub struct RpcContext(Arc); impl RpcContext { #[instrument(skip_all)] pub async fn init( - webserver: &WebServerAcceptorSetter, + webserver: &WebServerAcceptorSetter, config: &ServerConfig, disk_guid: InternedString, init_result: Option, @@ -167,7 +167,7 @@ impl RpcContext { } else { let net_ctrl = Arc::new(NetController::init(db.clone(), &account.hostname, socks_proxy).await?); - webserver.try_upgrade(|a| net_ctrl.net_iface.watcher.upgrade_listener(a))?; + webserver.send_modify(|wl| wl.set_ip_info(net_ctrl.net_iface.watcher.subscribe())); let os_net_service = net_ctrl.os_bindings().await?; (net_ctrl, os_net_service) }; diff --git a/core/src/context/setup.rs b/core/src/context/setup.rs index bbfee9862..39a6b5fd3 100644 --- a/core/src/context/setup.rs +++ b/core/src/context/setup.rs @@ -20,7 +20,7 @@ use crate::context::RpcContext; use crate::context::config::ServerConfig; use crate::disk::mount::guard::{MountGuard, TmpMountGuard}; use crate::hostname::Hostname; -use crate::net::gateway::UpgradableListener; +use crate::net::gateway::WildcardListener; use crate::net::web_server::{WebServer, WebServerAcceptorSetter}; use crate::prelude::*; use crate::progress::FullProgressTracker; @@ -51,7 +51,7 @@ pub struct SetupResult { } pub struct SetupContextSeed { - pub webserver: WebServerAcceptorSetter, + pub webserver: WebServerAcceptorSetter, pub config: SyncMutex, pub disable_encryption: bool, pub progress: FullProgressTracker, @@ -70,7 +70,7 @@ pub struct SetupContext(Arc); impl SetupContext { #[instrument(skip_all)] pub fn init( - webserver: &WebServer, + webserver: &WebServer, config: ServerConfig, ) -> Result { let (shutdown, _) = tokio::sync::broadcast::channel(1); diff --git a/core/src/init.rs b/core/src/init.rs index 39680015e..e9507ef49 100644 --- a/core/src/init.rs +++ b/core/src/init.rs @@ -20,7 +20,7 @@ use crate::db::model::public::ServerStatus; use crate::developer::OS_DEVELOPER_KEY_PATH; use crate::hostname::Hostname; use crate::middleware::auth::local::LocalAuthContext; -use crate::net::gateway::UpgradableListener; +use crate::net::gateway::WildcardListener; use crate::net::net_controller::{NetController, NetService}; use crate::net::socks::DEFAULT_SOCKS_LISTEN; use crate::net::utils::find_wifi_iface; @@ -144,7 +144,7 @@ pub async fn run_script>(path: P, mut progress: PhaseProgressTrac #[instrument(skip_all)] pub async fn init( - webserver: &WebServerAcceptorSetter, + webserver: &WebServerAcceptorSetter, cfg: &ServerConfig, InitPhases { preinit, @@ -218,7 +218,7 @@ pub async fn init( ) .await?, ); - webserver.try_upgrade(|a| net_ctrl.net_iface.watcher.upgrade_listener(a))?; + webserver.send_modify(|wl| wl.set_ip_info(net_ctrl.net_iface.watcher.subscribe())); let os_net_service = net_ctrl.os_bindings().await?; start_net.complete(); diff --git a/core/src/net/forward.rs b/core/src/net/forward.rs index 7cf5e7985..b9fe30a37 100644 --- a/core/src/net/forward.rs +++ b/core/src/net/forward.rs @@ -15,7 +15,6 @@ use tokio::sync::mpsc; use crate::GatewayId; use crate::context::{CliContext, RpcContext}; use crate::db::model::public::NetworkInterfaceInfo; -use crate::net::gateway::{DynInterfaceFilter, InterfaceFilter}; use crate::prelude::*; use crate::util::Invoke; use crate::util::future::NonDetachingJoinHandle; @@ -31,6 +30,33 @@ fn is_restricted(port: u16) -> bool { port <= 1024 || RESTRICTED_PORTS.contains(&port) } +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct ForwardRequirements { + pub public_gateways: BTreeSet, + pub private_ips: BTreeSet, + pub secure: bool, +} + +impl std::fmt::Display for ForwardRequirements { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "ForwardRequirements {{ public: {:?}, private: {:?}, secure: {} }}", + self.public_gateways, self.private_ips, self.secure + ) + } +} + +/// Source-IP filter for private forwards: restricts traffic to a subnet +/// while excluding gateway/router IPs that may masquerade internet traffic. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct SourceFilter { + /// Network CIDR to allow (e.g. "192.168.1.0/24") + subnet: String, + /// Comma-separated gateway IPs to exclude (they may masquerade internet traffic) + excluded: String, +} + #[derive(Debug, Deserialize, Serialize)] pub struct AvailablePorts(BTreeMap); impl AvailablePorts { @@ -85,10 +111,10 @@ pub fn forward_api() -> ParentHandler { } let mut table = Table::new(); - table.add_row(row![bc => "FROM", "TO", "FILTER"]); + table.add_row(row![bc => "FROM", "TO", "REQS"]); for (external, target) in res.0 { - table.add_row(row![external, target.target, target.filter]); + table.add_row(row![external, target.target, target.reqs]); } table.print_tty(false)?; @@ -103,6 +129,7 @@ struct ForwardMapping { source: SocketAddrV4, target: SocketAddrV4, target_prefix: u8, + src_filter: Option, rc: Weak<()>, } @@ -117,9 +144,10 @@ impl PortForwardState { source: SocketAddrV4, target: SocketAddrV4, target_prefix: u8, + src_filter: Option, ) -> Result, Error> { if let Some(existing) = self.mappings.get_mut(&source) { - if existing.target == target { + if existing.target == target && existing.src_filter == src_filter { if let Some(existing_rc) = existing.rc.upgrade() { return Ok(existing_rc); } else { @@ -128,21 +156,28 @@ impl PortForwardState { return Ok(rc); } } else { - // Different target, need to remove old and add new + // Different target or src_filter, need to remove old and add new if let Some(mapping) = self.mappings.remove(&source) { - unforward(mapping.source, mapping.target, mapping.target_prefix).await?; + unforward( + mapping.source, + mapping.target, + mapping.target_prefix, + mapping.src_filter.as_ref(), + ) + .await?; } } } let rc = Arc::new(()); - forward(source, target, target_prefix).await?; + forward(source, target, target_prefix, src_filter.as_ref()).await?; self.mappings.insert( source, ForwardMapping { source, target, target_prefix, + src_filter, rc: Arc::downgrade(&rc), }, ); @@ -160,7 +195,13 @@ impl PortForwardState { for source in to_remove { if let Some(mapping) = self.mappings.remove(&source) { - unforward(mapping.source, mapping.target, mapping.target_prefix).await?; + unforward( + mapping.source, + mapping.target, + mapping.target_prefix, + mapping.src_filter.as_ref(), + ) + .await?; } } Ok(()) @@ -181,9 +222,14 @@ impl Drop for PortForwardState { let mappings = std::mem::take(&mut self.mappings); tokio::spawn(async move { for (_, mapping) in mappings { - unforward(mapping.source, mapping.target, mapping.target_prefix) - .await - .log_err(); + unforward( + mapping.source, + mapping.target, + mapping.target_prefix, + mapping.src_filter.as_ref(), + ) + .await + .log_err(); } }); } @@ -195,6 +241,7 @@ enum PortForwardCommand { source: SocketAddrV4, target: SocketAddrV4, target_prefix: u8, + src_filter: Option, respond: oneshot::Sender, Error>>, }, Gc { @@ -281,9 +328,12 @@ impl PortForwardController { source, target, target_prefix, + src_filter, respond, } => { - let result = state.add_forward(source, target, target_prefix).await; + let result = state + .add_forward(source, target, target_prefix, src_filter) + .await; respond.send(result).ok(); } PortForwardCommand::Gc { respond } => { @@ -308,6 +358,7 @@ impl PortForwardController { source: SocketAddrV4, target: SocketAddrV4, target_prefix: u8, + src_filter: Option, ) -> Result, Error> { let (send, recv) = oneshot::channel(); self.req @@ -315,6 +366,7 @@ impl PortForwardController { source, target, target_prefix, + src_filter, respond: send, }) .map_err(err_has_exited)?; @@ -345,14 +397,14 @@ struct InterfaceForwardRequest { external: u16, target: SocketAddrV4, target_prefix: u8, - filter: DynInterfaceFilter, + reqs: ForwardRequirements, rc: Arc<()>, } #[derive(Clone)] struct InterfaceForwardEntry { external: u16, - filter: BTreeMap)>, + targets: BTreeMap)>, // Maps source SocketAddr -> strong reference for the forward created in PortForwardController forwards: BTreeMap>, } @@ -370,7 +422,7 @@ impl InterfaceForwardEntry { fn new(external: u16) -> Self { Self { external, - filter: BTreeMap::new(), + targets: BTreeMap::new(), forwards: BTreeMap::new(), } } @@ -382,28 +434,50 @@ impl InterfaceForwardEntry { ) -> Result<(), Error> { let mut keep = BTreeSet::::new(); - for (iface, info) in ip_info.iter() { - if let Some((target, target_prefix)) = self - .filter - .iter() - .filter(|(_, (_, _, rc))| rc.strong_count() > 0) - .find(|(filter, _)| filter.filter(iface, info)) - .map(|(_, (target, target_prefix, _))| (*target, *target_prefix)) - { - if let Some(ip_info) = &info.ip_info { - for addr in ip_info.subnets.iter().filter_map(|net| { - if let IpAddr::V4(ip) = net.addr() { - Some(SocketAddrV4::new(ip, self.external)) - } else { - None + for (gw_id, info) in ip_info.iter() { + if let Some(ip_info) = &info.ip_info { + for subnet in ip_info.subnets.iter() { + if let IpAddr::V4(ip) = subnet.addr() { + let addr = SocketAddrV4::new(ip, self.external); + if keep.contains(&addr) { + continue; } - }) { - keep.insert(addr); - if !self.forwards.contains_key(&addr) { - let rc = port_forward - .add_forward(addr, target, target_prefix) + + for (reqs, (target, target_prefix, rc)) in self.targets.iter() { + if rc.strong_count() == 0 { + continue; + } + if !reqs.secure && !info.secure() { + continue; + } + + let src_filter = + if reqs.public_gateways.contains(gw_id) { + None + } else if reqs.private_ips.contains(&IpAddr::V4(ip)) { + let excluded = ip_info + .lan_ip + .iter() + .filter_map(|ip| match ip { + IpAddr::V4(v4) => Some(v4.to_string()), + _ => None, + }) + .collect::>() + .join(","); + Some(SourceFilter { + subnet: subnet.trunc().to_string(), + excluded, + }) + } else { + continue; + }; + + keep.insert(addr); + let fwd_rc = port_forward + .add_forward(addr, *target, *target_prefix, src_filter) .await?; - self.forwards.insert(addr, rc); + self.forwards.insert(addr, fwd_rc); + break; } } } @@ -422,7 +496,7 @@ impl InterfaceForwardEntry { external, target, target_prefix, - filter, + reqs, mut rc, }: InterfaceForwardRequest, ip_info: &OrdMap, @@ -436,8 +510,8 @@ impl InterfaceForwardEntry { } let entry = self - .filter - .entry(filter) + .targets + .entry(reqs) .or_insert_with(|| (target, target_prefix, Arc::downgrade(&rc))); if entry.0 != target { entry.0 = target; @@ -460,7 +534,7 @@ impl InterfaceForwardEntry { ip_info: &OrdMap, port_forward: &PortForwardController, ) -> Result<(), Error> { - self.filter.retain(|_, (_, _, rc)| rc.strong_count() > 0); + self.targets.retain(|_, (_, _, rc)| rc.strong_count() > 0); self.update(ip_info, port_forward).await } @@ -519,7 +593,7 @@ pub struct ForwardTable(pub BTreeMap); pub struct ForwardTarget { pub target: SocketAddrV4, pub target_prefix: u8, - pub filter: String, + pub reqs: String, } impl From<&InterfaceForwardState> for ForwardTable { @@ -530,16 +604,16 @@ impl From<&InterfaceForwardState> for ForwardTable { .iter() .flat_map(|entry| { entry - .filter + .targets .iter() .filter(|(_, (_, _, rc))| rc.strong_count() > 0) - .map(|(filter, (target, target_prefix, _))| { + .map(|(reqs, (target, target_prefix, _))| { ( entry.external, ForwardTarget { target: *target, target_prefix: *target_prefix, - filter: format!("{:#?}", filter), + reqs: format!("{reqs}"), }, ) }) @@ -558,16 +632,6 @@ enum InterfaceForwardCommand { DumpTable(oneshot::Sender), } -#[test] -fn test() { - use crate::net::gateway::SecureFilter; - - assert_ne!( - false.into_dyn(), - SecureFilter { secure: false }.into_dyn().into_dyn() - ); -} - pub struct InterfacePortForwardController { req: mpsc::UnboundedSender, _thread: NonDetachingJoinHandle<()>, @@ -617,7 +681,7 @@ impl InterfacePortForwardController { pub async fn add( &self, external: u16, - filter: DynInterfaceFilter, + reqs: ForwardRequirements, target: SocketAddrV4, target_prefix: u8, ) -> Result, Error> { @@ -629,7 +693,7 @@ impl InterfacePortForwardController { external, target, target_prefix, - filter, + reqs, rc, }, send, @@ -661,15 +725,21 @@ async fn forward( source: SocketAddrV4, target: SocketAddrV4, target_prefix: u8, + src_filter: Option<&SourceFilter>, ) -> Result<(), Error> { - Command::new("/usr/lib/startos/scripts/forward-port") - .env("sip", source.ip().to_string()) + let mut cmd = Command::new("/usr/lib/startos/scripts/forward-port"); + cmd.env("sip", source.ip().to_string()) .env("dip", target.ip().to_string()) .env("dprefix", target_prefix.to_string()) .env("sport", source.port().to_string()) - .env("dport", target.port().to_string()) - .invoke(ErrorKind::Network) - .await?; + .env("dport", target.port().to_string()); + if let Some(filter) = src_filter { + cmd.env("src_subnet", &filter.subnet); + if !filter.excluded.is_empty() { + cmd.env("excluded_src", &filter.excluded); + } + } + cmd.invoke(ErrorKind::Network).await?; Ok(()) } @@ -677,15 +747,21 @@ async fn unforward( source: SocketAddrV4, target: SocketAddrV4, target_prefix: u8, + src_filter: Option<&SourceFilter>, ) -> Result<(), Error> { - Command::new("/usr/lib/startos/scripts/forward-port") - .env("UNDO", "1") + let mut cmd = Command::new("/usr/lib/startos/scripts/forward-port"); + cmd.env("UNDO", "1") .env("sip", source.ip().to_string()) .env("dip", target.ip().to_string()) .env("dprefix", target_prefix.to_string()) .env("sport", source.port().to_string()) - .env("dport", target.port().to_string()) - .invoke(ErrorKind::Network) - .await?; + .env("dport", target.port().to_string()); + if let Some(filter) = src_filter { + cmd.env("src_subnet", &filter.subnet); + if !filter.excluded.is_empty() { + cmd.env("excluded_src", &filter.excluded); + } + } + cmd.invoke(ErrorKind::Network).await?; Ok(()) } diff --git a/core/src/net/gateway.rs b/core/src/net/gateway.rs index 688892f85..e9c2575ba 100644 --- a/core/src/net/gateway.rs +++ b/core/src/net/gateway.rs @@ -1,14 +1,11 @@ -use std::any::Any; use std::collections::{BTreeMap, BTreeSet, HashMap}; -use std::fmt; use std::future::Future; -use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV6}; -use std::sync::{Arc, Weak}; -use std::task::{Poll, ready}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::sync::Arc; +use std::task::Poll; use std::time::Duration; use clap::Parser; -use futures::future::Either; use futures::{FutureExt, Stream, StreamExt, TryStreamExt}; use imbl::{OrdMap, OrdSet}; use imbl_value::InternedString; @@ -36,15 +33,14 @@ use crate::db::model::Database; use crate::db::model::public::{IpInfo, NetworkInterfaceInfo, NetworkInterfaceType}; use crate::net::forward::START9_BRIDGE_IFACE; use crate::net::gateway::device::DeviceProxy; -use crate::net::utils::ipv6_is_link_local; -use crate::net::web_server::{Accept, AcceptStream, Acceptor, MetadataVisitor}; +use crate::net::web_server::{Accept, AcceptStream, MetadataVisitor, TcpMetadata}; use crate::prelude::*; use crate::util::Invoke; use crate::util::collections::OrdMapIterMut; use crate::util::future::{NonDetachingJoinHandle, Until}; use crate::util::io::open_file; use crate::util::serde::{HandlerExtSerde, display_serializable}; -use crate::util::sync::{SyncMutex, Watch}; +use crate::util::sync::Watch; pub fn gateway_api() -> ParentHandler { ParentHandler::new() @@ -838,7 +834,6 @@ pub struct NetworkInterfaceWatcher { activated: Watch>, ip_info: Watch>, _watcher: NonDetachingJoinHandle<()>, - listeners: SyncMutex>>, } impl NetworkInterfaceWatcher { pub fn new( @@ -858,7 +853,6 @@ impl NetworkInterfaceWatcher { watcher(ip_info, activated).await }) .into(), - listeners: SyncMutex::new(BTreeMap::new()), } } @@ -885,51 +879,6 @@ impl NetworkInterfaceWatcher { pub fn ip_info(&self) -> OrdMap { self.ip_info.read() } - - pub fn bind(&self, bind: B, port: u16) -> Result, Error> { - let arc = Arc::new(()); - self.listeners.mutate(|l| { - if l.get(&port).filter(|w| w.strong_count() > 0).is_some() { - return Err(Error::new( - std::io::Error::from_raw_os_error(libc::EADDRINUSE), - ErrorKind::Network, - )); - } - l.insert(port, Arc::downgrade(&arc)); - Ok(()) - })?; - let ip_info = self.ip_info.clone_unseen(); - Ok(NetworkInterfaceListener { - _arc: arc, - ip_info, - listeners: ListenerMap::new(bind, port), - }) - } - - pub fn upgrade_listener( - &self, - SelfContainedNetworkInterfaceListener { - mut listener, - .. - }: SelfContainedNetworkInterfaceListener, - ) -> Result, Error> { - let port = listener.listeners.port; - let arc = &listener._arc; - self.listeners.mutate(|l| { - if l.get(&port).filter(|w| w.strong_count() > 0).is_some() { - return Err(Error::new( - std::io::Error::from_raw_os_error(libc::EADDRINUSE), - ErrorKind::Network, - )); - } - l.insert(port, Arc::downgrade(arc)); - Ok(()) - })?; - let ip_info = self.ip_info.clone_unseen(); - ip_info.mark_changed(); - listener.change_ip_info_source(ip_info); - Ok(listener) - } } pub struct NetworkInterfaceController { @@ -1237,235 +1186,6 @@ impl NetworkInterfaceController { } } -pub trait InterfaceFilter: Any + Clone + std::fmt::Debug + Eq + Ord + Send + Sync { - fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool; - fn eq(&self, other: &dyn Any) -> bool { - Some(self) == other.downcast_ref::() - } - fn cmp(&self, other: &dyn Any) -> std::cmp::Ordering { - match (self as &dyn Any).type_id().cmp(&other.type_id()) { - std::cmp::Ordering::Equal => { - std::cmp::Ord::cmp(self, other.downcast_ref::().unwrap()) - } - ord => ord, - } - } - fn as_any(&self) -> &dyn Any { - self - } - fn into_dyn(self) -> DynInterfaceFilter { - DynInterfaceFilter::new(self) - } -} - -impl InterfaceFilter for bool { - fn filter(&self, _: &GatewayId, _: &NetworkInterfaceInfo) -> bool { - *self - } -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct TypeFilter(pub NetworkInterfaceType); -impl InterfaceFilter for TypeFilter { - fn filter(&self, _: &GatewayId, info: &NetworkInterfaceInfo) -> bool { - info.ip_info.as_ref().and_then(|i| i.device_type) == Some(self.0) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct IdFilter(pub GatewayId); -impl InterfaceFilter for IdFilter { - fn filter(&self, id: &GatewayId, _: &NetworkInterfaceInfo) -> bool { - id == &self.0 - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct PublicFilter { - pub public: bool, -} -impl InterfaceFilter for PublicFilter { - fn filter(&self, _: &GatewayId, info: &NetworkInterfaceInfo) -> bool { - self.public == info.public() - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct SecureFilter { - pub secure: bool, -} -impl InterfaceFilter for SecureFilter { - fn filter(&self, _: &GatewayId, info: &NetworkInterfaceInfo) -> bool { - self.secure || info.secure() - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct AndFilter(pub A, pub B); -impl InterfaceFilter for AndFilter { - fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool { - self.0.filter(id, info) && self.1.filter(id, info) - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct OrFilter(pub A, pub B); -impl InterfaceFilter for OrFilter { - fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool { - self.0.filter(id, info) || self.1.filter(id, info) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct AnyFilter(pub BTreeSet); -impl InterfaceFilter for AnyFilter { - fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool { - self.0.iter().any(|f| InterfaceFilter::filter(f, id, info)) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct AllFilter(pub BTreeSet); -impl InterfaceFilter for AllFilter { - fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool { - self.0.iter().all(|f| InterfaceFilter::filter(f, id, info)) - } -} - -pub trait DynInterfaceFilterT: std::fmt::Debug + Any + Send + Sync { - fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool; - fn eq(&self, other: &dyn Any) -> bool; - fn cmp(&self, other: &dyn Any) -> std::cmp::Ordering; - fn as_any(&self) -> &dyn Any; -} -impl DynInterfaceFilterT for T { - fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool { - InterfaceFilter::filter(self, id, info) - } - fn eq(&self, other: &dyn Any) -> bool { - InterfaceFilter::eq(self, other) - } - fn cmp(&self, other: &dyn Any) -> std::cmp::Ordering { - InterfaceFilter::cmp(self, other) - } - fn as_any(&self) -> &dyn Any { - InterfaceFilter::as_any(self) - } -} - -#[test] -fn test_interface_filter_eq() { - let dyn_t = true.into_dyn(); - assert!(DynInterfaceFilterT::eq( - &dyn_t, - DynInterfaceFilterT::as_any(&true), - )) -} - -#[derive(Clone, Debug)] -pub struct DynInterfaceFilter(Arc); -impl InterfaceFilter for DynInterfaceFilter { - fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool { - self.0.filter(id, info) - } - fn eq(&self, other: &dyn Any) -> bool { - self.0.eq(other) - } - fn cmp(&self, other: &dyn Any) -> std::cmp::Ordering { - self.0.cmp(other) - } - fn as_any(&self) -> &dyn Any { - self.0.as_any() - } - fn into_dyn(self) -> DynInterfaceFilter { - self - } -} -impl DynInterfaceFilter { - fn new(value: T) -> Self { - Self(Arc::new(value)) - } -} -impl PartialEq for DynInterfaceFilter { - fn eq(&self, other: &Self) -> bool { - DynInterfaceFilterT::eq(&*self.0, DynInterfaceFilterT::as_any(&*other.0)) - } -} -impl Eq for DynInterfaceFilter {} -impl PartialOrd for DynInterfaceFilter { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.0.cmp(other.0.as_any())) - } -} -impl Ord for DynInterfaceFilter { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.0.cmp(other.0.as_any()) - } -} - -struct ListenerMap { - prev_filter: DynInterfaceFilter, - bind: B, - port: u16, - listeners: BTreeMap, -} -impl ListenerMap { - fn new(bind: B, port: u16) -> Self { - Self { - prev_filter: false.into_dyn(), - bind, - port, - listeners: BTreeMap::new(), - } - } - - #[instrument(skip(self))] - fn update( - &mut self, - ip_info: &OrdMap, - filter: &impl InterfaceFilter, - ) -> Result<(), Error> { - let mut keep = BTreeSet::::new(); - for (_, info) in ip_info - .iter() - .filter(|(id, info)| filter.filter(*id, *info)) - { - if let Some(ip_info) = &info.ip_info { - for ipnet in &ip_info.subnets { - let addr = match ipnet.addr() { - IpAddr::V6(ip6) => SocketAddrV6::new( - ip6, - self.port, - 0, - if ipv6_is_link_local(ip6) { - ip_info.scope_id - } else { - 0 - }, - ) - .into(), - ip => SocketAddr::new(ip, self.port), - }; - keep.insert(addr); - if !self.listeners.contains_key(&addr) { - self.listeners.insert(addr, self.bind.bind(addr)?); - } - } - } - } - self.listeners.retain(|key, _| keep.contains(key)); - self.prev_filter = filter.clone().into_dyn(); - Ok(()) - } - fn poll_accept( - &mut self, - cx: &mut std::task::Context<'_>, - ) -> Poll::Metadata, AcceptStream), Error>> { - let (metadata, stream) = ready!(self.listeners.poll_accept(cx)?); - Poll::Ready(Ok((metadata.key, metadata.inner, stream))) - } -} - pub fn lookup_info_by_addr( ip_info: &OrdMap, addr: SocketAddr, @@ -1477,28 +1197,6 @@ pub fn lookup_info_by_addr( }) } -pub trait Bind { - type Accept: Accept; - fn bind(&mut self, addr: SocketAddr) -> Result; -} - -#[derive(Clone, Copy, Default)] -pub struct BindTcp; -impl Bind for BindTcp { - type Accept = TcpListener; - fn bind(&mut self, addr: SocketAddr) -> Result { - TcpListener::from_std( - mio::net::TcpListener::bind(addr) - .with_kind(ErrorKind::Network)? - .into(), - ) - .with_kind(ErrorKind::Network) - } -} - -pub trait FromGatewayInfo { - fn from_gateway_info(id: &GatewayId, info: &NetworkInterfaceInfo) -> Self; -} #[derive(Clone, Debug)] pub struct GatewayInfo { pub id: GatewayId, @@ -1509,202 +1207,88 @@ impl Visit for GatewayInfo { visitor.visit(self) } } -impl FromGatewayInfo for GatewayInfo { - fn from_gateway_info(id: &GatewayId, info: &NetworkInterfaceInfo) -> Self { - Self { - id: id.clone(), - info: info.clone(), - } - } -} -pub struct NetworkInterfaceListener { - pub ip_info: Watch>, - listeners: ListenerMap, - _arc: Arc<()>, -} -impl NetworkInterfaceListener { - pub(super) fn new( - mut ip_info: Watch>, - bind: B, - port: u16, - ) -> Self { - ip_info.mark_unseen(); - Self { - ip_info, - listeners: ListenerMap::new(bind, port), - _arc: Arc::new(()), - } - } - - pub fn port(&self) -> u16 { - self.listeners.port - } - - #[cfg_attr(feature = "unstable", inline(never))] - pub fn poll_accept( - &mut self, - cx: &mut std::task::Context<'_>, - filter: &impl InterfaceFilter, - ) -> Poll::Metadata, AcceptStream), Error>> { - while self.ip_info.poll_changed(cx).is_ready() - || !DynInterfaceFilterT::eq(&self.listeners.prev_filter, filter.as_any()) - { - self.ip_info - .peek_and_mark_seen(|ip_info| self.listeners.update(ip_info, filter))?; - } - let (addr, inner, stream) = ready!(self.listeners.poll_accept(cx)?); - Poll::Ready(Ok(( - self.ip_info - .peek(|ip_info| { - lookup_info_by_addr(ip_info, addr) - .map(|(id, info)| M::from_gateway_info(id, info)) - }) - .or_not_found(lazy_format!("gateway for {addr}"))?, - inner, - stream, - ))) - } - - pub fn change_ip_info_source( - &mut self, - mut ip_info: Watch>, - ) { - ip_info.mark_unseen(); - self.ip_info = ip_info; - } - - pub async fn accept( - &mut self, - filter: &impl InterfaceFilter, - ) -> Result<(M, ::Metadata, AcceptStream), Error> { - futures::future::poll_fn(|cx| self.poll_accept(cx, filter)).await - } - - pub fn check_filter(&self) -> impl FnOnce(SocketAddr, &DynInterfaceFilter) -> bool + 'static { - let ip_info = self.ip_info.clone(); - move |addr, filter| { - ip_info.peek(|i| { - lookup_info_by_addr(i, addr).map_or(false, |(id, info)| { - InterfaceFilter::filter(filter, id, info) - }) - }) - } - } -} - -#[derive(VisitFields)] -pub struct NetworkInterfaceListenerAcceptMetadata { - pub inner: ::Metadata, +/// Metadata for connections accepted by WildcardListener or VHostBindListener. +#[derive(Clone, Debug, VisitFields)] +pub struct NetworkInterfaceListenerAcceptMetadata { + pub inner: TcpMetadata, pub info: GatewayInfo, } -impl fmt::Debug for NetworkInterfaceListenerAcceptMetadata { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("NetworkInterfaceListenerAcceptMetadata") - .field("inner", &self.inner) - .field("info", &self.info) - .finish() - } -} -impl Clone for NetworkInterfaceListenerAcceptMetadata -where - ::Metadata: Clone, -{ - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - info: self.info.clone(), - } - } -} -impl Visit for NetworkInterfaceListenerAcceptMetadata -where - B: Bind, - ::Metadata: Visit + Clone + Send + Sync + 'static, - V: MetadataVisitor, -{ +impl Visit for NetworkInterfaceListenerAcceptMetadata { fn visit(&self, visitor: &mut V) -> V::Result { self.visit_fields(visitor).collect() } } -impl Accept for NetworkInterfaceListener { - type Metadata = NetworkInterfaceListenerAcceptMetadata; +/// A simple TCP listener on 0.0.0.0:port that looks up GatewayInfo from the +/// connection's local address on each accepted connection. +pub struct WildcardListener { + listener: TcpListener, + ip_info: Watch>, + /// Handle to the self-contained watcher task started in `new()`. + /// Dropped (and thus aborted) when `set_ip_info` replaces the ip_info source. + _watcher: Option>, +} +impl WildcardListener { + pub fn new(port: u16) -> Result { + let listener = TcpListener::from_std( + mio::net::TcpListener::bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port)) + .with_kind(ErrorKind::Network)? + .into(), + ) + .with_kind(ErrorKind::Network)?; + let ip_info = Watch::new(OrdMap::new()); + let watcher_handle = + tokio::spawn(watcher(ip_info.clone(), Watch::new(BTreeMap::new()))).into(); + Ok(Self { + listener, + ip_info, + _watcher: Some(watcher_handle), + }) + } + + /// Replace the ip_info source with the one from the NetworkInterfaceController. + /// Aborts the self-contained watcher task. + pub fn set_ip_info(&mut self, ip_info: Watch>) { + self.ip_info = ip_info; + self._watcher = None; + } +} +impl Accept for WildcardListener { + type Metadata = NetworkInterfaceListenerAcceptMetadata; fn poll_accept( &mut self, cx: &mut std::task::Context<'_>, ) -> Poll> { - NetworkInterfaceListener::poll_accept(self, cx, &true).map(|res| { - res.map(|(info, inner, stream)| { - ( - NetworkInterfaceListenerAcceptMetadata { inner, info }, - stream, - ) - }) - }) - } -} - -pub struct SelfContainedNetworkInterfaceListener { - _watch_thread: NonDetachingJoinHandle<()>, - listener: NetworkInterfaceListener, -} -impl SelfContainedNetworkInterfaceListener { - pub fn bind(bind: B, port: u16) -> Self { - let ip_info = Watch::new(OrdMap::new()); - let _watch_thread = - tokio::spawn(watcher(ip_info.clone(), Watch::new(BTreeMap::new()))).into(); - Self { - _watch_thread, - listener: NetworkInterfaceListener::new(ip_info, bind, port), + if let Poll::Ready((stream, peer_addr)) = TcpListener::poll_accept(&self.listener, cx)? { + if let Err(e) = socket2::SockRef::from(&stream).set_keepalive(true) { + tracing::error!("Failed to set tcp keepalive: {e}"); + tracing::debug!("{e:?}"); + } + let local_addr = stream.local_addr()?; + let info = self + .ip_info + .peek(|ip_info| { + lookup_info_by_addr(ip_info, local_addr).map(|(id, info)| GatewayInfo { + id: id.clone(), + info: info.clone(), + }) + }) + .unwrap_or_else(|| GatewayInfo { + id: InternedString::from_static("").into(), + info: NetworkInterfaceInfo::default(), + }); + return Poll::Ready(Ok(( + NetworkInterfaceListenerAcceptMetadata { + inner: TcpMetadata { + local_addr, + peer_addr, + }, + info, + }, + Box::pin(stream), + ))); } + Poll::Pending } } -impl Accept for SelfContainedNetworkInterfaceListener { - type Metadata = as Accept>::Metadata; - fn poll_accept( - &mut self, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - Accept::poll_accept(&mut self.listener, cx) - } -} - -pub type UpgradableListener = - Option, NetworkInterfaceListener>>; - -impl Acceptor> -where - B: Bind + Send + Sync + 'static, - B::Accept: Send + Sync, -{ - pub fn bind_upgradable(listener: SelfContainedNetworkInterfaceListener) -> Self { - Self::new(Some(Either::Left(listener))) - } -} - -#[test] -fn test_filter() { - let wg1 = "wg1".parse::().unwrap(); - assert!(!InterfaceFilter::filter( - &AndFilter(IdFilter(wg1.clone()), PublicFilter { public: false }).into_dyn(), - &wg1, - &NetworkInterfaceInfo { - name: None, - public: None, - secure: None, - ip_info: Some(Arc::new(IpInfo { - name: "".into(), - scope_id: 3, - device_type: Some(NetworkInterfaceType::Wireguard), - subnets: ["10.59.0.2/24".parse::().unwrap()] - .into_iter() - .collect(), - lan_ip: Default::default(), - wan_ip: None, - ntp_servers: Default::default(), - dns_servers: Default::default(), - })), - }, - )); -} diff --git a/core/src/net/host/binding.rs b/core/src/net/host/binding.rs index 3c78c4338..8db806399 100644 --- a/core/src/net/host/binding.rs +++ b/core/src/net/host/binding.rs @@ -8,17 +8,15 @@ use serde::{Deserialize, Serialize}; use ts_rs::TS; use crate::context::{CliContext, RpcContext}; -use crate::db::model::public::NetworkInterfaceInfo; use crate::db::prelude::Map; use crate::net::forward::AvailablePorts; -use crate::net::gateway::InterfaceFilter; use crate::net::host::HostApiKind; use crate::net::service_interface::HostnameInfo; use crate::net::vhost::AlpnInfo; use crate::prelude::*; use crate::util::FromStrParser; use crate::util::serde::{HandlerExtSerde, display_serializable}; -use crate::{GatewayId, HostId}; +use crate::HostId; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, TS)] #[ts(export)] @@ -51,9 +49,9 @@ impl FromStr for BindId { #[ts(export)] #[model = "Model"] pub struct DerivedAddressInfo { - /// User-controlled: private-gateway addresses the user has disabled + /// User-controlled: private addresses the user has disabled pub private_disabled: BTreeSet, - /// User-controlled: public-gateway addresses the user has enabled + /// User-controlled: public addresses the user has enabled pub public_enabled: BTreeSet, /// COMPUTED: NetServiceData::update — all possible addresses for this binding pub possible: BTreeSet, @@ -76,26 +74,6 @@ impl DerivedAddressInfo { .collect() } - /// Derive a gateway-level InterfaceFilter from the enabled addresses. - /// A gateway passes the filter if it has any enabled address for this binding. - pub fn gateway_filter(&self) -> AddressFilter { - let enabled_gateways: BTreeSet = self - .enabled() - .into_iter() - .map(|h| h.gateway.id.clone()) - .collect(); - AddressFilter(enabled_gateways) - } -} - -/// Gateway-level filter derived from DerivedAddressInfo. -/// Passes if the gateway has at least one enabled address. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct AddressFilter(pub BTreeSet); -impl InterfaceFilter for AddressFilter { - fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool { - info.ip_info.is_some() && self.0.contains(id) - } } #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] @@ -145,12 +123,6 @@ pub struct NetInfo { pub assigned_port: Option, pub assigned_ssl_port: Option, } -impl InterfaceFilter for NetInfo { - fn filter(&self, _id: &GatewayId, info: &NetworkInterfaceInfo) -> bool { - info.ip_info.is_some() - } -} - impl BindInfo { pub fn new(available_ports: &mut AvailablePorts, options: BindOptions) -> Result { let mut assigned_port = None; diff --git a/core/src/net/net_controller.rs b/core/src/net/net_controller.rs index 56c30ed4e..37c63f333 100644 --- a/core/src/net/net_controller.rs +++ b/core/src/net/net_controller.rs @@ -16,11 +16,10 @@ use crate::db::model::public::NetworkInterfaceType; use crate::error::ErrorCollection; use crate::hostname::Hostname; use crate::net::dns::DnsController; -use crate::net::forward::{InterfacePortForwardController, START9_BRIDGE_IFACE, add_iptables_rule}; -use crate::net::gateway::{ - AndFilter, AnyFilter, DynInterfaceFilter, IdFilter, InterfaceFilter, - NetworkInterfaceController, OrFilter, PublicFilter, SecureFilter, +use crate::net::forward::{ + ForwardRequirements, InterfacePortForwardController, START9_BRIDGE_IFACE, add_iptables_rule, }; +use crate::net::gateway::NetworkInterfaceController; use crate::net::host::address::HostAddress; use crate::net::host::binding::{AddSslOptions, BindId, BindOptions}; use crate::net::host::{Host, Hosts, host_for}; @@ -31,7 +30,7 @@ use crate::net::vhost::{AlpnInfo, DynVHostTarget, ProxyTarget, VHostController}; use crate::prelude::*; use crate::service::effects::callbacks::ServiceCallbacks; use crate::util::serde::MaybeUtf8String; -use crate::{HOST_IP, HostId, OptionExt, PackageId}; +use crate::{GatewayId, HOST_IP, HostId, OptionExt, PackageId}; pub struct NetController { pub(crate) db: TypedPatchDb, @@ -161,7 +160,7 @@ impl NetController { #[derive(Default, Debug)] struct HostBinds { - forwards: BTreeMap)>, + forwards: BTreeMap)>, vhosts: BTreeMap<(Option, u16), (ProxyTarget, Arc<()>)>, private_dns: BTreeMap>, } @@ -257,213 +256,36 @@ impl NetServiceData { id: HostId, mut host: Host, ) -> Result<(), Error> { - let mut forwards: BTreeMap = BTreeMap::new(); + let mut forwards: BTreeMap = BTreeMap::new(); let mut vhosts: BTreeMap<(Option, u16), ProxyTarget> = BTreeMap::new(); let mut private_dns: BTreeSet = BTreeSet::new(); let binds = self.binds.entry(id.clone()).or_default(); let peek = ctrl.db.peek().await; - - // LAN let server_info = peek.as_public().as_server_info(); let net_ifaces = ctrl.net_iface.watcher.ip_info(); let hostname = server_info.as_hostname().de()?; let host_addresses: Vec<_> = host.addresses().collect(); - for (port, bind) in host.bindings.iter_mut() { + + // Collect private DNS entries (domains without public config) + for HostAddress { + address, public, .. + } in &host_addresses + { + if public.is_none() { + private_dns.insert(address.clone()); + } + } + + // ── Phase 1: Compute possible addresses ── + for (_port, bind) in host.bindings.iter_mut() { if !bind.enabled { continue; } if bind.net.assigned_port.is_none() && bind.net.assigned_ssl_port.is_none() { continue; } - let mut hostnames = BTreeSet::new(); - let mut gw_filter = AnyFilter( - [PublicFilter { public: false }.into_dyn()] - .into_iter() - .chain( - bind.addresses - .public_enabled - .iter() - .map(|a| a.gateway.id.clone()) - .collect::>() - .into_iter() - .map(IdFilter) - .map(InterfaceFilter::into_dyn), - ) - .collect(), - ); - if let Some(ssl) = &bind.options.add_ssl { - let external = bind - .net - .assigned_ssl_port - .or_not_found("assigned ssl port")?; - let addr = (self.ip, *port).into(); - let connect_ssl = if let Some(alpn) = ssl.alpn.clone() { - Err(alpn) - } else { - if bind.options.secure.as_ref().map_or(false, |s| s.ssl) { - Ok(()) - } else { - Err(AlpnInfo::Reflect) - } - }; - for hostname in ctrl.server_hostnames.iter().cloned() { - vhosts.insert( - (hostname, external), - ProxyTarget { - filter: gw_filter.clone().into_dyn(), - acme: None, - addr, - add_x_forwarded_headers: ssl.add_x_forwarded_headers, - connect_ssl: connect_ssl - .clone() - .map(|_| ctrl.tls_client_config.clone()), - }, // TODO: allow public traffic? - ); - } - for HostAddress { - address, - public, - private, - } in host_addresses.iter().cloned() - { - if hostnames.insert(address.clone()) { - let address = Some(address.clone()); - if ssl.preferred_external_port == 443 { - if let Some(public) = &public { - vhosts.insert( - (address.clone(), 5443), - ProxyTarget { - filter: AndFilter( - bind.net.clone(), - AndFilter( - IdFilter(public.gateway.clone()), - PublicFilter { public: false }, - ), - ) - .into_dyn(), - acme: public.acme.clone(), - addr, - add_x_forwarded_headers: ssl.add_x_forwarded_headers, - connect_ssl: connect_ssl - .clone() - .map(|_| ctrl.tls_client_config.clone()), - }, - ); - vhosts.insert( - (address.clone(), 443), - ProxyTarget { - filter: AndFilter( - bind.net.clone(), - if private { - OrFilter( - IdFilter(public.gateway.clone()), - PublicFilter { public: false }, - ) - .into_dyn() - } else { - AndFilter( - IdFilter(public.gateway.clone()), - PublicFilter { public: true }, - ) - .into_dyn() - }, - ) - .into_dyn(), - acme: public.acme.clone(), - addr, - add_x_forwarded_headers: ssl.add_x_forwarded_headers, - connect_ssl: connect_ssl - .clone() - .map(|_| ctrl.tls_client_config.clone()), - }, - ); - } else { - vhosts.insert( - (address.clone(), 443), - ProxyTarget { - filter: AndFilter( - bind.net.clone(), - PublicFilter { public: false }, - ) - .into_dyn(), - acme: None, - addr, - add_x_forwarded_headers: ssl.add_x_forwarded_headers, - connect_ssl: connect_ssl - .clone() - .map(|_| ctrl.tls_client_config.clone()), - }, - ); - } - } else { - if let Some(public) = public { - vhosts.insert( - (address.clone(), external), - ProxyTarget { - filter: AndFilter( - bind.net.clone(), - if private { - OrFilter( - IdFilter(public.gateway.clone()), - PublicFilter { public: false }, - ) - .into_dyn() - } else { - IdFilter(public.gateway.clone()).into_dyn() - }, - ) - .into_dyn(), - acme: public.acme.clone(), - addr, - add_x_forwarded_headers: ssl.add_x_forwarded_headers, - connect_ssl: connect_ssl - .clone() - .map(|_| ctrl.tls_client_config.clone()), - }, - ); - } else { - vhosts.insert( - (address.clone(), external), - ProxyTarget { - filter: AndFilter( - bind.net.clone(), - PublicFilter { public: false }, - ) - .into_dyn(), - acme: None, - addr, - add_x_forwarded_headers: ssl.add_x_forwarded_headers, - connect_ssl: connect_ssl - .clone() - .map(|_| ctrl.tls_client_config.clone()), - }, - ); - } - } - } - } - } - if bind - .options - .secure - .map_or(true, |s| !(s.ssl && bind.options.add_ssl.is_some())) - { - let external = bind.net.assigned_port.or_not_found("assigned lan port")?; - forwards.insert( - external, - ( - SocketAddrV4::new(self.ip, *port), - AndFilter( - SecureFilter { - secure: bind.options.secure.is_some(), - }, - bind.net.clone(), - ) - .into_dyn(), - ), - ); - } + bind.addresses.possible.clear(); for (gateway_id, info) in net_ifaces .iter() @@ -472,7 +294,7 @@ impl NetServiceData { !matches!(i.device_type, Some(NetworkInterfaceType::Bridge)) }) }) - .filter(|(id, info)| bind.net.filter(id, info)) + .filter(|(_, info)| info.ip_info.is_some()) { let gateway = GatewayInfo { id: gateway_id.clone(), @@ -488,6 +310,7 @@ impl NetServiceData { !(s.ssl && bind.options.add_ssl.is_some()) || info.secure() }) }); + // .local addresses (private only, non-public, non-wireguard gateways) if !info.public() && info.ip_info.as_ref().map_or(false, |i| { i.device_type != Some(NetworkInterfaceType::Wireguard) @@ -506,46 +329,39 @@ impl NetServiceData { }, }); } + // Domain addresses for HostAddress { address, public, private, } in host_addresses.iter().cloned() { - if public.is_none() { - private_dns.insert(address.clone()); - } let private = private && !info.public(); - let public = public.as_ref().map_or(false, |p| &p.gateway == gateway_id); + let public = + public.as_ref().map_or(false, |p| &p.gateway == gateway_id); if public || private { - if bind + let (domain_port, domain_ssl_port) = if bind .options .add_ssl .as_ref() .map_or(false, |ssl| ssl.preferred_external_port == 443) { - bind.addresses.possible.insert(HostnameInfo { - gateway: gateway.clone(), - public, - hostname: IpHostname::Domain { - value: address.clone(), - port: None, - ssl_port: Some(443), - }, - }); + (None, Some(443)) } else { - bind.addresses.possible.insert(HostnameInfo { - gateway: gateway.clone(), - public, - hostname: IpHostname::Domain { - value: address.clone(), - port, - ssl_port: bind.net.assigned_ssl_port, - }, - }); - } + (port, bind.net.assigned_ssl_port) + }; + bind.addresses.possible.insert(HostnameInfo { + gateway: gateway.clone(), + public, + hostname: IpHostname::Domain { + value: address.clone(), + port: domain_port, + ssl_port: domain_ssl_port, + }, + }); } } + // IP addresses if let Some(ip_info) = &info.ip_info { let public = info.public(); if let Some(wan_ip) = ip_info.wan_ip { @@ -592,6 +408,137 @@ impl NetServiceData { } } + // ── Phase 2: Build controller entries from enabled addresses ── + for (port, bind) in host.bindings.iter() { + if !bind.enabled { + continue; + } + if bind.net.assigned_port.is_none() && bind.net.assigned_ssl_port.is_none() { + continue; + } + + let enabled_addresses = bind.addresses.enabled(); + let addr: SocketAddr = (self.ip, *port).into(); + + // SSL vhosts + if let Some(ssl) = &bind.options.add_ssl { + let connect_ssl = if let Some(alpn) = ssl.alpn.clone() { + Err(alpn) + } else if bind.options.secure.as_ref().map_or(false, |s| s.ssl) { + Ok(()) + } else { + Err(AlpnInfo::Reflect) + }; + + if let Some(assigned_ssl_port) = bind.net.assigned_ssl_port { + // Collect private IPs from enabled private addresses' gateways + let server_private_ips: BTreeSet = enabled_addresses + .iter() + .filter(|a| !a.public) + .filter_map(|a| { + net_ifaces + .get(&a.gateway.id) + .and_then(|info| info.ip_info.as_ref()) + }) + .flat_map(|ip_info| ip_info.subnets.iter().map(|s| s.addr())) + .collect(); + + // Server hostname vhosts (on assigned_ssl_port) — private only + if !server_private_ips.is_empty() { + for hostname in ctrl.server_hostnames.iter().cloned() { + vhosts.insert( + (hostname, assigned_ssl_port), + ProxyTarget { + public: BTreeSet::new(), + private: server_private_ips.clone(), + acme: None, + addr, + add_x_forwarded_headers: ssl.add_x_forwarded_headers, + connect_ssl: connect_ssl + .clone() + .map(|_| ctrl.tls_client_config.clone()), + }, + ); + } + } + } + + // Domain vhosts: group by (domain, ssl_port), merge public/private sets + for addr_info in &enabled_addresses { + if let IpHostname::Domain { + value: domain, + ssl_port: Some(domain_ssl_port), + .. + } = &addr_info.hostname + { + let key = (Some(domain.clone()), *domain_ssl_port); + let target = vhosts.entry(key).or_insert_with(|| ProxyTarget { + public: BTreeSet::new(), + private: BTreeSet::new(), + acme: host_addresses + .iter() + .find(|a| &a.address == domain) + .and_then(|a| a.public.as_ref()) + .and_then(|p| p.acme.clone()), + addr, + add_x_forwarded_headers: ssl.add_x_forwarded_headers, + connect_ssl: connect_ssl + .clone() + .map(|_| ctrl.tls_client_config.clone()), + }); + if addr_info.public { + target.public.insert(addr_info.gateway.id.clone()); + } else { + // Add interface IPs for this gateway to private set + if let Some(info) = net_ifaces.get(&addr_info.gateway.id) { + if let Some(ip_info) = &info.ip_info { + for subnet in &ip_info.subnets { + target.private.insert(subnet.addr()); + } + } + } + } + } + } + } + + // Non-SSL forwards + if bind + .options + .secure + .map_or(true, |s| !(s.ssl && bind.options.add_ssl.is_some())) + { + let external = bind.net.assigned_port.or_not_found("assigned lan port")?; + let fwd_public: BTreeSet = enabled_addresses + .iter() + .filter(|a| a.public) + .map(|a| a.gateway.id.clone()) + .collect(); + let fwd_private: BTreeSet = enabled_addresses + .iter() + .filter(|a| !a.public) + .filter_map(|a| { + net_ifaces + .get(&a.gateway.id) + .and_then(|i| i.ip_info.as_ref()) + }) + .flat_map(|ip| ip.subnets.iter().map(|s| s.addr())) + .collect(); + forwards.insert( + external, + ( + SocketAddrV4::new(self.ip, *port), + ForwardRequirements { + public_gateways: fwd_public, + private_ips: fwd_private, + secure: bind.options.secure.is_some(), + }, + ), + ); + } + } + + // ── Phase 3: Reconcile ── let all = binds .forwards .keys() @@ -600,8 +547,8 @@ impl NetServiceData { .collect::>(); for external in all { let mut prev = binds.forwards.remove(&external); - if let Some((internal, filter)) = forwards.remove(&external) { - prev = prev.filter(|(i, f, _)| i == &internal && *f == filter); + if let Some((internal, reqs)) = forwards.remove(&external) { + prev = prev.filter(|(i, r, _)| i == &internal && *r == reqs); binds.forwards.insert( external, if let Some(prev) = prev { @@ -609,11 +556,11 @@ impl NetServiceData { } else { ( internal, - filter.clone(), + reqs.clone(), ctrl.forward .add( external, - filter, + reqs, internal, net_ifaces .iter() diff --git a/core/src/net/vhost.rs b/core/src/net/vhost.rs index 4996ca937..9023576c3 100644 --- a/core/src/net/vhost.rs +++ b/core/src/net/vhost.rs @@ -1,19 +1,19 @@ use std::any::Any; use std::collections::{BTreeMap, BTreeSet}; use std::fmt; -use std::net::{IpAddr, SocketAddr}; +use std::net::{IpAddr, SocketAddr, SocketAddrV6}; use std::sync::{Arc, Weak}; use std::task::{Poll, ready}; -use std::time::Duration; use async_acme::acme::ACME_TLS_ALPN_NAME; use color_eyre::eyre::eyre; use futures::FutureExt; use futures::future::BoxFuture; +use imbl::OrdMap; use imbl_value::{InOMap, InternedString}; use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn}; use serde::{Deserialize, Serialize}; -use tokio::net::TcpStream; +use tokio::net::{TcpListener, TcpStream}; use tokio_rustls::TlsConnector; use tokio_rustls::rustls::crypto::CryptoProvider; use tokio_rustls::rustls::pki_types::ServerName; @@ -23,28 +23,28 @@ use tracing::instrument; use ts_rs::TS; use visit_rs::Visit; -use crate::ResultExt; use crate::context::{CliContext, RpcContext}; use crate::db::model::Database; -use crate::db::model::public::AcmeSettings; +use crate::db::model::public::{AcmeSettings, NetworkInterfaceInfo}; use crate::db::{DbAccessByKey, DbAccessMut}; use crate::net::acme::{ AcmeCertStore, AcmeProvider, AcmeTlsAlpnCache, AcmeTlsHandler, GetAcmeProvider, }; use crate::net::gateway::{ - AnyFilter, BindTcp, DynInterfaceFilter, GatewayInfo, InterfaceFilter, - NetworkInterfaceController, NetworkInterfaceListener, + GatewayInfo, NetworkInterfaceController, NetworkInterfaceListenerAcceptMetadata, }; use crate::net::ssl::{CertStore, RootCaTlsHandler}; use crate::net::tls::{ ChainedHandler, TlsHandlerWrapper, TlsListener, TlsMetadata, WrapTlsHandler, }; +use crate::net::utils::ipv6_is_link_local; use crate::net::web_server::{Accept, AcceptStream, ExtractVisitor, TcpMetadata, extract}; use crate::prelude::*; use crate::util::collections::EqSet; use crate::util::future::{NonDetachingJoinHandle, WeakFuture}; use crate::util::serde::{HandlerExtSerde, MaybeUtf8String, display_serializable}; use crate::util::sync::{SyncMutex, Watch}; +use crate::{GatewayId, ResultExt}; pub fn vhost_api() -> ParentHandler { ParentHandler::new().subcommand( @@ -93,7 +93,7 @@ pub struct VHostController { interfaces: Arc, crypto_provider: Arc, acme_cache: AcmeTlsAlpnCache, - servers: SyncMutex>>, + servers: SyncMutex>>, } impl VHostController { pub fn new( @@ -114,14 +114,22 @@ impl VHostController { &self, hostname: Option, external: u16, - target: DynVHostTarget, + target: DynVHostTarget, ) -> Result, Error> { self.servers.mutate(|writable| { let server = if let Some(server) = writable.remove(&external) { server } else { + let bind_reqs = Watch::new(VHostBindRequirements::default()); + let listener = VHostBindListener { + ip_info: self.interfaces.watcher.subscribe(), + port: external, + bind_reqs: bind_reqs.clone_unseen(), + listeners: BTreeMap::new(), + }; VHostServer::new( - self.interfaces.watcher.bind(BindTcp, external)?, + listener, + bind_reqs, self.db.clone(), self.crypto_provider.clone(), self.acme_cache.clone(), @@ -173,6 +181,143 @@ impl VHostController { } } +/// Union of all ProxyTargets' bind requirements for a VHostServer. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct VHostBindRequirements { + pub public_gateways: BTreeSet, + pub private_ips: BTreeSet, +} + +fn compute_bind_reqs(mapping: &Mapping) -> VHostBindRequirements { + let mut reqs = VHostBindRequirements::default(); + for (_, targets) in mapping { + for (target, rc) in targets { + if rc.strong_count() > 0 { + let (pub_gw, priv_ip) = target.0.bind_requirements(); + reqs.public_gateways.extend(pub_gw); + reqs.private_ips.extend(priv_ip); + } + } + } + reqs +} + +/// Listener that manages its own TCP listeners with IP-level precision. +/// Binds ALL IPs of public gateways and ONLY matching private IPs. +pub struct VHostBindListener { + ip_info: Watch>, + port: u16, + bind_reqs: Watch, + listeners: BTreeMap, +} + +fn update_vhost_listeners( + listeners: &mut BTreeMap, + port: u16, + ip_info: &OrdMap, + reqs: &VHostBindRequirements, +) -> Result<(), Error> { + let mut keep = BTreeSet::::new(); + for (gw_id, info) in ip_info { + if let Some(ip_info) = &info.ip_info { + for ipnet in &ip_info.subnets { + let ip = ipnet.addr(); + let should_bind = + reqs.public_gateways.contains(gw_id) || reqs.private_ips.contains(&ip); + if should_bind { + let addr = match ip { + IpAddr::V6(ip6) => SocketAddrV6::new( + ip6, + port, + 0, + if ipv6_is_link_local(ip6) { + ip_info.scope_id + } else { + 0 + }, + ) + .into(), + ip => SocketAddr::new(ip, port), + }; + keep.insert(addr); + if let Some((_, existing_info)) = listeners.get_mut(&addr) { + *existing_info = GatewayInfo { + id: gw_id.clone(), + info: info.clone(), + }; + } else { + let tcp = TcpListener::from_std( + mio::net::TcpListener::bind(addr) + .with_kind(ErrorKind::Network)? + .into(), + ) + .with_kind(ErrorKind::Network)?; + listeners.insert( + addr, + ( + tcp, + GatewayInfo { + id: gw_id.clone(), + info: info.clone(), + }, + ), + ); + } + } + } + } + } + listeners.retain(|key, _| keep.contains(key)); + Ok(()) +} + +impl Accept for VHostBindListener { + type Metadata = NetworkInterfaceListenerAcceptMetadata; + fn poll_accept( + &mut self, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + // Update listeners when ip_info or bind_reqs change + while self.ip_info.poll_changed(cx).is_ready() + || self.bind_reqs.poll_changed(cx).is_ready() + { + let reqs = self.bind_reqs.read_and_mark_seen(); + let listeners = &mut self.listeners; + let port = self.port; + self.ip_info.peek_and_mark_seen(|ip_info| { + update_vhost_listeners(listeners, port, ip_info, &reqs) + })?; + } + + // Poll each listener for incoming connections + for (&addr, (listener, gw_info)) in &self.listeners { + match listener.poll_accept(cx) { + Poll::Ready(Ok((stream, peer_addr))) => { + if let Err(e) = socket2::SockRef::from(&stream).set_keepalive(true) { + tracing::error!("Failed to set tcp keepalive: {e}"); + tracing::debug!("{e:?}"); + } + return Poll::Ready(Ok(( + NetworkInterfaceListenerAcceptMetadata { + inner: TcpMetadata { + local_addr: addr, + peer_addr, + }, + info: gw_info.clone(), + }, + Box::pin(stream), + ))); + } + Poll::Ready(Err(e)) => { + tracing::trace!("VHostBindListener accept error on {addr}: {e}"); + } + Poll::Pending => {} + } + } + Poll::Pending + } +} + pub trait VHostTarget: std::fmt::Debug + Eq { type PreprocessRes: Send + 'static; #[allow(unused_variables)] @@ -182,6 +327,10 @@ pub trait VHostTarget: std::fmt::Debug + Eq { fn acme(&self) -> Option<&AcmeProvider> { None } + /// Returns (public_gateways, private_ips) this target needs the listener to bind on. + fn bind_requirements(&self) -> (BTreeSet, BTreeSet) { + (BTreeSet::new(), BTreeSet::new()) + } fn preprocess<'a>( &'a self, prev: ServerConfig, @@ -200,6 +349,7 @@ pub trait VHostTarget: std::fmt::Debug + Eq { pub trait DynVHostTargetT: std::fmt::Debug + Any { fn filter(&self, metadata: &::Metadata) -> bool; fn acme(&self) -> Option<&AcmeProvider>; + fn bind_requirements(&self) -> (BTreeSet, BTreeSet); fn preprocess<'a>( &'a self, prev: ServerConfig, @@ -224,6 +374,9 @@ impl + 'static> DynVHostTargetT for T { fn acme(&self) -> Option<&AcmeProvider> { VHostTarget::acme(self) } + fn bind_requirements(&self) -> (BTreeSet, BTreeSet) { + VHostTarget::bind_requirements(self) + } fn preprocess<'a>( &'a self, prev: ServerConfig, @@ -301,7 +454,8 @@ impl Preprocessed { #[derive(Clone)] pub struct ProxyTarget { - pub filter: DynInterfaceFilter, + pub public: BTreeSet, + pub private: BTreeSet, pub acme: Option, pub addr: SocketAddr, pub add_x_forwarded_headers: bool, @@ -309,7 +463,8 @@ pub struct ProxyTarget { } impl PartialEq for ProxyTarget { fn eq(&self, other: &Self) -> bool { - self.filter == other.filter + self.public == other.public + && self.private == other.private && self.acme == other.acme && self.addr == other.addr && self.connect_ssl.as_ref().map(Arc::as_ptr) @@ -320,7 +475,8 @@ impl Eq for ProxyTarget {} impl fmt::Debug for ProxyTarget { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ProxyTarget") - .field("filter", &self.filter) + .field("public", &self.public) + .field("private", &self.private) .field("acme", &self.acme) .field("addr", &self.addr) .field("add_x_forwarded_headers", &self.add_x_forwarded_headers) @@ -340,16 +496,37 @@ where { type PreprocessRes = AcceptStream; fn filter(&self, metadata: &::Metadata) -> bool { - let info = extract::(metadata); - if info.is_none() { - tracing::warn!("No GatewayInfo on metadata"); + let gw = extract::(metadata); + let tcp = extract::(metadata); + let (Some(gw), Some(tcp)) = (gw, tcp) else { + return false; + }; + let Some(ip_info) = &gw.info.ip_info else { + return false; + }; + + let src = tcp.peer_addr.ip(); + // Public if: source is a gateway/router IP (NAT'd internet), + // or source is outside all known subnets (direct internet) + let is_public = ip_info.lan_ip.contains(&src) + || !ip_info.subnets.iter().any(|s| s.contains(&src)); + + if is_public { + self.public.contains(&gw.id) + } else { + // Private: accept if connection arrived on an interface with a matching IP + ip_info + .subnets + .iter() + .any(|s| self.private.contains(&s.addr())) } - info.as_ref() - .map_or(true, |i| self.filter.filter(&i.id, &i.info)) } fn acme(&self) -> Option<&AcmeProvider> { self.acme.as_ref() } + fn bind_requirements(&self) -> (BTreeSet, BTreeSet) { + (self.public.clone(), self.private.clone()) + } async fn preprocess<'a>( &'a self, mut prev: ServerConfig, @@ -634,28 +811,15 @@ where struct VHostServer { mapping: Watch>, + bind_reqs: Watch, _thread: NonDetachingJoinHandle<()>, } -impl<'a> From<&'a BTreeMap, BTreeMap>>> for AnyFilter { - fn from(value: &'a BTreeMap, BTreeMap>>) -> Self { - Self( - value - .iter() - .flat_map(|(_, v)| { - v.iter() - .filter(|(_, r)| r.strong_count() > 0) - .map(|(t, _)| t.filter.clone()) - }) - .collect(), - ) - } -} - impl VHostServer { #[instrument(skip_all)] fn new( listener: A, + bind_reqs: Watch, db: TypedPatchDb, crypto_provider: Arc, acme_cache: AcmeTlsAlpnCache, @@ -679,6 +843,7 @@ impl VHostServer { let mapping = Watch::new(BTreeMap::new()); Self { mapping: mapping.clone(), + bind_reqs, _thread: tokio::spawn(async move { let mut listener = VHostListener(TlsListener::new( listener, @@ -729,6 +894,9 @@ impl VHostServer { targets.insert(target, Arc::downgrade(&rc)); writable.insert(hostname, targets); res = Ok(rc); + if changed { + self.update_bind_reqs(writable); + } changed }); if self.mapping.watcher_count() > 1 { @@ -752,9 +920,23 @@ impl VHostServer { if !targets.is_empty() { writable.insert(hostname, targets); } + if pre != post { + self.update_bind_reqs(writable); + } pre == post }); } + fn update_bind_reqs(&self, mapping: &Mapping) { + let new_reqs = compute_bind_reqs(mapping); + self.bind_reqs.send_if_modified(|reqs| { + if *reqs != new_reqs { + *reqs = new_reqs; + true + } else { + false + } + }); + } fn is_empty(&self) -> bool { self.mapping.peek(|m| m.is_empty()) } diff --git a/core/src/net/web_server.rs b/core/src/net/web_server.rs index 2ac5b035f..8ffe9deaa 100644 --- a/core/src/net/web_server.rs +++ b/core/src/net/web_server.rs @@ -366,28 +366,6 @@ where pub struct WebServerAcceptorSetter { acceptor: Watch, } -impl WebServerAcceptorSetter>> -where - A: Accept, - B: Accept, -{ - pub fn try_upgrade Result>(&self, f: F) -> Result<(), Error> { - let mut res = Ok(()); - self.acceptor.send_modify(|a| { - *a = match a.take() { - Some(Either::Left(a)) => match f(a) { - Ok(b) => Some(Either::Right(b)), - Err(e) => { - res = Err(e); - None - } - }, - x => x, - } - }); - res - } -} impl Deref for WebServerAcceptorSetter { type Target = Watch; fn deref(&self) -> &Self::Target { diff --git a/core/src/tunnel/api.rs b/core/src/tunnel/api.rs index b8f5fd693..47f9a33e3 100644 --- a/core/src/tunnel/api.rs +++ b/core/src/tunnel/api.rs @@ -459,7 +459,7 @@ pub async fn add_forward( }) .map(|s| s.prefix_len()) .unwrap_or(32); - let rc = ctx.forward.add_forward(source, target, prefix).await?; + let rc = ctx.forward.add_forward(source, target, prefix, None).await?; ctx.active_forwards.mutate(|m| { m.insert(source, rc); }); diff --git a/core/src/tunnel/context.rs b/core/src/tunnel/context.rs index 5afac62ab..ac56eaa36 100644 --- a/core/src/tunnel/context.rs +++ b/core/src/tunnel/context.rs @@ -199,7 +199,7 @@ impl TunnelContext { }) .map(|s| s.prefix_len()) .unwrap_or(32); - active_forwards.insert(from, forward.add_forward(from, to, prefix).await?); + active_forwards.insert(from, forward.add_forward(from, to, prefix, None).await?); } Ok(Self(Arc::new(TunnelContextSeed { diff --git a/core/src/version/v0_4_0_alpha_20.rs b/core/src/version/v0_4_0_alpha_20.rs index 3da7caf5c..625d72a51 100644 --- a/core/src/version/v0_4_0_alpha_20.rs +++ b/core/src/version/v0_4_0_alpha_20.rs @@ -89,6 +89,10 @@ impl VersionT for Version { } } + // Migrate availablePorts from IdPool format to BTreeMap + // Rebuild from actual assigned ports in all bindings + migrate_available_ports(db); + Ok(Value::Null) } fn down(self, _db: &mut Value) -> Result<(), Error> { @@ -96,6 +100,62 @@ impl VersionT for Version { } } +fn collect_ports_from_host(host: Option<&Value>, ports: &mut Value) { + let Some(bindings) = host + .and_then(|h| h.get("bindings")) + .and_then(|b| b.as_object()) + else { + return; + }; + for (_, binding) in bindings.iter() { + if let Some(net) = binding.get("net") { + if let Some(port) = net.get("assignedPort").and_then(|p| p.as_u64()) { + if let Some(obj) = ports.as_object_mut() { + obj.insert(port.to_string().into(), Value::from(false)); + } + } + if let Some(port) = net.get("assignedSslPort").and_then(|p| p.as_u64()) { + if let Some(obj) = ports.as_object_mut() { + obj.insert(port.to_string().into(), Value::from(true)); + } + } + } + } +} + +fn migrate_available_ports(db: &mut Value) { + let mut new_ports: Value = serde_json::json!({}).into(); + + // Collect from server host + let server_host = db + .get("public") + .and_then(|p| p.get("serverInfo")) + .and_then(|s| s.get("network")) + .and_then(|n| n.get("host")) + .cloned(); + collect_ports_from_host(server_host.as_ref(), &mut new_ports); + + // Collect from all package hosts + if let Some(packages) = db + .get("public") + .and_then(|p| p.get("packageData")) + .and_then(|p| p.as_object()) + { + for (_, package) in packages.iter() { + if let Some(hosts) = package.get("hosts").and_then(|h| h.as_object()) { + for (_, host) in hosts.iter() { + collect_ports_from_host(Some(host), &mut new_ports); + } + } + } + } + + // Replace private.availablePorts + if let Some(private) = db.get_mut("private").and_then(|p| p.as_object_mut()) { + private.insert("availablePorts".into(), new_ports); + } +} + fn migrate_host(host: Option<&mut Value>) { let Some(host) = host.and_then(|h| h.as_object_mut()) else { return; diff --git a/sdk/package/lib/StartSdk.ts b/sdk/package/lib/StartSdk.ts index 9d1dc0164..3855d594d 100644 --- a/sdk/package/lib/StartSdk.ts +++ b/sdk/package/lib/StartSdk.ts @@ -67,7 +67,7 @@ import { import { getOwnServiceInterfaces } from '../../base/lib/util/getServiceInterfaces' import { Volumes, createVolumes } from './util/Volume' -export const OSVersion = testTypeVersion('0.4.0-alpha.19') +export const OSVersion = testTypeVersion('0.4.0-alpha.20') // prettier-ignore type AnyNeverCond = diff --git a/web/package-lock.json b/web/package-lock.json index bb0aca4f2..8c72087c8 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "startos-ui", - "version": "0.4.0-alpha.19", + "version": "0.4.0-alpha.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "startos-ui", - "version": "0.4.0-alpha.19", + "version": "0.4.0-alpha.20", "license": "MIT", "dependencies": { "@angular/animations": "^20.3.0", diff --git a/web/package.json b/web/package.json index 49fc3a76d..6d4e17883 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "startos-ui", - "version": "0.4.0-alpha.19", + "version": "0.4.0-alpha.20", "author": "Start9 Labs, Inc", "homepage": "https://start9.com/", "license": "MIT", From 8ef4ecf5ac61c77ca645fbf93cd521fd64fa6b09 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 12 Feb 2026 08:27:09 -0700 Subject: [PATCH 11/17] outbound gateway support (#3120) * Multiple (#3111) * fix alerts i18n, fix status display, better, remove usb media, hide shutdown for install complete * trigger chnage detection for localize pipe and round out implementing localize pipe for consistency even though not needed * Fix PackageInfoShort to handle LocaleString on releaseNotes (#3112) * Fix PackageInfoShort to handle LocaleString on releaseNotes * fix: filter by target_version in get_matching_models and pass otherVersions from install * chore: add exver documentation for ai agents * frontend plus some be types --------- Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> --- agents/exver.md | 301 ++++++++++++++++++ core/src/db/model/package.rs | 5 +- core/src/db/model/public.rs | 56 ++-- core/src/install/mod.rs | 3 +- core/src/net/gateway.rs | 6 +- core/src/net/tunnel.rs | 30 +- core/src/registry/package/get.rs | 88 +++-- core/src/service/service_map.rs | 1 + core/src/version/update_details/v0_4_0.md | 14 +- .../src/pages/list/item/item.component.html | 2 +- .../src/pages/list/item/item.module.ts | 4 +- .../src/pages/show/about.component.ts | 6 +- .../src/app/components/remove-media.dialog.ts | 121 +++++++ .../setup-wizard/src/app/pages/drives.page.ts | 43 ++- .../src/app/pages/success.page.ts | 40 ++- .../shared/src/i18n/dictionaries/de.ts | 19 +- .../shared/src/i18n/dictionaries/en.ts | 19 +- .../shared/src/i18n/dictionaries/es.ts | 19 +- .../shared/src/i18n/dictionaries/fr.ts | 21 +- .../shared/src/i18n/dictionaries/pl.ts | 19 +- web/projects/shared/src/i18n/localize.pipe.ts | 3 + .../src/app/services/patch-db/data-model.ts | 2 +- .../components/interface-item.component.ts | 1 - .../services/routes/actions.component.ts | 200 ++++++++---- .../routes/gateways/gateways.component.ts | 31 +- .../system/routes/gateways/item.component.ts | 88 +++-- .../system/routes/gateways/table.component.ts | 13 +- .../ui/src/app/services/api/api.fixures.ts | 3 + .../ui/src/app/services/api/api.types.ts | 16 +- .../app/services/api/embassy-api.service.ts | 8 + .../services/api/embassy-live-api.service.ts | 12 + .../services/api/embassy-mock-api.service.ts | 45 ++- .../ui/src/app/services/api/mock-patch.ts | 24 +- .../ui/src/app/services/controls.service.ts | 10 +- .../ui/src/app/services/gateway.service.ts | 68 ++-- .../services/pkg-status-rendering.service.ts | 2 +- .../app/services/standard-actions.service.ts | 9 +- 37 files changed, 1113 insertions(+), 239 deletions(-) create mode 100644 agents/exver.md create mode 100644 web/projects/setup-wizard/src/app/components/remove-media.dialog.ts diff --git a/agents/exver.md b/agents/exver.md new file mode 100644 index 000000000..b58be3258 --- /dev/null +++ b/agents/exver.md @@ -0,0 +1,301 @@ +# exver — Extended Versioning + +Extended semver supporting **downstream versioning** (wrapper updates independent of upstream) and **flavors** (package fork variants). + +Two implementations exist: +- **Rust crate** (`exver`) — used in `core/`. Source: https://github.com/Start9Labs/exver-rs +- **TypeScript** (`sdk/base/lib/exver/index.ts`) — used in `sdk/` and `web/` + +Both parse the same string format and agree on `satisfies` semantics. + +## Version Format + +An **ExtendedVersion** string looks like: + +``` +[#flavor:]upstream:downstream +``` + +- **upstream** — the original package version (semver-style: `1.2.3`, `1.2.3-beta.1`) +- **downstream** — the StartOS wrapper version (incremented independently) +- **flavor** — optional lowercase ASCII prefix for fork variants + +Examples: +- `1.2.3:0` — upstream 1.2.3, first downstream release +- `1.2.3:2` — upstream 1.2.3, third downstream release +- `#bitcoin:21.0:1` — bitcoin flavor, upstream 21.0, downstream 1 +- `1.0.0-rc.1:0` — upstream with prerelease tag + +## Core Types + +### `Version` + +A semver-style version with arbitrary digit segments and optional prerelease. + +**Rust:** +```rust +use exver::Version; + +let v = Version::new([1, 2, 3], []); // 1.2.3 +let v = Version::new([1, 0], ["beta".into()]); // 1.0-beta +let v: Version = "1.2.3".parse().unwrap(); + +v.number() // &[1, 2, 3] +v.prerelease() // &[] +``` + +**TypeScript:** +```typescript +const v = new Version([1, 2, 3], []) +const v = Version.parse("1.2.3") + +v.number // number[] +v.prerelease // (string | number)[] +v.compare(other) // 'greater' | 'equal' | 'less' +v.compareForSort(other) // -1 | 0 | 1 +``` + +Default: `0` + +### `ExtendedVersion` + +The primary version type. Wraps upstream + downstream `Version` plus an optional flavor. + +**Rust:** +```rust +use exver::ExtendedVersion; + +let ev = ExtendedVersion::new( + Version::new([1, 2, 3], []), + Version::default(), // downstream = 0 +); +let ev: ExtendedVersion = "1.2.3:0".parse().unwrap(); + +ev.flavor() // Option<&str> +ev.upstream() // &Version +ev.downstream() // &Version + +// Builder methods (consuming): +ev.with_flavor("bitcoin") +ev.without_flavor() +ev.map_upstream(|v| ...) +ev.map_downstream(|v| ...) +``` + +**TypeScript:** +```typescript +const ev = new ExtendedVersion(null, upstream, downstream) +const ev = ExtendedVersion.parse("1.2.3:0") +const ev = ExtendedVersion.parseEmver("1.2.3.4") // emver compat + +ev.flavor // string | null +ev.upstream // Version +ev.downstream // Version + +ev.compare(other) // 'greater' | 'equal' | 'less' | null +ev.equals(other) // boolean +ev.greaterThan(other) // boolean +ev.lessThan(other) // boolean +ev.incrementMajor() // new ExtendedVersion +ev.incrementMinor() // new ExtendedVersion +``` + +**Ordering:** Versions with different flavors are **not comparable** (`PartialOrd`/`compare` returns `None`/`null`). + +Default: `0:0` + +### `VersionString` (Rust only, StartOS wrapper) + +Defined in `core/src/util/version.rs`. Caches the original string representation alongside the parsed `ExtendedVersion`. Used as the key type in registry version maps. + +```rust +use crate::util::VersionString; + +let vs: VersionString = "1.2.3:0".parse().unwrap(); +let vs = VersionString::from(extended_version); + +// Deref to ExtendedVersion: +vs.satisfies(&range); +vs.upstream(); + +// String access: +vs.as_str(); // &str +AsRef::::as_ref(&vs); +``` + +`Ord` is implemented with a total ordering — versions with different flavors are ordered by flavor name (unflavored sorts last). + +### `VersionRange` + +A predicate over `ExtendedVersion`. Supports comparison operators, boolean logic, and flavor constraints. + +**Rust:** +```rust +use exver::VersionRange; + +// Constructors: +VersionRange::any() // matches everything +VersionRange::none() // matches nothing +VersionRange::exactly(ev) // = ev +VersionRange::anchor(GTE, ev) // >= ev +VersionRange::caret(ev) // ^ev (compatible changes) +VersionRange::tilde(ev) // ~ev (patch-level changes) + +// Combinators (smart — eagerly simplify): +VersionRange::and(a, b) // a && b +VersionRange::or(a, b) // a || b +VersionRange::not(a) // !a + +// Parsing: +let r: VersionRange = ">=1.0.0:0".parse().unwrap(); +let r: VersionRange = "^1.2.3:0".parse().unwrap(); +let r: VersionRange = ">=1.0.0 <2.0.0".parse().unwrap(); // implicit AND +let r: VersionRange = ">=1.0.0 || >=2.0.0".parse().unwrap(); +let r: VersionRange = "#bitcoin".parse().unwrap(); // flavor match +let r: VersionRange = "*".parse().unwrap(); // any + +// Monoid wrappers for folding: +AnyRange // fold with or, empty = None +AllRange // fold with and, empty = Any +``` + +**TypeScript:** +```typescript +// Constructors: +VersionRange.any() +VersionRange.none() +VersionRange.anchor('=', ev) +VersionRange.anchor('>=', ev) +VersionRange.anchor('^', ev) // ^ and ~ are first-class operators +VersionRange.anchor('~', ev) +VersionRange.flavor(null) // match unflavored versions +VersionRange.flavor("bitcoin") // match #bitcoin versions + +// Combinators — static (smart, variadic): +VersionRange.and(a, b, c, ...) +VersionRange.or(a, b, c, ...) + +// Combinators — instance (not smart, just wrap): +range.and(other) +range.or(other) +range.not() + +// Parsing: +VersionRange.parse(">=1.0.0:0") +VersionRange.parseEmver(">=1.2.3.4") // emver compat + +// Analysis (TS only): +range.normalize() // canonical form (see below) +range.satisfiable() // boolean +range.intersects(other) // boolean +``` + +**Checking satisfaction:** + +```rust +// Rust: +version.satisfies(&range) // bool +``` +```typescript +// TypeScript: +version.satisfies(range) // boolean +range.satisfiedBy(version) // boolean (convenience) +``` + +Also available on `Version` (wraps in `ExtendedVersion` with downstream=0). + +When no operator is specified in a range string, `^` (caret) is the default. + +## Operators + +| Syntax | Rust | TS | Meaning | +|--------|------|----|---------| +| `=` | `EQ` | `'='` | Equal | +| `!=` | `NEQ` | `'!='` | Not equal | +| `>` | `GT` | `'>'` | Greater than | +| `>=` | `GTE` | `'>='` | Greater than or equal | +| `<` | `LT` | `'<'` | Less than | +| `<=` | `LTE` | `'<='` | Less than or equal | +| `^` | expanded to `And(GTE, LT)` | `'^'` | Compatible (first non-zero digit unchanged) | +| `~` | expanded to `And(GTE, LT)` | `'~'` | Patch-level (minor unchanged) | + +## Flavor Rules + +- Versions with **different flavors** never satisfy comparison operators (except `!=`, which returns true) +- `VersionRange::Flavor(Some("bitcoin"))` matches only `#bitcoin:*` versions +- `VersionRange::Flavor(None)` matches only unflavored versions +- Flavor constraints compose with `and`/`or`/`not` like any other range + +## Reduction and Normalization + +### Rust: `reduce()` (shallow) + +`VersionRange::reduce(self) -> Self` re-applies smart constructor rules to one level of the AST. Useful for simplifying a node that was constructed directly (e.g. deserialized) rather than through the smart constructors. + +**Smart constructor rules applied by `and`, `or`, `not`, and `reduce`:** + +`and`: +- `and(Any, b) → b`, `and(a, Any) → a` +- `and(None, _) → None`, `and(_, None) → None` + +`or`: +- `or(Any, _) → Any`, `or(_, Any) → Any` +- `or(None, b) → b`, `or(a, None) → a` + +`not`: +- `not(=v) → !=v`, `not(!=v) → =v` +- `not(and(a, b)) → or(not(a), not(b))` (De Morgan) +- `not(or(a, b)) → and(not(a), not(b))` (De Morgan) +- `not(not(a)) → a` +- `not(Any) → None`, `not(None) → Any` + +### TypeScript: `normalize()` (deep, canonical) + +`VersionRange.normalize(): VersionRange` in `sdk/base/lib/exver/index.ts` performs full normalization by converting the range AST into a canonical form. This is a deep operation that produces a semantically equivalent but simplified range. + +**How it works:** + +1. **`tables()`** — Converts the VersionRange AST into truth tables (`VersionRangeTable`). Each table is a number line split at version boundary points, with boolean values for each segment indicating whether versions in that segment satisfy the range. Separate tables are maintained per flavor (and for flavor negations). + +2. **`VersionRangeTable.zip(a, b, func)`** — Merges two tables by walking their boundary points in sorted order and applying a boolean function (`&&` or `||`) to combine segment values. Adjacent segments with the same boolean value are collapsed automatically. + +3. **`VersionRangeTable.and/or/not`** — Table-level boolean operations. `and` computes the cross-product of flavor tables (since `#a && #b` for different flavors is unsatisfiable). `not` inverts all segment values. + +4. **`VersionRangeTable.collapse()`** — Checks if a table is uniformly true or false across all flavors and segments. Returns `true`, `false`, or `null` (mixed). + +5. **`VersionRangeTable.minterms()`** — Converts truth tables back into a VersionRange AST in [sum-of-products](https://en.wikipedia.org/wiki/Canonical_normal_form#Minterms) canonical form. Each `true` segment becomes a product term (conjunction of boundary constraints), and all terms are joined with `or`. Adjacent boundary points collapse into `=` anchors. + +**Example:** `normalize` can simplify: +- `>=1.0.0:0 && <=1.0.0:0` → `=1.0.0:0` +- `>=2.0.0:0 || >=1.0.0:0` → `>=1.0.0:0` +- `!(!>=1.0.0:0)` → `>=1.0.0:0` + +**Also exposes:** +- `satisfiable(): boolean` — returns `true` if there exists any version satisfying the range (checks if `collapse(tables())` is not `false`) +- `intersects(other): boolean` — returns `true` if `and(this, other)` is satisfiable + +## API Differences Between Rust and TypeScript + +| | Rust | TypeScript | +|-|------|------------| +| **`^` / `~`** | Expanded at construction to `And(GTE, LT)` | First-class operator on `Anchor` | +| **`not()`** | Static, eagerly simplifies (De Morgan, double negation) | Instance method, just wraps | +| **`and()`/`or()`** | Binary static | Both binary instance and variadic static | +| **Normalization** | `reduce()` — shallow, one AST level | `normalize()` — deep canonical form via truth tables | +| **Satisfiability** | Not available | `satisfiable()` and `intersects(other)` | +| **ExtendedVersion helpers** | `with_flavor()`, `without_flavor()`, `map_upstream()`, `map_downstream()` | `incrementMajor()`, `incrementMinor()`, `greaterThan()`, `lessThan()`, `equals()`, etc. | +| **Monoid wrappers** | `AnyRange` (fold with `or`) and `AllRange` (fold with `and`) | Not present — use variadic static methods | +| **`VersionString`** | Wrapper caching parsed + string form | Not present | +| **Emver compat** | `From` for `ExtendedVersion` | `ExtendedVersion.parseEmver()`, `VersionRange.parseEmver()` | + +## Serde + +All types serialize/deserialize as strings (requires `serde` feature, enabled in StartOS): + +```json +{ + "version": "1.2.3:0", + "targetVersion": ">=1.0.0:0 <2.0.0:0", + "sourceVersion": "^0.3.0:0" +} +``` diff --git a/core/src/db/model/package.rs b/core/src/db/model/package.rs index 70c33a360..63db0b8ac 100644 --- a/core/src/db/model/package.rs +++ b/core/src/db/model/package.rs @@ -18,7 +18,7 @@ use crate::s9pk::manifest::{LocaleString, Manifest}; use crate::status::StatusInfo; use crate::util::DataUrl; use crate::util::serde::{Pem, is_partial_of}; -use crate::{ActionId, HealthCheckId, HostId, PackageId, ReplayId, ServiceInterfaceId}; +use crate::{ActionId, GatewayId, HealthCheckId, HostId, PackageId, ReplayId, ServiceInterfaceId}; #[derive(Debug, Default, Deserialize, Serialize, TS)] #[ts(export)] @@ -381,6 +381,9 @@ pub struct PackageDataEntry { pub hosts: Hosts, #[ts(type = "string[]")] pub store_exposed_dependents: Vec, + #[serde(default)] + #[ts(type = "string | null")] + pub outbound_gateway: Option, } impl AsRef for PackageDataEntry { fn as_ref(&self) -> &PackageDataEntry { diff --git a/core/src/db/model/public.rs b/core/src/db/model/public.rs index cd2dc2169..0cdf868c9 100644 --- a/core/src/db/model/public.rs +++ b/core/src/db/model/public.rs @@ -116,6 +116,7 @@ impl Public { acme }, dns: Default::default(), + default_outbound: None, }, status_info: ServerStatus { backup_progress: None, @@ -219,6 +220,9 @@ pub struct NetworkInfo { pub acme: BTreeMap, #[serde(default)] pub dns: DnsSettings, + #[serde(default)] + #[ts(type = "string | null")] + pub default_outbound: Option, } #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] @@ -238,39 +242,42 @@ pub struct DnsSettings { #[ts(export)] pub struct NetworkInterfaceInfo { pub name: Option, + #[ts(skip)] pub public: Option, pub secure: Option, pub ip_info: Option>, + #[serde(default, rename = "type")] + pub gateway_type: Option, } impl NetworkInterfaceInfo { pub fn public(&self) -> bool { self.public.unwrap_or_else(|| { !self.ip_info.as_ref().map_or(true, |ip_info| { - let ip4s = ip_info - .subnets - .iter() - .filter_map(|ipnet| { - if let IpAddr::V4(ip4) = ipnet.addr() { - Some(ip4) - } else { - None - } - }) - .collect::>(); - if !ip4s.is_empty() { - return ip4s - .iter() - .all(|ip4| ip4.is_loopback() || ip4.is_private() || ip4.is_link_local()); - } - ip_info.subnets.iter().all(|ipnet| { - if let IpAddr::V6(ip6) = ipnet.addr() { - ipv6_is_local(ip6) + let ip4s = ip_info + .subnets + .iter() + .filter_map(|ipnet| { + if let IpAddr::V4(ip4) = ipnet.addr() { + Some(ip4) } else { - true + None } }) + .collect::>(); + if !ip4s.is_empty() { + return ip4s + .iter() + .all(|ip4| ip4.is_loopback() || ip4.is_private() || ip4.is_link_local()); + } + ip_info.subnets.iter().all(|ipnet| { + if let IpAddr::V6(ip6) = ipnet.addr() { + ipv6_is_local(ip6) + } else { + true + } }) }) + }) } pub fn secure(&self) -> bool { @@ -309,6 +316,15 @@ pub enum NetworkInterfaceType { Loopback, } +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS, clap::ValueEnum)] +#[ts(export)] +#[serde(rename_all = "kebab-case")] +pub enum GatewayType { + #[default] + InboundOutbound, + OutboundOnly, +} + #[derive(Debug, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] #[model = "Model"] diff --git a/core/src/install/mod.rs b/core/src/install/mod.rs index 5fe707463..9267be2d5 100644 --- a/core/src/install/mod.rs +++ b/core/src/install/mod.rs @@ -137,6 +137,7 @@ pub async fn install( json!({ "id": id, "targetVersion": VersionRange::exactly(version.deref().clone()), + "otherVersions": "none", }), RegistryUrlParams { registry: registry.clone(), @@ -484,7 +485,7 @@ pub async fn cli_install( let mut packages: GetPackageResponse = from_value( ctx.call_remote::( "package.get", - json!({ "id": &id, "targetVersion": version, "sourceVersion": source_version }), + json!({ "id": &id, "targetVersion": version, "sourceVersion": source_version, "otherVersions": "none" }), ) .await?, )?; diff --git a/core/src/net/gateway.rs b/core/src/net/gateway.rs index e9c2575ba..d8d0a6c6e 100644 --- a/core/src/net/gateway.rs +++ b/core/src/net/gateway.rs @@ -754,13 +754,14 @@ async fn watch_ip( write_to.send_if_modified( |m: &mut OrdMap| { - let (name, public, secure, prev_wan_ip) = m + let (name, public, secure, gateway_type, prev_wan_ip) = m .get(&iface) - .map_or((None, None, None, None), |i| { + .map_or((None, None, None, None, None), |i| { ( i.name.clone(), i.public, i.secure, + i.gateway_type, i.ip_info .as_ref() .and_then(|i| i.wan_ip), @@ -775,6 +776,7 @@ async fn watch_ip( public, secure, ip_info: Some(ip_info.clone()), + gateway_type, }, ) .filter(|old| &old.ip_info == &Some(ip_info)) diff --git a/core/src/net/tunnel.rs b/core/src/net/tunnel.rs index 8635475e1..deb7ec6a3 100644 --- a/core/src/net/tunnel.rs +++ b/core/src/net/tunnel.rs @@ -8,7 +8,7 @@ use ts_rs::TS; use crate::GatewayId; use crate::context::{CliContext, RpcContext}; -use crate::db::model::public::{NetworkInterfaceInfo, NetworkInterfaceType}; +use crate::db::model::public::{GatewayType, NetworkInterfaceInfo, NetworkInterfaceType}; use crate::net::host::all_hosts; use crate::prelude::*; use crate::util::Invoke; @@ -32,14 +32,19 @@ pub fn tunnel_api() -> ParentHandler { } #[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] #[ts(export)] pub struct AddTunnelParams { #[arg(help = "help.arg.tunnel-name")] name: InternedString, #[arg(help = "help.arg.wireguard-config")] config: String, - #[arg(help = "help.arg.is-public")] - public: bool, + #[arg(help = "help.arg.gateway-type")] + #[serde(default, rename = "type")] + gateway_type: Option, + #[arg(help = "help.arg.set-as-default-outbound")] + #[serde(default)] + set_as_default_outbound: bool, } fn sanitize_config(config: &str) -> String { @@ -64,7 +69,8 @@ pub async fn add_tunnel( AddTunnelParams { name, config, - public, + gateway_type, + set_as_default_outbound, }: AddTunnelParams, ) -> Result { let ifaces = ctx.net_controller.net_iface.watcher.subscribe(); @@ -76,9 +82,10 @@ pub async fn add_tunnel( iface.clone(), NetworkInterfaceInfo { name: Some(name), - public: Some(public), + public: None, secure: None, ip_info: None, + gateway_type, }, ); return true; @@ -120,6 +127,19 @@ pub async fn add_tunnel( sub.recv().await; + if set_as_default_outbound { + ctx.db + .mutate(|db| { + db.as_public_mut() + .as_server_info_mut() + .as_network_mut() + .as_default_outbound_mut() + .ser(&Some(iface.clone())) + }) + .await + .result?; + } + Ok(iface) } diff --git a/core/src/registry/package/get.rs b/core/src/registry/package/get.rs index 7525ce54d..2d834bf87 100644 --- a/core/src/registry/package/get.rs +++ b/core/src/registry/package/get.rs @@ -15,6 +15,7 @@ use crate::progress::{FullProgressTracker, ProgressUnits}; use crate::registry::context::RegistryContext; use crate::registry::device_info::DeviceInfo; use crate::registry::package::index::{PackageIndex, PackageVersionInfo}; +use crate::s9pk::manifest::LocaleString; use crate::s9pk::merkle_archive::source::ArchiveSource; use crate::s9pk::v2::SIG_CONTEXT; use crate::util::VersionString; @@ -38,11 +39,11 @@ impl Default for PackageDetailLevel { } } -#[derive(Debug, Deserialize, Serialize, TS)] +#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export)] pub struct PackageInfoShort { - pub release_notes: String, + pub release_notes: LocaleString, } #[derive(Debug, Deserialize, Serialize, TS, Parser, HasModel)] @@ -89,17 +90,20 @@ impl GetPackageResponse { let lesser_versions: BTreeMap<_, _> = self .other_versions - .as_ref() + .clone() .into_iter() .flatten() - .filter(|(v, _)| ***v < *version) + .filter(|(v, _)| **v < *version) .collect(); if !lesser_versions.is_empty() { table.add_row(row![bc => "OLDER VERSIONS"]); table.add_row(row![bc => "VERSION", "RELEASE NOTES"]); for (version, info) in lesser_versions { - table.add_row(row![AsRef::::as_ref(version), &info.release_notes]); + table.add_row(row![ + AsRef::::as_ref(&version), + &info.release_notes.localized() + ]); } } @@ -147,6 +151,7 @@ fn get_matching_models( id, source_version, device_info, + target_version, .. }: &GetPackageParams, ) -> Result)>, Error> { @@ -165,26 +170,29 @@ fn get_matching_models( .as_entries()? .into_iter() .map(|(v, info)| { + let ev = ExtendedVersion::from(v); Ok::<_, Error>( - if source_version.as_ref().map_or(Ok(true), |source_version| { - Ok::<_, Error>( - source_version.satisfies( - &info - .as_source_version() - .de()? - .unwrap_or(VersionRange::any()), - ), - ) - })? { + if target_version.as_ref().map_or(true, |tv| ev.satisfies(tv)) + && source_version.as_ref().map_or(Ok(true), |source_version| { + Ok::<_, Error>( + source_version.satisfies( + &info + .as_source_version() + .de()? + .unwrap_or(VersionRange::any()), + ), + ) + })? + { let mut info = info.clone(); if let Some(device_info) = &device_info { if info.for_device(device_info)? { - Some((k.clone(), ExtendedVersion::from(v), info)) + Some((k.clone(), ev, info)) } else { None } } else { - Some((k.clone(), ExtendedVersion::from(v), info)) + Some((k.clone(), ev, info)) } } else { None @@ -207,12 +215,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu for (id, version, info) in get_matching_models(&peek.as_index().as_package(), ¶ms)? { let package_best = best.entry(id.clone()).or_default(); let package_other = other.entry(id.clone()).or_default(); - if params - .target_version - .as_ref() - .map_or(true, |v| version.satisfies(v)) - && package_best.keys().all(|k| !(**k > version)) - { + if package_best.keys().all(|k| !(**k > version)) { for worse_version in package_best .keys() .filter(|k| ***k < version) @@ -569,3 +572,42 @@ pub async fn cli_download( Ok(()) } + +#[test] +fn check_matching_info_short() { + use crate::registry::package::index::PackageMetadata; + use crate::s9pk::manifest::{Alerts, Description}; + use crate::util::DataUrl; + + let lang_map = |s: &str| { + LocaleString::LanguageMap([("en".into(), s.into())].into_iter().collect()) + }; + + let info = PackageVersionInfo { + metadata: PackageMetadata { + title: "Test Package".into(), + icon: DataUrl::from_vec("image/png", vec![]), + description: Description { + short: lang_map("A short description"), + long: lang_map("A longer description of the test package"), + }, + release_notes: lang_map("Initial release"), + git_hash: None, + license: "MIT".into(), + wrapper_repo: "https://github.com/example/wrapper".parse().unwrap(), + upstream_repo: "https://github.com/example/upstream".parse().unwrap(), + support_site: "https://example.com/support".parse().unwrap(), + marketing_site: "https://example.com".parse().unwrap(), + donation_url: None, + docs_url: None, + alerts: Alerts::default(), + dependency_metadata: BTreeMap::new(), + os_version: exver::Version::new([0, 3, 6], []), + sdk_version: None, + hardware_acceleration: false, + }, + source_version: None, + s9pks: Vec::new(), + }; + from_value::(to_value(&info).unwrap()).unwrap(); +} diff --git a/core/src/service/service_map.rs b/core/src/service/service_map.rs index fe8192d26..56292baba 100644 --- a/core/src/service/service_map.rs +++ b/core/src/service/service_map.rs @@ -259,6 +259,7 @@ impl ServiceMap { service_interfaces: Default::default(), hosts: Default::default(), store_exposed_dependents: Default::default(), + outbound_gateway: None, }, )?; }; diff --git a/core/src/version/update_details/v0_4_0.md b/core/src/version/update_details/v0_4_0.md index e546704ee..e3881a5fd 100644 --- a/core/src/version/update_details/v0_4_0.md +++ b/core/src/version/update_details/v0_4_0.md @@ -10,13 +10,13 @@ A server is not a toy. It is a critical component of the computing paradigm, and Start9 is paving new ground with StartOS, trying to create what most developers and IT professionals thought impossible; namely, an OS and user experience that affords a normal person the same independent control over their data and communications as an experienced Linux sysadmin. -The difficulty of our endeavor requires making mistakes; and our integrity and dedication to excellence require that we correct them. This means a willingness to discard bad ideas and broken parts, and if absolutely necessary, to tear it all down and start over. That is exactly what we did with StartOS v0.2.0 in 2020. It is what we did with StartOS v0.3.0 in 2022. And we are doing it now with StartOS v0.4.0 in 2025. +The difficulty of our endeavor requires making mistakes; and our integrity and dedication to excellence require that we correct them. This means a willingness to discard bad ideas and broken parts, and if absolutely necessary, to tear it all down and start over. That is exactly what we did with StartOS v0.2.0 in 2020. It is what we did with StartOS v0.3.0 in 2022. And we are doing it now with StartOS v0.4.0 in 2026. v0.4.0 is a complete rewrite of StartOS, almost nothing survived. After nearly six years of building StartOS, we believe that we have finally arrived at the correct architecture and foundation that will allow us to deliver on the promise of sovereign computing. ## Changelog -### Improved User interface +### New User interface We re-wrote the StartOS UI to be more performant, more intuitive, and better looking on both mobile and desktop. Enjoy. @@ -28,6 +28,10 @@ StartOS v0.4.0 supports multiple languages and also makes it easy to add more la Neither Docker nor Podman offer the reliability and flexibility needed for StartOS. Instead, v0.4.0 uses a nested container paradigm based on LXC for the outer container and Linux namespaces for sub containers. This architecture naturally supports multi container setups. +### Hardware Acceleration + +Services can take advantage of (and require) the presence of certain hardware modules, such as Nvidia GPUs, for transcoding or inference purposes. For example, StartOS and Ollama can run natively on The Nvidia DGX Spark and take full advantage of the hardware/firmware stack to perform local inference against open source models. + ### New S9PK archive format The S9PK archive format has been overhauled to allow for signature verification of partial downloads, and allow direct mounting of container images without unpacking the s9pk. @@ -80,13 +84,13 @@ The new start-fs fuse module unifies file system expectations for various platfo StartOS now uses Extended Versioning (Exver), which consists of three parts: (1) a Semver-compliant upstream version, (2) a Semver-compliant wrapper version, and (3) an optional "flavor" prefix. Flavors can be thought of as alternative implementations of services, where a user would only want one or the other installed, and data can feasibly be migrating between the two. Another common characteristic of flavors is that they satisfy the same API requirement of dependents, though this is not strictly necessary. A valid Exver looks something like this: `#knots:29.0:1.0-beta.1`. This would translate to "the first beta release of StartOS wrapper version 1.0 of Bitcoin Knots version 29.0". -### ACME +### Let's Encrypt -StartOS now supports using ACME protocol to automatically obtain SSL/TLS certificates from widely trusted certificate authorities, such as Let's Encrypt, for your public domains. This means people visiting your public websites and APIs will not need to download and trust your server's Root CA. +StartOS now supports Let's Encrypt to automatically obtain SSL/TLS certificates for public domains. This means people visiting your public websites and APIs will not need to download and trust your server's Root CA. ### Gateways -Gateways connect your server to the Internet. They process outbound traffic, and under certain conditions, they also permit inbound traffic. For example, your router is a gateway. It is now possible add gateways to StartOS, such as StartTunnel, in order to more granularly control how your installed services are exposed to the Internet. +Gateways connect your server to the Internet, facilitating inbound and outbound traffic. Your router is a gateway. It is now possible to add Wireguard VPN gateways to your server to control how devices outside the LAN connect to your server and how your server connects out to the Internet. ### Static DNS Servers diff --git a/web/projects/marketplace/src/pages/list/item/item.component.html b/web/projects/marketplace/src/pages/list/item/item.component.html index 4b3d5fb26..33b9d45e3 100644 --- a/web/projects/marketplace/src/pages/list/item/item.component.html +++ b/web/projects/marketplace/src/pages/list/item/item.component.html @@ -12,7 +12,7 @@ {{ pkg.title }} - {{ pkg.description.short }} + {{ pkg.description.short | localize }}
diff --git a/web/projects/marketplace/src/pages/list/item/item.module.ts b/web/projects/marketplace/src/pages/list/item/item.module.ts index e6989b562..682f24de5 100644 --- a/web/projects/marketplace/src/pages/list/item/item.module.ts +++ b/web/projects/marketplace/src/pages/list/item/item.module.ts @@ -1,12 +1,12 @@ import { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' import { RouterModule } from '@angular/router' -import { SharedPipesModule, TickerComponent } from '@start9labs/shared' +import { LocalizePipe, SharedPipesModule, TickerComponent } from '@start9labs/shared' import { ItemComponent } from './item.component' @NgModule({ declarations: [ItemComponent], exports: [ItemComponent], - imports: [CommonModule, RouterModule, SharedPipesModule, TickerComponent], + imports: [CommonModule, RouterModule, SharedPipesModule, TickerComponent, LocalizePipe], }) export class ItemModule {} diff --git a/web/projects/marketplace/src/pages/show/about.component.ts b/web/projects/marketplace/src/pages/show/about.component.ts index edbe5c5a5..a4f7a9135 100644 --- a/web/projects/marketplace/src/pages/show/about.component.ts +++ b/web/projects/marketplace/src/pages/show/about.component.ts @@ -6,7 +6,7 @@ import { output, } from '@angular/core' import { MarketplacePkgBase } from '../../types' -import { CopyService, i18nPipe } from '@start9labs/shared' +import { CopyService, i18nPipe, LocalizePipe } from '@start9labs/shared' import { DatePipe } from '@angular/common' import { MarketplaceItemComponent } from './item.component' @@ -71,7 +71,7 @@ import { MarketplaceItemComponent } from './item.component'

{{ 'Description' | i18n }}

-

+

`, @@ -129,7 +129,7 @@ import { MarketplaceItemComponent } from './item.component' } `, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [MarketplaceItemComponent, DatePipe, i18nPipe], + imports: [MarketplaceItemComponent, DatePipe, i18nPipe, LocalizePipe], }) export class MarketplaceAboutComponent { readonly copyService = inject(CopyService) diff --git a/web/projects/setup-wizard/src/app/components/remove-media.dialog.ts b/web/projects/setup-wizard/src/app/components/remove-media.dialog.ts new file mode 100644 index 000000000..0daef9e94 --- /dev/null +++ b/web/projects/setup-wizard/src/app/components/remove-media.dialog.ts @@ -0,0 +1,121 @@ +import { Component } from '@angular/core' +import { i18nPipe } from '@start9labs/shared' +import { TuiButton, TuiDialogContext } from '@taiga-ui/core' +import { injectContext } from '@taiga-ui/polymorpheus' + +@Component({ + standalone: true, + imports: [TuiButton, i18nPipe], + template: ` +
+
+
+
+
+
+
+
+
+

+ {{ + 'Remove USB stick or other installation media from your server' | i18n + }} +

+
+ +
+ `, + styles: ` + :host { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + } + + .animation-container { + position: relative; + width: 160px; + height: 69px; + } + + .port { + position: absolute; + left: 20px; + top: 50%; + transform: translateY(-50%); + width: 28px; + height: 18px; + background: var(--tui-background-neutral-1); + border: 2px solid var(--tui-border-normal); + border-radius: 2px; + } + + .port-inner { + position: absolute; + top: 3px; + left: 3px; + right: 3px; + bottom: 3px; + background: var(--tui-background-neutral-2); + border-radius: 1px; + } + + .usb-stick { + position: absolute; + top: 50%; + transform: translateY(-50%); + display: flex; + align-items: center; + animation: slide-out 2s ease-in-out 0.5s infinite; + left: 32px; + } + + .usb-connector { + width: 20px; + height: 12px; + background: var(--tui-text-secondary); + border-radius: 1px; + } + + .usb-body { + width: 40px; + height: 20px; + background: var(--tui-status-info); + border-radius: 2px 4px 4px 2px; + } + + @keyframes slide-out { + 0% { + left: 32px; + opacity: 0; + } + 5% { + left: 32px; + opacity: 1; + } + 80% { + left: 130px; + opacity: 0; + } + 100% { + left: 130px; + opacity: 0; + } + } + + p { + margin: 0 0 2rem; + } + + footer { + display: flex; + justify-content: center; + } + `, +}) +export class RemoveMediaDialog { + protected readonly context = injectContext>() +} diff --git a/web/projects/setup-wizard/src/app/pages/drives.page.ts b/web/projects/setup-wizard/src/app/pages/drives.page.ts index f3c7ded42..2790fc306 100644 --- a/web/projects/setup-wizard/src/app/pages/drives.page.ts +++ b/web/projects/setup-wizard/src/app/pages/drives.page.ts @@ -1,4 +1,9 @@ -import { ChangeDetectorRef, Component, inject } from '@angular/core' +import { + ChangeDetectorRef, + Component, + HostListener, + inject, +} from '@angular/core' import { Router } from '@angular/router' import { FormsModule } from '@angular/forms' import { @@ -21,13 +26,14 @@ import { import { TuiDataListWrapper, TuiSelect, TuiTooltip } from '@taiga-ui/kit' import { TuiCardLarge, TuiHeader } from '@taiga-ui/layout' import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' -import { filter } from 'rxjs' +import { filter, Subscription } from 'rxjs' import { ApiService } from '../services/api.service' import { StateService } from '../services/state.service' import { PreserveOverwriteDialog } from '../components/preserve-overwrite.dialog' @Component({ template: ` + @if (!shuttingDown) {

{{ 'Select Drives' | i18n }}

@@ -132,6 +138,7 @@ import { PreserveOverwriteDialog } from '../components/preserve-overwrite.dialog }
+ } `, styles: ` .no-drives { @@ -176,6 +183,14 @@ export default class DrivesPage { protected readonly mobile = inject(TUI_IS_MOBILE) + @HostListener('document:keydown', ['$event']) + onKeydown(event: KeyboardEvent) { + if (event.ctrlKey && event.shiftKey && event.key === 'X') { + event.preventDefault() + this.shutdownServer() + } + } + readonly osDriveTooltip = this.i18n.transform( 'The drive where the StartOS operating system will be installed.', ) @@ -185,6 +200,8 @@ export default class DrivesPage { drives: DiskInfo[] = [] loading = true + shuttingDown = false + private dialogSub?: Subscription selectedOsDrive: DiskInfo | null = null selectedDataDrive: DiskInfo | null = null preserveData: boolean | null = null @@ -339,22 +356,18 @@ export default class DrivesPage { loader.unsubscribe() // Show success dialog - this.dialogs - .openConfirm({ + this.dialogSub = this.dialogs + .openAlert('StartOS has been installed successfully.', { label: 'Installation Complete!', size: 's', - data: { - content: 'StartOS has been installed successfully.', - yes: 'Continue to Setup', - no: 'Shutdown', - }, + dismissible: false, + closeable: true, + data: { button: this.i18n.transform('Continue to Setup') }, }) - .subscribe(continueSetup => { - if (continueSetup) { + .subscribe({ + complete: () => { this.navigateToNextStep(result.attach) - } else { - this.shutdownServer() - } + }, }) } catch (e: any) { loader.unsubscribe() @@ -372,10 +385,12 @@ export default class DrivesPage { } private async shutdownServer() { + this.dialogSub?.unsubscribe() const loader = this.loader.open('Beginning shutdown').subscribe() try { await this.api.shutdown() + this.shuttingDown = true } catch (e: any) { this.errorService.handleError(e) } finally { diff --git a/web/projects/setup-wizard/src/app/pages/success.page.ts b/web/projects/setup-wizard/src/app/pages/success.page.ts index 8f84f788b..03dae0203 100644 --- a/web/projects/setup-wizard/src/app/pages/success.page.ts +++ b/web/projects/setup-wizard/src/app/pages/success.page.ts @@ -6,7 +6,12 @@ import { ViewChild, DOCUMENT, } from '@angular/core' -import { DownloadHTMLService, ErrorService, i18nPipe } from '@start9labs/shared' +import { + DialogService, + DownloadHTMLService, + ErrorService, + i18nPipe, +} from '@start9labs/shared' import { TuiIcon, TuiLoader, TuiTitle } from '@taiga-ui/core' import { TuiAvatar } from '@taiga-ui/kit' import { TuiCardLarge, TuiCell, TuiHeader } from '@taiga-ui/layout' @@ -14,7 +19,9 @@ import { ApiService } from '../services/api.service' import { StateService } from '../services/state.service' import { DocumentationComponent } from '../components/documentation.component' import { MatrixComponent } from '../components/matrix.component' +import { RemoveMediaDialog } from '../components/remove-media.dialog' import { SetupCompleteRes } from '../types' +import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' @Component({ template: ` @@ -29,12 +36,8 @@ import { SetupCompleteRes } from '../types' @if (!stateService.kiosk) { {{ - stateService.setupType === 'restore' - ? ('You can unplug your backup drive' | i18n) - : stateService.setupType === 'transfer' - ? ('You can unplug your transfer drive' | i18n) - : ('http://start.local was for setup only. It will no longer work.' - | i18n) + 'http://start.local was for setup only. It will no longer work.' + | i18n }} } @@ -69,14 +72,15 @@ import { SetupCompleteRes } from '../types' tuiCell="l" [class.disabled]="!stateService.kiosk && !downloaded" [disabled]="(!stateService.kiosk && !downloaded) || usbRemoved" - (click)="usbRemoved = true" + (click)="removeMedia()" >
- {{ 'USB Removed' | i18n }} + {{ 'Remove Installation Media' | i18n }}
{{ - 'Remove the USB installation media from your server' | i18n + 'Remove USB stick or other installation media from your server' + | i18n }}
@@ -184,6 +188,7 @@ export default class SuccessPage implements AfterViewInit { private readonly errorService = inject(ErrorService) private readonly api = inject(ApiService) private readonly downloadHtml = inject(DownloadHTMLService) + private readonly dialogs = inject(DialogService) private readonly i18n = inject(i18nPipe) readonly stateService = inject(StateService) @@ -225,6 +230,21 @@ export default class SuccessPage implements AfterViewInit { }) } + removeMedia() { + this.dialogs + .openComponent( + new PolymorpheusComponent(RemoveMediaDialog), + { + size: 's', + dismissible: false, + closeable: false, + }, + ) + .subscribe(() => { + this.usbRemoved = true + }) + } + exitKiosk() { this.api.exit() } diff --git a/web/projects/shared/src/i18n/dictionaries/de.ts b/web/projects/shared/src/i18n/dictionaries/de.ts index e229d9f24..f5113fc61 100644 --- a/web/projects/shared/src/i18n/dictionaries/de.ts +++ b/web/projects/shared/src/i18n/dictionaries/de.ts @@ -100,6 +100,7 @@ export default { 102: 'Verlassen', 103: 'Sind Sie sicher?', 104: 'Neues Netzwerk-Gateway', + 107: 'Onion-Domains', 108: 'Öffentlich', 109: 'privat', 111: 'Keine Onion-Domains', @@ -639,13 +640,11 @@ export default { 667: 'Einrichtung wird gestartet', 670: 'Warten Sie 1–2 Minuten und aktualisieren Sie die Seite', 672: 'Einrichtung abgeschlossen!', - 673: 'Sie können Ihr Backup-Laufwerk entfernen', - 674: 'Sie können Ihr Übertragungs-Laufwerk entfernen', 675: 'http://start.local war nur für die Einrichtung gedacht. Es funktioniert nicht mehr.', 676: 'Adressinformationen herunterladen', 677: 'Enthält die permanente lokale Adresse Ihres Servers und die Root-CA', - 678: 'USB entfernt', - 679: 'Entfernen Sie das USB-Installationsmedium aus Ihrem Server', + 678: 'Installationsmedium entfernen', + 679: 'Entfernen Sie den USB-Stick oder andere Installationsmedien von Ihrem Server', 680: 'Server neu starten', 681: 'Warten, bis der Server wieder online ist', 682: 'Server ist wieder online', @@ -680,4 +679,16 @@ export default { 714: 'Installation abgeschlossen!', 715: 'StartOS wurde erfolgreich installiert.', 716: 'Weiter zur Einrichtung', + 717: '', + 718: '', + 719: '', + 720: '', + 721: '', + 722: '', + 723: '', + 724: '', + 725: '', + 726: '', + 727: '', + 728: '', } satisfies i18n diff --git a/web/projects/shared/src/i18n/dictionaries/en.ts b/web/projects/shared/src/i18n/dictionaries/en.ts index b7e05c7b6..3c2cd04d1 100644 --- a/web/projects/shared/src/i18n/dictionaries/en.ts +++ b/web/projects/shared/src/i18n/dictionaries/en.ts @@ -99,6 +99,7 @@ export const ENGLISH: Record = { 'Leave': 102, 'Are you sure?': 103, 'New gateway': 104, // as in, a network gateway + 'Tor Domains': 107, 'public': 108, 'private': 109, 'No Tor domains': 111, @@ -639,13 +640,11 @@ export const ENGLISH: Record = { 'Starting setup': 667, 'Wait 1-2 minutes and refresh the page': 670, 'Setup Complete!': 672, - 'You can unplug your backup drive': 673, - 'You can unplug your transfer drive': 674, 'http://start.local was for setup only. It will no longer work.': 675, 'Download Address Info': 676, "Contains your server's permanent local address and Root CA": 677, - 'USB Removed': 678, - 'Remove the USB installation media from your server': 679, + 'Remove Installation Media': 678, + 'Remove USB stick or other installation media from your server': 679, 'Restart Server': 680, 'Waiting for server to come back online': 681, 'Server is back online': 682, @@ -680,4 +679,16 @@ export const ENGLISH: Record = { 'Installation Complete!': 714, 'StartOS has been installed successfully.': 715, 'Continue to Setup': 716, + 'Set Outbound Gateway': 717, + 'Current': 718, + 'System default)': 719, + 'Outbound Gateway': 720, + 'Select the gateway for outbound traffic': 721, + 'The type of gateway': 722, + 'Outbound Only': 723, + 'Set as default outbound': 724, + 'Route all outbound traffic through this gateway': 725, + 'Wireguard Config File': 726, + 'Inbound/Outbound': 727, + 'StartTunnel (Inbound/Outbound)': 728, } diff --git a/web/projects/shared/src/i18n/dictionaries/es.ts b/web/projects/shared/src/i18n/dictionaries/es.ts index 3233d1496..df7d867cb 100644 --- a/web/projects/shared/src/i18n/dictionaries/es.ts +++ b/web/projects/shared/src/i18n/dictionaries/es.ts @@ -100,6 +100,7 @@ export default { 102: 'Salir', 103: '¿Estás seguro?', 104: 'Nueva puerta de enlace de red', + 107: 'dominios onion', 108: 'público', 109: 'privado', 111: 'Sin dominios onion', @@ -639,13 +640,11 @@ export default { 667: 'Iniciando configuración', 670: 'Espere 1–2 minutos y actualice la página', 672: '¡Configuración completa!', - 673: 'Puede desconectar su unidad de copia de seguridad', - 674: 'Puede desconectar su unidad de transferencia', 675: 'http://start.local era solo para la configuración. Ya no funcionará.', 676: 'Descargar información de direcciones', 677: 'Contiene la dirección local permanente de su servidor y la CA raíz', - 678: 'USB retirado', - 679: 'Retire el medio de instalación USB de su servidor', + 678: 'Retirar medio de instalación', + 679: 'Retire la memoria USB u otro medio de instalación de su servidor', 680: 'Reiniciar servidor', 681: 'Esperando a que el servidor vuelva a estar en línea', 682: 'El servidor ha vuelto a estar en línea', @@ -680,4 +679,16 @@ export default { 714: '¡Instalación completada!', 715: 'StartOS se ha instalado correctamente.', 716: 'Continuar con la configuración', + 717: '', + 718: '', + 719: '', + 720: '', + 721: '', + 722: '', + 723: '', + 724: '', + 725: '', + 726: '', + 727: '', + 728: '', } satisfies i18n diff --git a/web/projects/shared/src/i18n/dictionaries/fr.ts b/web/projects/shared/src/i18n/dictionaries/fr.ts index fc86d901c..6d418644d 100644 --- a/web/projects/shared/src/i18n/dictionaries/fr.ts +++ b/web/projects/shared/src/i18n/dictionaries/fr.ts @@ -100,6 +100,7 @@ export default { 102: 'Quitter', 103: 'Êtes-vous sûr ?', 104: 'Nouvelle passerelle réseau', + 107: 'domaine onion', 108: 'public', 109: 'privé', 111: 'Aucune domaine onion', @@ -639,13 +640,11 @@ export default { 667: 'Démarrage de la configuration', 670: 'Attendez 1 à 2 minutes puis actualisez la page', 672: 'Configuration terminée !', - 673: 'Vous pouvez débrancher votre disque de sauvegarde', - 674: 'Vous pouvez débrancher votre disque de transfert', 675: 'http://start.local était réservé à la configuration. Il ne fonctionnera plus.', 676: 'Télécharger les informations d’adresse', - 677: 'Contient l’adresse locale permanente de votre serveur et la CA racine', - 678: 'USB retiré', - 679: 'Retirez le support d’installation USB de votre serveur', + 677: 'Contient l\u2019adresse locale permanente de votre serveur et la CA racine', + 678: 'Retirer le support d\u2019installation', + 679: 'Retirez la clé USB ou tout autre support d\u2019installation de votre serveur', 680: 'Redémarrer le serveur', 681: 'En attente du retour en ligne du serveur', 682: 'Le serveur est de nouveau en ligne', @@ -680,4 +679,16 @@ export default { 714: 'Installation terminée !', 715: 'StartOS a été installé avec succès.', 716: 'Continuer vers la configuration', + 717: '', + 718: '', + 719: '', + 720: '', + 721: '', + 722: '', + 723: '', + 724: '', + 725: '', + 726: '', + 727: '', + 728: '', } satisfies i18n diff --git a/web/projects/shared/src/i18n/dictionaries/pl.ts b/web/projects/shared/src/i18n/dictionaries/pl.ts index 3911af7bb..66d6ed757 100644 --- a/web/projects/shared/src/i18n/dictionaries/pl.ts +++ b/web/projects/shared/src/i18n/dictionaries/pl.ts @@ -100,6 +100,7 @@ export default { 102: 'Opuść', 103: 'Czy jesteś pewien?', 104: 'Nowa brama sieciowa', + 107: 'domeny onion', 108: 'publiczny', 109: 'prywatny', 111: 'Brak domeny onion', @@ -639,13 +640,11 @@ export default { 667: 'Rozpoczynanie konfiguracji', 670: 'Poczekaj 1–2 minuty i odśwież stronę', 672: 'Konfiguracja zakończona!', - 673: 'Możesz odłączyć dysk kopii zapasowej', - 674: 'Możesz odłączyć dysk transferowy', 675: 'http://start.local służył tylko do konfiguracji. Nie będzie już działać.', 676: 'Pobierz informacje adresowe', 677: 'Zawiera stały lokalny adres serwera oraz główny urząd certyfikacji (Root CA)', - 678: 'USB usunięty', - 679: 'Usuń instalacyjny nośnik USB z serwera', + 678: 'Usuń nośnik instalacyjny', + 679: 'Usuń pamięć USB lub inny nośnik instalacyjny z serwera', 680: 'Uruchom ponownie serwer', 681: 'Oczekiwanie na ponowne połączenie serwera', 682: 'Serwer jest ponownie online', @@ -680,4 +679,16 @@ export default { 714: 'Instalacja zakończona!', 715: 'StartOS został pomyślnie zainstalowany.', 716: 'Przejdź do konfiguracji', + 717: '', + 718: '', + 719: '', + 720: '', + 721: '', + 722: '', + 723: '', + 724: '', + 725: '', + 726: '', + 727: '', + 728: '', } satisfies i18n diff --git a/web/projects/shared/src/i18n/localize.pipe.ts b/web/projects/shared/src/i18n/localize.pipe.ts index e9f439ec0..1ad76bb0b 100644 --- a/web/projects/shared/src/i18n/localize.pipe.ts +++ b/web/projects/shared/src/i18n/localize.pipe.ts @@ -1,5 +1,6 @@ import { inject, Injectable, Pipe, PipeTransform } from '@angular/core' import { i18nService } from './i18n.service' +import { I18N } from './i18n.providers' import { T } from '@start9labs/start-sdk' @Pipe({ @@ -9,8 +10,10 @@ import { T } from '@start9labs/start-sdk' @Injectable({ providedIn: 'root' }) export class LocalizePipe implements PipeTransform { private readonly i18nService = inject(i18nService) + private readonly i18n = inject(I18N) transform(string: T.LocaleString): string { + this.i18n() // read signal to trigger change detection on language switch return this.i18nService.localize(string) } } diff --git a/web/projects/start-tunnel/src/app/services/patch-db/data-model.ts b/web/projects/start-tunnel/src/app/services/patch-db/data-model.ts index f84546717..73548d7a5 100644 --- a/web/projects/start-tunnel/src/app/services/patch-db/data-model.ts +++ b/web/projects/start-tunnel/src/app/services/patch-db/data-model.ts @@ -45,8 +45,8 @@ export const mockTunnelData: TunnelData = { gateways: { eth0: { name: null, - public: null, secure: null, + type: null, ipInfo: { name: 'Wired Connection 1', scopeId: 1, diff --git a/web/projects/ui/src/app/routes/portal/routes/services/components/interface-item.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/components/interface-item.component.ts index 4e76f7e54..9a5a94387 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/components/interface-item.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/components/interface-item.component.ts @@ -15,7 +15,6 @@ import { TuiBadge } from '@taiga-ui/kit' `, styles: ` :host { - clip-path: inset(0 round 0.75rem); cursor: pointer; &:hover { diff --git a/web/projects/ui/src/app/routes/portal/routes/services/routes/actions.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/routes/actions.component.ts index 321423718..f8b0fcf47 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/routes/actions.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/routes/actions.component.ts @@ -6,12 +6,18 @@ import { inject, } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' -import { getPkgId, i18nPipe } from '@start9labs/shared' -import { T } from '@start9labs/start-sdk' +import { + ErrorService, + getPkgId, + i18nPipe, + LoadingService, +} from '@start9labs/shared' +import { ISB, T } from '@start9labs/start-sdk' import { TuiCell } from '@taiga-ui/layout' import { PatchDB } from 'patch-db-client' -import { map } from 'rxjs' +import { firstValueFrom, map } from 'rxjs' import { ActionService } from 'src/app/services/action.service' +import { ApiService } from 'src/app/services/api/embassy-api.service' import { DataModel } from 'src/app/services/patch-db/data-model' import { StandardActionsService } from 'src/app/services/standard-actions.service' import { getManifest } from 'src/app/utils/get-package-data' @@ -20,6 +26,9 @@ import { PrimaryStatus, renderPkgStatus, } from 'src/app/services/pkg-status-rendering.service' +import { FormDialogService } from 'src/app/services/form-dialog.service' +import { FormComponent } from 'src/app/routes/portal/components/form.component' +import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' const INACTIVE: PrimaryStatus[] = [ 'installing', @@ -65,6 +74,12 @@ const ALLOWED_STATUSES: Record> = {
StartOS
+
`, - styles: ` - :host { - max-width: 64rem; - } - `, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ CommonModule, @@ -85,8 +81,19 @@ export default class GatewaysComponent { default: null, placeholder: 'StartTunnel 1', }), + type: ISB.Value.select({ + name: this.i18n.transform('Type'), + description: this.i18n.transform('The type of gateway'), + default: 'inbound-outbound', + values: { + 'inbound-outbound': this.i18n.transform( + 'StartTunnel (Inbound/Outbound)', + ), + 'outbound-only': this.i18n.transform('Outbound Only'), + }, + }), config: ISB.Value.union({ - name: this.i18n.transform('StartTunnel Config File'), + name: this.i18n.transform('Wireguard Config File'), default: 'paste', variants: ISB.Variants.of({ paste: { @@ -113,10 +120,17 @@ export default class GatewaysComponent { }, }), }), + setAsDefaultOutbound: ISB.Value.toggle({ + name: this.i18n.transform('Set as default outbound'), + description: this.i18n.transform( + 'Route all outbound traffic through this gateway', + ), + default: false, + }), }) this.formDialog.open(FormComponent, { - label: 'Add StartTunnel Gateway', + label: 'Add Wireguard Gateway', data: { spec: await configBuilderToSpec(spec), buttons: [ @@ -132,7 +146,8 @@ export default class GatewaysComponent { input.config.selection === 'paste' ? input.config.value.file : await (input.config.value.file as any as File).text(), - public: false, + type: input.type as RR.GatewayType, + setAsDefaultOutbound: input.setAsDefaultOutbound, }) return true } catch (e: any) { diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/item.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/item.component.ts index 430c7e0df..14f63662d 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/item.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/item.component.ts @@ -15,6 +15,7 @@ import { TuiButton, TuiDataList, TuiDropdown, + TuiIcon, TuiOptGroup, TuiTextfield, } from '@taiga-ui/core' @@ -24,32 +25,55 @@ import { ApiService } from 'src/app/services/api/embassy-api.service' import { FormDialogService } from 'src/app/services/form-dialog.service' import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' import { GatewayPlus } from 'src/app/services/gateway.service' +import { TuiBadge } from '@taiga-ui/kit' @Component({ selector: 'tr[gateway]', template: ` @if (gateway(); as gateway) { - + {{ gateway.name }} - - - @if (gateway.ipInfo.deviceType; as type) { - {{ type }} ({{ - gateway.public ? ('public' | i18n) : ('private' | i18n) - }}) - } @else { - - + @if (gateway.isDefaultOutbound) { + Default outbound + } + + + @switch (gateway.ipInfo.deviceType) { + @case ('ethernet') { + + {{ 'Ethernet' | i18n }} + } + @case ('wireless') { + + {{ 'WiFi' | i18n }} + } + @case ('wireguard') { + + {{ 'WireGuard' | i18n }} + } + @default { + {{ gateway.ipInfo.deviceType }} + } + } + + + @if (gateway.type === 'outbound-only') { + + {{ 'Outbound Only' | i18n }} + } @else { + + {{ 'Inbound/Outbound' | i18n }} } - {{ gateway.lanIpv4.join(', ') }} {{ gateway.ipInfo.wanIp || ('Error' | i18n) }} + {{ gateway.lanIpv4.join(', ') || '-' }} + + } @if (gateway.ipInfo.deviceType === 'wireguard') {