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
This commit is contained in:
Aiden McClelland
2026-02-10 13:28:24 -07:00
parent 1974dfd66f
commit 2ee403e7de
53 changed files with 3147 additions and 9306 deletions

View File

@@ -1,5 +1,6 @@
{ {
"attribution": { "attribution": {
"commit": "" "commit": "",
"pr": ""
} }
} }

View File

@@ -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 | | `sdk/base/lib/interfaces/Host.ts` | SDK `MultiHost.bindPort()` — no changes needed |
| `core/src/db/model/public.rs` | Public DB model — port forward mapping | | `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 - [ ] Auto-configure port forwards via UPnP/NAT-PMP/PCP - @dr-bonez
**Blocked by**: "Support preferred external ports besides 443" (must be implemented and tested **Blocked by**: "Support preferred external ports besides 443" (must be implemented and tested

2724
core/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,7 @@ license = "MIT"
name = "start-os" name = "start-os"
readme = "README.md" readme = "README.md"
repository = "https://github.com/Start9Labs/start-os" repository = "https://github.com/Start9Labs/start-os"
version = "0.4.0-alpha.19" # VERSION_BUMP version = "0.4.0-alpha.20" # VERSION_BUMP
[lib] [lib]
name = "startos" name = "startos"
@@ -42,17 +42,6 @@ name = "tunnelbox"
path = "src/main/tunnelbox.rs" path = "src/main/tunnelbox.rs"
[features] [features]
arti = [
"arti-client",
"safelog",
"tor-cell",
"tor-hscrypto",
"tor-hsservice",
"tor-keymgr",
"tor-llcrypto",
"tor-proto",
"tor-rtcompat",
]
beta = [] beta = []
console = ["console-subscriber", "tokio/tracing"] console = ["console-subscriber", "tokio/tracing"]
default = [] default = []
@@ -62,16 +51,6 @@ unstable = ["backtrace-on-stack-overflow"]
[dependencies] [dependencies]
aes = { version = "0.7.5", features = ["ctr"] } 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 = [ async-acme = { version = "0.6.0", git = "https://github.com/dr-bonez/async-acme.git", features = [
"use_rustls", "use_rustls",
"use_tokio", "use_tokio",
@@ -100,7 +79,6 @@ console-subscriber = { version = "0.5.0", optional = true }
const_format = "0.2.34" const_format = "0.2.34"
cookie = "0.18.0" cookie = "0.18.0"
cookie_store = "0.22.0" cookie_store = "0.22.0"
curve25519-dalek = "4.1.3"
der = { version = "0.7.9", features = ["derive", "pem"] } der = { version = "0.7.9", features = ["derive", "pem"] }
digest = "0.10.7" digest = "0.10.7"
divrem = "1.0.0" divrem = "1.0.0"
@@ -216,7 +194,6 @@ rpassword = "7.2.0"
rust-argon2 = "3.0.0" rust-argon2 = "3.0.0"
rust-i18n = "3.1.5" rust-i18n = "3.1.5"
rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git" } 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"] } semver = { version = "1.0.20", features = ["serde"] }
serde = { version = "1.0", features = ["derive", "rc"] } serde = { version = "1.0", features = ["derive", "rc"] }
serde_cbor = { package = "ciborium", version = "0.2.1" } 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-tar = { git = "https://github.com/dr-bonez/tokio-tar.git" }
tokio-tungstenite = { version = "0.26.2", features = ["native-tls", "url"] } tokio-tungstenite = { version = "0.26.2", features = ["native-tls", "url"] }
tokio-util = { version = "0.7.9", features = ["io"] } 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" tower-service = "0.3.3"
tracing = "0.1.39" tracing = "0.1.39"
tracing-error = "0.2.0" tracing-error = "0.2.0"

View File

@@ -8,7 +8,6 @@ use openssl::x509::X509;
use crate::db::model::DatabaseModel; use crate::db::model::DatabaseModel;
use crate::hostname::{Hostname, generate_hostname, generate_id}; use crate::hostname::{Hostname, generate_hostname, generate_id};
use crate::net::ssl::{gen_nistp256, make_root_cert}; use crate::net::ssl::{gen_nistp256, make_root_cert};
use crate::net::tor::TorSecretKey;
use crate::prelude::*; use crate::prelude::*;
use crate::util::serde::Pem; use crate::util::serde::Pem;
@@ -26,7 +25,6 @@ pub struct AccountInfo {
pub server_id: String, pub server_id: String,
pub hostname: Hostname, pub hostname: Hostname,
pub password: String, pub password: String,
pub tor_keys: Vec<TorSecretKey>,
pub root_ca_key: PKey<Private>, pub root_ca_key: PKey<Private>,
pub root_ca_cert: X509, pub root_ca_cert: X509,
pub ssh_key: ssh_key::PrivateKey, pub ssh_key: ssh_key::PrivateKey,
@@ -36,7 +34,6 @@ impl AccountInfo {
pub fn new(password: &str, start_time: SystemTime) -> Result<Self, Error> { pub fn new(password: &str, start_time: SystemTime) -> Result<Self, Error> {
let server_id = generate_id(); let server_id = generate_id();
let hostname = generate_hostname(); let hostname = generate_hostname();
let tor_key = vec![TorSecretKey::generate()];
let root_ca_key = gen_nistp256()?; let root_ca_key = gen_nistp256()?;
let root_ca_cert = make_root_cert(&root_ca_key, &hostname, start_time)?; 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( let ssh_key = ssh_key::PrivateKey::from(ssh_key::private::Ed25519Keypair::random(
@@ -48,7 +45,6 @@ impl AccountInfo {
server_id, server_id,
hostname, hostname,
password: hash_password(password)?, password: hash_password(password)?,
tor_keys: tor_key,
root_ca_key, root_ca_key,
root_ca_cert, root_ca_cert,
ssh_key, ssh_key,
@@ -61,17 +57,6 @@ impl AccountInfo {
let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?); let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?);
let password = db.as_private().as_password().de()?; let password = db.as_private().as_password().de()?;
let key_store = db.as_private().as_key_store(); 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::<Result<_, _>>()?;
let cert_store = key_store.as_local_certs(); let cert_store = key_store.as_local_certs();
let root_ca_key = cert_store.as_root_key().de()?.0; let root_ca_key = cert_store.as_root_key().de()?.0;
let root_ca_cert = cert_store.as_root_cert().de()?.0; let root_ca_cert = cert_store.as_root_cert().de()?.0;
@@ -82,7 +67,6 @@ impl AccountInfo {
server_id, server_id,
hostname, hostname,
password, password,
tor_keys,
root_ca_key, root_ca_key,
root_ca_cert, root_ca_cert,
ssh_key, ssh_key,
@@ -97,17 +81,6 @@ impl AccountInfo {
server_info server_info
.as_pubkey_mut() .as_pubkey_mut()
.ser(&self.ssh_key.public_key().to_openssh()?)?; .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)?; server_info.as_password_hash_mut().ser(&self.password)?;
db.as_private_mut().as_password_mut().ser(&self.password)?; db.as_private_mut().as_password_mut().ser(&self.password)?;
db.as_private_mut() db.as_private_mut()
@@ -117,9 +90,6 @@ impl AccountInfo {
.as_developer_key_mut() .as_developer_key_mut()
.ser(Pem::new_ref(&self.developer_key))?; .ser(Pem::new_ref(&self.developer_key))?;
let key_store = db.as_private_mut().as_key_store_mut(); 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(); let cert_store = key_store.as_local_certs_mut();
if cert_store.as_root_cert().de()?.0 != self.root_ca_cert { if cert_store.as_root_cert().de()?.0 != self.root_ca_cert {
cert_store cert_store
@@ -148,11 +118,5 @@ impl AccountInfo {
self.hostname.no_dot_host_name(), self.hostname.no_dot_host_name(),
self.hostname.local_domain_name(), self.hostname.local_domain_name(),
] ]
.into_iter()
.chain(
self.tor_keys
.iter()
.map(|k| InternedString::from_display(&k.onion_address())),
)
} }
} }

View File

@@ -7,9 +7,7 @@ use ssh_key::private::Ed25519Keypair;
use crate::account::AccountInfo; use crate::account::AccountInfo;
use crate::hostname::{Hostname, generate_hostname, generate_id}; use crate::hostname::{Hostname, generate_hostname, generate_id};
use crate::net::tor::TorSecretKey;
use crate::prelude::*; use crate::prelude::*;
use crate::util::crypto::ed25519_expand_key;
use crate::util::serde::{Base32, Base64, Pem}; use crate::util::serde::{Base32, Base64, Pem};
pub struct OsBackup { pub struct OsBackup {
@@ -85,10 +83,6 @@ impl OsBackupV0 {
&mut ssh_key::rand_core::OsRng::default(), &mut ssh_key::rand_core::OsRng::default(),
ssh_key::Algorithm::Ed25519, ssh_key::Algorithm::Ed25519,
)?, )?,
tor_keys: TorSecretKey::from_bytes(self.tor_key.0)
.ok()
.into_iter()
.collect(),
developer_key: ed25519_dalek::SigningKey::generate( developer_key: ed25519_dalek::SigningKey::generate(
&mut ssh_key::rand_core::OsRng::default(), &mut ssh_key::rand_core::OsRng::default(),
), ),
@@ -119,10 +113,6 @@ impl OsBackupV1 {
root_ca_key: self.root_ca_key.0, root_ca_key: self.root_ca_key.0,
root_ca_cert: self.root_ca_cert.0, root_ca_cert: self.root_ca_cert.0,
ssh_key: ssh_key::PrivateKey::from(Ed25519Keypair::from_seed(&self.net_key.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), developer_key: ed25519_dalek::SigningKey::from_bytes(&self.net_key),
}, },
ui: self.ui, ui: self.ui,
@@ -140,7 +130,6 @@ struct OsBackupV2 {
root_ca_key: Pem<PKey<Private>>, // PEM Encoded OpenSSL Key root_ca_key: Pem<PKey<Private>>, // PEM Encoded OpenSSL Key
root_ca_cert: Pem<X509>, // PEM Encoded OpenSSL X509 Certificate root_ca_cert: Pem<X509>, // PEM Encoded OpenSSL X509 Certificate
ssh_key: Pem<ssh_key::PrivateKey>, // PEM Encoded OpenSSH Key ssh_key: Pem<ssh_key::PrivateKey>, // PEM Encoded OpenSSH Key
tor_keys: Vec<TorSecretKey>, // Base64 Encoded Ed25519 Expanded Secret Key
compat_s9pk_key: Pem<ed25519_dalek::SigningKey>, // PEM Encoded ED25519 Key compat_s9pk_key: Pem<ed25519_dalek::SigningKey>, // PEM Encoded ED25519 Key
ui: Value, // JSON Value ui: Value, // JSON Value
} }
@@ -154,7 +143,6 @@ impl OsBackupV2 {
root_ca_key: self.root_ca_key.0, root_ca_key: self.root_ca_key.0,
root_ca_cert: self.root_ca_cert.0, root_ca_cert: self.root_ca_cert.0,
ssh_key: self.ssh_key.0, ssh_key: self.ssh_key.0,
tor_keys: self.tor_keys,
developer_key: self.compat_s9pk_key.0, developer_key: self.compat_s9pk_key.0,
}, },
ui: self.ui, ui: self.ui,
@@ -167,7 +155,6 @@ impl OsBackupV2 {
root_ca_key: Pem(backup.account.root_ca_key.clone()), root_ca_key: Pem(backup.account.root_ca_key.clone()),
root_ca_cert: Pem(backup.account.root_ca_cert.clone()), root_ca_cert: Pem(backup.account.root_ca_cert.clone()),
ssh_key: Pem(backup.account.ssh_key.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()), compat_s9pk_key: Pem(backup.account.developer_key.clone()),
ui: backup.ui.clone(), ui: backup.ui.clone(),
} }

View File

@@ -89,7 +89,6 @@ impl Public {
)] )]
.into_iter() .into_iter()
.collect(), .collect(),
onions: account.tor_keys.iter().map(|k| k.onion_address()).collect(),
public_domains: BTreeMap::new(), public_domains: BTreeMap::new(),
private_domains: BTreeSet::new(), private_domains: BTreeSet::new(),
hostname_info: BTreeMap::new(), hostname_info: BTreeMap::new(),

View File

@@ -42,11 +42,11 @@ pub enum ErrorKind {
ParseUrl = 19, ParseUrl = 19,
DiskNotAvailable = 20, DiskNotAvailable = 20,
BlockDevice = 21, BlockDevice = 21,
InvalidOnionAddress = 22, // InvalidOnionAddress = 22,
Pack = 23, Pack = 23,
ValidateS9pk = 24, ValidateS9pk = 24,
DiskCorrupted = 25, // Remove DiskCorrupted = 25, // Remove
Tor = 26, // Tor = 26,
ConfigGen = 27, ConfigGen = 27,
ParseNumber = 28, ParseNumber = 28,
Database = 29, Database = 29,
@@ -126,11 +126,11 @@ impl ErrorKind {
ParseUrl => t!("error.parse-url"), ParseUrl => t!("error.parse-url"),
DiskNotAvailable => t!("error.disk-not-available"), DiskNotAvailable => t!("error.disk-not-available"),
BlockDevice => t!("error.block-device"), BlockDevice => t!("error.block-device"),
InvalidOnionAddress => t!("error.invalid-onion-address"), // InvalidOnionAddress => t!("error.invalid-onion-address"),
Pack => t!("error.pack"), Pack => t!("error.pack"),
ValidateS9pk => t!("error.validate-s9pk"), ValidateS9pk => t!("error.validate-s9pk"),
DiskCorrupted => t!("error.disk-corrupted"), // Remove DiskCorrupted => t!("error.disk-corrupted"), // Remove
Tor => t!("error.tor"), // Tor => t!("error.tor"),
ConfigGen => t!("error.config-gen"), ConfigGen => t!("error.config-gen"),
ParseNumber => t!("error.parse-number"), ParseNumber => t!("error.parse-number"),
Database => t!("error.database"), Database => t!("error.database"),
@@ -370,17 +370,6 @@ impl From<reqwest::Error> for Error {
Error::new(e, kind) Error::new(e, kind)
} }
} }
#[cfg(feature = "arti")]
impl From<arti_client::Error> for Error {
fn from(e: arti_client::Error) -> Self {
Error::new(e, ErrorKind::Tor)
}
}
impl From<torut::control::ConnError> for Error {
fn from(e: torut::control::ConnError) -> Self {
Error::new(e, ErrorKind::Tor)
}
}
impl From<zbus::Error> for Error { impl From<zbus::Error> for Error {
fn from(e: zbus::Error) -> Self { fn from(e: zbus::Error) -> Self {
Error::new(e, ErrorKind::DBus) Error::new(e, ErrorKind::DBus)

View File

@@ -12,7 +12,6 @@ use crate::context::{CliContext, RpcContext};
use crate::db::model::DatabaseModel; use crate::db::model::DatabaseModel;
use crate::net::acme::AcmeProvider; use crate::net::acme::AcmeProvider;
use crate::net::host::{HostApiKind, all_hosts}; use crate::net::host::{HostApiKind, all_hosts};
use crate::net::tor::OnionAddress;
use crate::prelude::*; use crate::prelude::*;
use crate::util::serde::{HandlerExtSerde, display_serializable}; use crate::util::serde::{HandlerExtSerde, display_serializable};
@@ -21,9 +20,6 @@ use crate::util::serde::{HandlerExtSerde, display_serializable};
#[serde(rename_all_fields = "camelCase")] #[serde(rename_all_fields = "camelCase")]
#[serde(tag = "kind")] #[serde(tag = "kind")]
pub enum HostAddress { pub enum HostAddress {
Onion {
address: OnionAddress,
},
Domain { Domain {
address: InternedString, address: InternedString,
public: Option<PublicDomainConfig>, public: Option<PublicDomainConfig>,
@@ -38,18 +34,7 @@ pub struct PublicDomainConfig {
} }
fn handle_duplicates(db: &mut DatabaseModel) -> Result<(), Error> { fn handle_duplicates(db: &mut DatabaseModel) -> Result<(), Error> {
let mut onions = BTreeSet::<OnionAddress>::new();
let mut domains = BTreeSet::<InternedString>::new(); let mut domains = BTreeSet::<InternedString>::new();
let check_onion = |onions: &mut BTreeSet<OnionAddress>, 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<InternedString>, domain: InternedString| { let check_domain = |domains: &mut BTreeSet<InternedString>, domain: InternedString| {
if domains.contains(&domain) { if domains.contains(&domain) {
return Err(Error::new( return Err(Error::new(
@@ -68,9 +53,6 @@ fn handle_duplicates(db: &mut DatabaseModel) -> Result<(), Error> {
not_in_use.push(host); not_in_use.push(host);
continue; continue;
} }
for onion in host.as_onions().de()? {
check_onion(&mut onions, onion)?;
}
let public = host.as_public_domains().keys()?; let public = host.as_public_domains().keys()?;
for domain in &public { for domain in &public {
check_domain(&mut domains, domain.clone())?; check_domain(&mut domains, domain.clone())?;
@@ -82,16 +64,11 @@ fn handle_duplicates(db: &mut DatabaseModel) -> Result<(), Error> {
} }
} }
for host in not_in_use { for host in not_in_use {
host.as_onions_mut()
.mutate(|o| Ok(o.retain(|o| !onions.contains(o))))?;
host.as_public_domains_mut() host.as_public_domains_mut()
.mutate(|d| Ok(d.retain(|d, _| !domains.contains(d))))?; .mutate(|d| Ok(d.retain(|d, _| !domains.contains(d))))?;
host.as_private_domains_mut() host.as_private_domains_mut()
.mutate(|d| Ok(d.retain(|d| !domains.contains(d))))?; .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()?; let public = host.as_public_domains().keys()?;
for domain in &public { for domain in &public {
check_domain(&mut domains, domain.clone())?; check_domain(&mut domains, domain.clone())?;
@@ -159,29 +136,6 @@ pub fn address_api<C: Context, Kind: HostApiKind>()
) )
.with_inherited(Kind::inheritance), .with_inherited(Kind::inheritance),
) )
.subcommand(
"onion",
ParentHandler::<C, Empty, Kind::Inheritance>::new()
.subcommand(
"add",
from_fn_async(add_onion::<Kind>)
.with_metadata("sync_db", Value::Bool(true))
.with_inherited(|_, a| a)
.no_display()
.with_about("about.add-address-to-host")
.with_call_remote::<CliContext>(),
)
.subcommand(
"remove",
from_fn_async(remove_onion::<Kind>)
.with_metadata("sync_db", Value::Bool(true))
.with_inherited(|_, a| a)
.no_display()
.with_about("about.remove-address-from-host")
.with_call_remote::<CliContext>(),
)
.with_inherited(Kind::inheritance),
)
.subcommand( .subcommand(
"list", "list",
from_fn_async(list_addresses::<Kind>) from_fn_async(list_addresses::<Kind>)
@@ -199,9 +153,6 @@ pub fn address_api<C: Context, Kind: HostApiKind>()
table.add_row(row![bc => "ADDRESS", "PUBLIC", "ACME PROVIDER"]); table.add_row(row![bc => "ADDRESS", "PUBLIC", "ACME PROVIDER"]);
for address in &res { for address in &res {
match address { match address {
HostAddress::Onion { address } => {
table.add_row(row![address, true, "N/A"]);
}
HostAddress::Domain { HostAddress::Domain {
address, address,
public: Some(PublicDomainConfig { gateway, acme }), public: Some(PublicDomainConfig { gateway, acme }),
@@ -351,55 +302,6 @@ pub async fn remove_private_domain<Kind: HostApiKind>(
Ok(()) Ok(())
} }
#[derive(Deserialize, Serialize, Parser)]
pub struct OnionParams {
#[arg(help = "help.arg.onion-address")]
pub onion: String,
}
pub async fn add_onion<Kind: HostApiKind>(
ctx: RpcContext,
OnionParams { onion }: OnionParams,
inheritance: Kind::Inheritance,
) -> Result<(), Error> {
let onion = onion.parse::<OnionAddress>()?;
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<Kind: HostApiKind>(
ctx: RpcContext,
OnionParams { onion }: OnionParams,
inheritance: Kind::Inheritance,
) -> Result<(), Error> {
let onion = onion.parse::<OnionAddress>()?;
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<Kind: HostApiKind>( pub async fn list_addresses<Kind: HostApiKind>(
ctx: RpcContext, ctx: RpcContext,
_: Empty, _: Empty,

View File

@@ -15,7 +15,6 @@ use crate::net::forward::AvailablePorts;
use crate::net::host::address::{HostAddress, PublicDomainConfig, address_api}; use crate::net::host::address::{HostAddress, PublicDomainConfig, address_api};
use crate::net::host::binding::{BindInfo, BindOptions, binding}; use crate::net::host::binding::{BindInfo, BindOptions, binding};
use crate::net::service_interface::HostnameInfo; use crate::net::service_interface::HostnameInfo;
use crate::net::tor::OnionAddress;
use crate::prelude::*; use crate::prelude::*;
use crate::{HostId, PackageId}; use crate::{HostId, PackageId};
@@ -28,8 +27,6 @@ pub mod binding;
#[ts(export)] #[ts(export)]
pub struct Host { pub struct Host {
pub bindings: BTreeMap<u16, BindInfo>, pub bindings: BTreeMap<u16, BindInfo>,
#[ts(type = "string[]")]
pub onions: BTreeSet<OnionAddress>,
pub public_domains: BTreeMap<InternedString, PublicDomainConfig>, pub public_domains: BTreeMap<InternedString, PublicDomainConfig>,
pub private_domains: BTreeSet<InternedString>, pub private_domains: BTreeSet<InternedString>,
/// COMPUTED: NetService::update /// COMPUTED: NetService::update
@@ -46,19 +43,13 @@ impl Host {
Self::default() Self::default()
} }
pub fn addresses<'a>(&'a self) -> impl Iterator<Item = HostAddress> + 'a { pub fn addresses<'a>(&'a self) -> impl Iterator<Item = HostAddress> + 'a {
self.onions self.public_domains
.iter() .iter()
.cloned() .map(|(address, config)| HostAddress::Domain {
.map(|address| HostAddress::Onion { address }) address: address.clone(),
.chain( public: Some(config.clone()),
self.public_domains private: self.private_domains.contains(address),
.iter() })
.map(|(address, config)| HostAddress::Domain {
address: address.clone(),
public: Some(config.clone()),
private: self.private_domains.contains(address),
}),
)
.chain( .chain(
self.private_domains self.private_domains
.iter() .iter()
@@ -112,22 +103,7 @@ pub fn host_for<'a>(
.as_hosts_mut(), .as_hosts_mut(),
) )
} }
let tor_key = if host_info(db, package_id)?.as_idx(host_id).is_none() { host_info(db, package_id)?.upsert(host_id, || Ok(Host::new()))
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)
})
} }
pub fn all_hosts(db: &mut DatabaseModel) -> impl Iterator<Item = Result<&mut Model<Host>, Error>> { pub fn all_hosts(db: &mut DatabaseModel) -> impl Iterator<Item = Result<&mut Model<Host>, Error>> {

View File

@@ -3,28 +3,21 @@ use serde::{Deserialize, Serialize};
use crate::account::AccountInfo; use crate::account::AccountInfo;
use crate::net::acme::AcmeCertStore; use crate::net::acme::AcmeCertStore;
use crate::net::ssl::CertStore; use crate::net::ssl::CertStore;
use crate::net::tor::OnionStore;
use crate::prelude::*; use crate::prelude::*;
#[derive(Debug, Deserialize, Serialize, HasModel)] #[derive(Debug, Deserialize, Serialize, HasModel)]
#[model = "Model<Self>"] #[model = "Model<Self>"]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct KeyStore { pub struct KeyStore {
pub onion: OnionStore,
pub local_certs: CertStore, pub local_certs: CertStore,
#[serde(default)] #[serde(default)]
pub acme: AcmeCertStore, pub acme: AcmeCertStore,
} }
impl KeyStore { impl KeyStore {
pub fn new(account: &AccountInfo) -> Result<Self, Error> { pub fn new(account: &AccountInfo) -> Result<Self, Error> {
let mut res = Self { Ok(Self {
onion: OnionStore::new(),
local_certs: CertStore::new(account)?, local_certs: CertStore::new(account)?,
acme: AcmeCertStore::new(), acme: AcmeCertStore::new(),
}; })
for tor_key in account.tor_keys.iter().cloned() {
res.onion.insert(tor_key);
}
Ok(res)
} }
} }

View File

@@ -14,7 +14,6 @@ pub mod socks;
pub mod ssl; pub mod ssl;
pub mod static_server; pub mod static_server;
pub mod tls; pub mod tls;
pub mod tor;
pub mod tunnel; pub mod tunnel;
pub mod utils; pub mod utils;
pub mod vhost; pub mod vhost;
@@ -23,7 +22,6 @@ pub mod wifi;
pub fn net_api<C: Context>() -> ParentHandler<C> { pub fn net_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new() ParentHandler::new()
.subcommand("tor", tor::tor_api::<C>().with_about("about.tor-commands"))
.subcommand( .subcommand(
"acme", "acme",
acme::acme_api::<C>().with_about("about.setup-acme-certificate"), acme::acme_api::<C>().with_about("about.setup-acme-certificate"),

View File

@@ -3,7 +3,7 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
use imbl::{OrdMap, vector}; use imbl::vector;
use imbl_value::InternedString; use imbl_value::InternedString;
use ipnet::IpNet; use ipnet::IpNet;
use tokio::sync::Mutex; 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::forward::{InterfacePortForwardController, START9_BRIDGE_IFACE, add_iptables_rule};
use crate::net::gateway::{ use crate::net::gateway::{
AndFilter, DynInterfaceFilter, IdFilter, InterfaceFilter, NetworkInterfaceController, OrFilter, AndFilter, DynInterfaceFilter, IdFilter, InterfaceFilter, NetworkInterfaceController, OrFilter,
PublicFilter, SecureFilter, TypeFilter, PublicFilter, SecureFilter,
}; };
use crate::net::host::address::HostAddress; use crate::net::host::address::HostAddress;
use crate::net::host::binding::{AddSslOptions, BindId, BindOptions}; use crate::net::host::binding::{AddSslOptions, BindId, BindOptions};
use crate::net::host::{Host, Hosts, host_for}; 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::socks::SocksController;
use crate::net::tor::{OnionAddress, TorController, TorSecretKey};
use crate::net::utils::ipv6_is_local; use crate::net::utils::ipv6_is_local;
use crate::net::vhost::{AlpnInfo, DynVHostTarget, ProxyTarget, VHostController}; use crate::net::vhost::{AlpnInfo, DynVHostTarget, ProxyTarget, VHostController};
use crate::prelude::*; use crate::prelude::*;
use crate::service::effects::callbacks::ServiceCallbacks; use crate::service::effects::callbacks::ServiceCallbacks;
use crate::util::serde::MaybeUtf8String; use crate::util::serde::MaybeUtf8String;
use crate::{GatewayId, HOST_IP, HostId, OptionExt, PackageId}; use crate::{HOST_IP, HostId, OptionExt, PackageId};
pub struct NetController { pub struct NetController {
pub(crate) db: TypedPatchDb<Database>, pub(crate) db: TypedPatchDb<Database>,
pub(super) tor: TorController,
pub(super) vhost: VHostController, pub(super) vhost: VHostController,
pub(super) tls_client_config: Arc<TlsClientConfig>, pub(super) tls_client_config: Arc<TlsClientConfig>,
pub(crate) net_iface: Arc<NetworkInterfaceController>, pub(crate) net_iface: Arc<NetworkInterfaceController>,
@@ -54,8 +52,7 @@ impl NetController {
socks_listen: SocketAddr, socks_listen: SocketAddr,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let net_iface = Arc::new(NetworkInterfaceController::new(db.clone())); let net_iface = Arc::new(NetworkInterfaceController::new(db.clone()));
let tor = TorController::new()?; let socks = SocksController::new(socks_listen)?;
let socks = SocksController::new(socks_listen, tor.clone())?;
let crypto_provider = Arc::new(tokio_rustls::rustls::crypto::ring::default_provider()); let crypto_provider = Arc::new(tokio_rustls::rustls::crypto::ring::default_provider());
let tls_client_config = Arc::new(crate::net::tls::client_config( let tls_client_config = Arc::new(crate::net::tls::client_config(
crypto_provider.clone(), crypto_provider.clone(),
@@ -87,7 +84,6 @@ impl NetController {
.await?; .await?;
Ok(Self { Ok(Self {
db: db.clone(), db: db.clone(),
tor,
vhost: VHostController::new(db.clone(), net_iface.clone(), crypto_provider), vhost: VHostController::new(db.clone(), net_iface.clone(), crypto_provider),
tls_client_config, tls_client_config,
dns: DnsController::init(db, &net_iface.watcher).await?, dns: DnsController::init(db, &net_iface.watcher).await?,
@@ -168,7 +164,6 @@ struct HostBinds {
forwards: BTreeMap<u16, (SocketAddrV4, DynInterfaceFilter, Arc<()>)>, forwards: BTreeMap<u16, (SocketAddrV4, DynInterfaceFilter, Arc<()>)>,
vhosts: BTreeMap<(Option<InternedString>, u16), (ProxyTarget, Arc<()>)>, vhosts: BTreeMap<(Option<InternedString>, u16), (ProxyTarget, Arc<()>)>,
private_dns: BTreeMap<InternedString, Arc<()>>, private_dns: BTreeMap<InternedString, Arc<()>>,
tor: BTreeMap<OnionAddress, (OrdMap<u16, SocketAddr>, Vec<Arc<()>>)>,
} }
pub struct NetServiceData { pub struct NetServiceData {
@@ -260,8 +255,6 @@ impl NetServiceData {
let mut forwards: BTreeMap<u16, (SocketAddrV4, DynInterfaceFilter)> = BTreeMap::new(); let mut forwards: BTreeMap<u16, (SocketAddrV4, DynInterfaceFilter)> = BTreeMap::new();
let mut vhosts: BTreeMap<(Option<InternedString>, u16), ProxyTarget> = BTreeMap::new(); let mut vhosts: BTreeMap<(Option<InternedString>, u16), ProxyTarget> = BTreeMap::new();
let mut private_dns: BTreeSet<InternedString> = BTreeSet::new(); let mut private_dns: BTreeSet<InternedString> = BTreeSet::new();
let mut tor: BTreeMap<OnionAddress, (TorSecretKey, OrdMap<u16, SocketAddr>)> =
BTreeMap::new();
let mut hostname_info: BTreeMap<u16, Vec<HostnameInfo>> = BTreeMap::new(); let mut hostname_info: BTreeMap<u16, Vec<HostnameInfo>> = BTreeMap::new();
let binds = self.binds.entry(id.clone()).or_default(); let binds = self.binds.entry(id.clone()).or_default();
@@ -308,29 +301,6 @@ impl NetServiceData {
} }
for address in host.addresses() { for address in host.addresses() {
match address { 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 { HostAddress::Domain {
address, address,
public, public,
@@ -615,66 +585,6 @@ impl NetServiceData {
} }
} }
struct TorHostnamePorts {
non_ssl: Option<u16>,
ssl: Option<u16>,
}
let mut tor_hostname_ports = BTreeMap::<u16, TorHostnamePorts>::new();
let mut tor_binds = OrdMap::<u16, SocketAddr>::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 let all = binds
.forwards .forwards
.keys() .keys()
@@ -763,48 +673,9 @@ impl NetServiceData {
} }
ctrl.dns.gc_private_domains(&rm)?; ctrl.dns.gc_private_domains(&rm)?;
let all = binds
.tor
.keys()
.chain(tor.keys())
.cloned()
.collect::<BTreeSet<_>>();
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 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)) {
if let Some(cbs) = ctrl.callbacks.get_host_info(&(pkg_id.clone(), id)) { cbs.call(vector![]).await?;
cbs.call(vector![]).await?;
}
} }
} }
Ok(()) Ok(())

View File

@@ -17,15 +17,11 @@ pub enum HostnameInfo {
public: bool, public: bool,
hostname: IpHostname, hostname: IpHostname,
}, },
Onion {
hostname: OnionHostname,
},
} }
impl HostnameInfo { impl HostnameInfo {
pub fn to_san_hostname(&self) -> InternedString { pub fn to_san_hostname(&self) -> InternedString {
match self { match self {
Self::Ip { hostname, .. } => hostname.to_san_hostname(), Self::Ip { hostname, .. } => hostname.to_san_hostname(),
Self::Onion { hostname } => hostname.to_san_hostname(),
} }
} }
} }
@@ -39,21 +35,6 @@ pub struct GatewayInfo {
pub public: bool, 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<u16>,
pub ssl_port: Option<u16>,
}
impl OnionHostname {
pub fn to_san_hostname(&self) -> InternedString {
self.value.clone()
}
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]

View File

@@ -8,7 +8,6 @@ use socks5_impl::server::{AuthAdaptor, ClientConnection, Server};
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
use crate::HOST_IP; use crate::HOST_IP;
use crate::net::tor::TorController;
use crate::prelude::*; use crate::prelude::*;
use crate::util::actor::background::BackgroundJobQueue; use crate::util::actor::background::BackgroundJobQueue;
use crate::util::future::NonDetachingJoinHandle; use crate::util::future::NonDetachingJoinHandle;
@@ -22,7 +21,7 @@ pub struct SocksController {
_thread: NonDetachingJoinHandle<()>, _thread: NonDetachingJoinHandle<()>,
} }
impl SocksController { impl SocksController {
pub fn new(listen: SocketAddr, tor: TorController) -> Result<Self, Error> { pub fn new(listen: SocketAddr) -> Result<Self, Error> {
Ok(Self { Ok(Self {
_thread: tokio::spawn(async move { _thread: tokio::spawn(async move {
let auth: AuthAdaptor<()> = Arc::new(NoAuth); let auth: AuthAdaptor<()> = Arc::new(NoAuth);
@@ -45,7 +44,6 @@ impl SocksController {
loop { loop {
match server.accept().await { match server.accept().await {
Ok((stream, _)) => { Ok((stream, _)) => {
let tor = tor.clone();
bg.add_job(async move { bg.add_job(async move {
if let Err(e) = async { if let Err(e) = async {
match stream match stream
@@ -57,40 +55,6 @@ impl SocksController {
.await .await
.with_kind(ErrorKind::Network)? .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) => { ClientConnection::Connect(reply, addr) => {
if let Ok(mut target) = match addr { if let Ok(mut target) = match addr {
Address::DomainAddress(domain, port) => { Address::DomainAddress(domain, port) => {

View File

@@ -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<Self, Self::Err> {
Ok(Self(
if s.ends_with(".onion") {
Cow::Borrowed(s)
} else {
Cow::Owned(format!("{s}.onion"))
}
.parse::<HsId>()
.with_kind(ErrorKind::Tor)?,
))
}
}
impl Serialize for OnionAddress {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serialize_display(self, serializer)
}
}
impl<'de> Deserialize<'de> for OnionAddress {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<std::cmp::Ordering> {
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<Self, Error> {
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, Self::Err> {
Self::from_bytes(Base64::<[u8; 64]>::from_str(s)?.0)
}
}
impl Serialize for TorSecretKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serialize_display(self, serializer)
}
}
impl<'de> Deserialize<'de> for TorSecretKey {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserialize_from_str(deserializer)
}
}
#[derive(Default, Deserialize, Serialize)]
pub struct OnionStore(BTreeMap<OnionAddress, TorSecretKey>);
impl Map for OnionStore {
type Key = OnionAddress;
type Value = TorSecretKey;
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
Self::key_string(key)
}
fn key_string(key: &Self::Key) -> Result<imbl_value::InternedString, Error> {
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<OnionStore> {
pub fn new_key(&mut self) -> Result<TorSecretKey, Error> {
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<TorSecretKey, Error> {
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<OnionAddress, TorSecretKey>);
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<C: Context>() -> ParentHandler<C> {
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::<CliContext>(),
)
.subcommand(
"reset",
from_fn_async(reset)
.no_display()
.with_about("about.reset-tor-daemon")
.with_call_remote::<CliContext>(),
)
.subcommand(
"key",
key::<C>().with_about("about.manage-onion-service-key-store"),
)
}
pub fn key<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"generate",
from_fn_async(generate_key)
.with_about("about.generate-onion-service-key-add-to-store")
.with_call_remote::<CliContext>(),
)
.subcommand(
"add",
from_fn_async(add_key)
.with_about("about.add-onion-service-key-to-store")
.with_call_remote::<CliContext>(),
)
.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::<CliContext>(),
)
}
pub async fn generate_key(ctx: RpcContext) -> Result<OnionAddress, Error> {
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<OnionAddress, Error> {
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<BTreeSet<OnionAddress>, 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<Empty>,
services: BTreeMap<OnionAddress, OnionServiceInfo>,
) -> 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<ArtiOnionServiceState> 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<u16, SocketAddr>,
}
pub async fn list_services(
ctx: RpcContext,
_: Empty,
) -> Result<BTreeMap<OnionAddress, OnionServiceInfo>, Error> {
ctx.net_controller.tor.list_services().await
}
#[derive(Clone)]
pub struct TorController(Arc<TorControllerInner>);
struct TorControllerInner {
client: Watch<(usize, TorClient<TokioRustlsRuntime>)>,
_bootstrapper: NonDetachingJoinHandle<()>,
services: SyncMutex<BTreeMap<OnionAddress, OnionService>>,
reset: Arc<Notify>,
}
impl TorController {
pub fn new() -> Result<Self, Error> {
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::<HsNickname>()
.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<OnionService, Error> {
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<OnionAddress>) -> 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<BTreeMap<OnionAddress, OnionServiceInfo>, 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<Box<dyn ReadWriter + Unpin + Send + Sync + 'static>, 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<OnionServiceData>);
struct OnionServiceData {
service: Arc<SyncMutex<Option<Arc<RunningOnionService>>>>,
bindings: Arc<SyncRwLock<BTreeMap<u16, BTreeMap<SocketAddr, Weak<()>>>>>,
_thread: NonDetachingJoinHandle<()>,
}
impl OnionService {
fn launch(
mut client: Watch<(usize, TorClient<TokioRustlsRuntime>)>,
key: TorSecretKey,
) -> Result<Self, Error> {
let service = Arc::new(SyncMutex::new(None));
let bindings = Arc::new(SyncRwLock::new(BTreeMap::<
u16,
BTreeMap<SocketAddr, Weak<()>>,
>::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::<HsNickname>()
.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<Rcs: FromIterator<Arc<()>>>(
&self,
bindings: impl IntoIterator<Item = (u16, SocketAddr)>,
) -> Result<Rcs, Error> {
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()
}),
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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};

View File

@@ -55,11 +55,9 @@ pub async fn get_ssl_certificate(
.map(|(_, m)| m.as_hosts().as_entries()) .map(|(_, m)| m.as_hosts().as_entries())
.flatten_ok() .flatten_ok()
.map_ok(|(_, m)| { .map_ok(|(_, m)| {
Ok(m.as_onions() Ok(m.as_public_domains()
.de()? .keys()?
.iter() .into_iter()
.map(InternedString::from_display)
.chain(m.as_public_domains().keys()?)
.chain(m.as_private_domains().de()?) .chain(m.as_private_domains().de()?)
.chain( .chain(
m.as_hostname_info() m.as_hostname_info()
@@ -68,7 +66,7 @@ pub async fn get_ssl_certificate(
.flatten() .flatten()
.map(|h| h.to_san_hostname()), .map(|h| h.to_san_hostname()),
) )
.collect::<Vec<_>>()) .collect::<Vec<InternedString>>())
}) })
.map(|a| a.and_then(|a| a)) .map(|a| a.and_then(|a| a))
.flatten_ok() .flatten_ok()
@@ -181,11 +179,9 @@ pub async fn get_ssl_key(
.map(|m| m.as_hosts().as_entries()) .map(|m| m.as_hosts().as_entries())
.flatten_ok() .flatten_ok()
.map_ok(|(_, m)| { .map_ok(|(_, m)| {
Ok(m.as_onions() Ok(m.as_public_domains()
.de()? .keys()?
.iter() .into_iter()
.map(InternedString::from_display)
.chain(m.as_public_domains().keys()?)
.chain(m.as_private_domains().de()?) .chain(m.as_private_domains().de()?)
.chain( .chain(
m.as_hostname_info() m.as_hostname_info()
@@ -194,7 +190,7 @@ pub async fn get_ssl_key(
.flatten() .flatten()
.map(|h| h.to_san_hostname()), .map(|h| h.to_san_hostname()),
) )
.collect::<Vec<_>>()) .collect::<Vec<InternedString>>())
}) })
.map(|a| a.and_then(|a| a)) .map(|a| a.and_then(|a| a))
.flatten_ok() .flatten_ok()

View File

@@ -59,8 +59,9 @@ mod v0_4_0_alpha_16;
mod v0_4_0_alpha_17; mod v0_4_0_alpha_17;
mod v0_4_0_alpha_18; mod v0_4_0_alpha_18;
mod v0_4_0_alpha_19; 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 { impl Current {
#[instrument(skip(self, db))] #[instrument(skip(self, db))]
@@ -181,7 +182,8 @@ enum Version {
V0_4_0_alpha_16(Wrapper<v0_4_0_alpha_16::Version>), V0_4_0_alpha_16(Wrapper<v0_4_0_alpha_16::Version>),
V0_4_0_alpha_17(Wrapper<v0_4_0_alpha_17::Version>), V0_4_0_alpha_17(Wrapper<v0_4_0_alpha_17::Version>),
V0_4_0_alpha_18(Wrapper<v0_4_0_alpha_18::Version>), V0_4_0_alpha_18(Wrapper<v0_4_0_alpha_18::Version>),
V0_4_0_alpha_19(Wrapper<v0_4_0_alpha_19::Version>), // VERSION_BUMP V0_4_0_alpha_19(Wrapper<v0_4_0_alpha_19::Version>),
V0_4_0_alpha_20(Wrapper<v0_4_0_alpha_20::Version>), // VERSION_BUMP
Other(exver::Version), 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_16(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_17(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_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) => { Self::Other(v) => {
return Err(Error::new( return Err(Error::new(
eyre!("unknown version {v}"), 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_16(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_17(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_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(), Version::Other(x) => x.clone(),
} }
} }

View File

@@ -1,4 +1,4 @@
use std::collections::{BTreeMap, BTreeSet}; use std::collections::BTreeMap;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::path::Path; use std::path::Path;
@@ -23,17 +23,14 @@ use crate::disk::mount::filesystem::cifs::Cifs;
use crate::disk::mount::util::unmount; use crate::disk::mount::util::unmount;
use crate::hostname::Hostname; use crate::hostname::Hostname;
use crate::net::forward::AvailablePorts; use crate::net::forward::AvailablePorts;
use crate::net::host::Host;
use crate::net::keys::KeyStore; use crate::net::keys::KeyStore;
use crate::net::tor::{OnionAddress, TorSecretKey};
use crate::notifications::Notifications; use crate::notifications::Notifications;
use crate::prelude::*; use crate::prelude::*;
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
use crate::ssh::{SshKeys, SshPubKey}; use crate::ssh::{SshKeys, SshPubKey};
use crate::util::Invoke; use crate::util::Invoke;
use crate::util::crypto::ed25519_expand_key;
use crate::util::serde::Pem; 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! { lazy_static::lazy_static! {
static ref V0_3_6_alpha_0: exver::Version = exver::Version::new( static ref V0_3_6_alpha_0: exver::Version = exver::Version::new(
@@ -146,12 +143,7 @@ pub struct Version;
impl VersionT for Version { impl VersionT for Version {
type Previous = v0_3_5_2::Version; type Previous = v0_3_5_2::Version;
type PreUpRes = ( type PreUpRes = (AccountInfo, SshKeys, CifsTargets);
AccountInfo,
SshKeys,
CifsTargets,
BTreeMap<PackageId, BTreeMap<HostId, TorSecretKey>>,
);
fn semver(self) -> exver::Version { fn semver(self) -> exver::Version {
V0_3_6_alpha_0.clone() V0_3_6_alpha_0.clone()
} }
@@ -166,20 +158,18 @@ impl VersionT for Version {
let cifs = previous_cifs(&pg).await?; let cifs = previous_cifs(&pg).await?;
let tor_keys = previous_tor_keys(&pg).await?;
Command::new("systemctl") Command::new("systemctl")
.arg("stop") .arg("stop")
.arg("postgresql@*.service") .arg("postgresql@*.service")
.invoke(crate::ErrorKind::Database) .invoke(crate::ErrorKind::Database)
.await?; .await?;
Ok((account, ssh_keys, cifs, tor_keys)) Ok((account, ssh_keys, cifs))
} }
fn up( fn up(
self, self,
db: &mut Value, db: &mut Value,
(account, ssh_keys, cifs, tor_keys): Self::PreUpRes, (account, ssh_keys, cifs): Self::PreUpRes,
) -> Result<Value, Error> { ) -> Result<Value, Error> {
let prev_package_data = db["package-data"].clone(); let prev_package_data = db["package-data"].clone();
@@ -242,11 +232,7 @@ impl VersionT for Version {
"ui": db["ui"], "ui": db["ui"],
}); });
let mut keystore = KeyStore::new(&account)?; let 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 private = { let private = {
let mut value = json!({}); let mut value = json!({});
@@ -350,20 +336,6 @@ impl VersionT for Version {
false 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::<OnionAddress>()?,
))
})
})
.collect::<Result<BTreeMap<_, _>, Error>>()?;
if let Err(e) = async { if let Err(e) = async {
let package_s9pk = tokio::fs::File::open(path).await?; let package_s9pk = tokio::fs::File::open(path).await?;
let file = MultiCursorFile::open(&package_s9pk).await?; let file = MultiCursorFile::open(&package_s9pk).await?;
@@ -381,11 +353,8 @@ impl VersionT for Version {
.await? .await?
.await?; .await?;
let to_sync = ctx ctx.db
.db
.mutate(|db| { .mutate(|db| {
let mut to_sync = BTreeSet::new();
let package = db let package = db
.as_public_mut() .as_public_mut()
.as_package_data_mut() .as_package_data_mut()
@@ -396,29 +365,11 @@ impl VersionT for Version {
.as_tasks_mut() .as_tasks_mut()
.remove(&ReplayId::from("needs-config"))?; .remove(&ReplayId::from("needs-config"))?;
} }
for (id, onion) in onions { Ok(())
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)
}) })
.await .await
.result?; .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>(()) Ok::<_, Error>(())
} }
.await .await
@@ -481,33 +432,6 @@ async fn previous_account_info(pg: &sqlx::Pool<sqlx::Postgres>) -> Result<Accoun
password: account_query password: account_query
.try_get("password") .try_get("password")
.with_ctx(|_| (ErrorKind::Database, "password"))?, .with_ctx(|_| (ErrorKind::Database, "password"))?,
tor_keys: vec![TorSecretKey::from_bytes(
if let Some(bytes) = account_query
.try_get::<Option<Vec<u8>>, _>("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::<Vec<u8>, _>("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 server_id: account_query
.try_get("server_id") .try_get("server_id")
.with_ctx(|_| (ErrorKind::Database, "server_id"))?, .with_ctx(|_| (ErrorKind::Database, "server_id"))?,
@@ -579,68 +503,3 @@ async fn previous_ssh_keys(pg: &sqlx::Pool<sqlx::Postgres>) -> Result<SshKeys, E
Ok(ssh_keys) Ok(ssh_keys)
} }
#[tracing::instrument(skip_all)]
async fn previous_tor_keys(
pg: &sqlx::Pool<sqlx::Postgres>,
) -> Result<BTreeMap<PackageId, BTreeMap<HostId, TorSecretKey>>, Error> {
let mut res = BTreeMap::<PackageId, BTreeMap<HostId, TorSecretKey>>::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::<String, _>("package")
.with_ctx(|_| (ErrorKind::Database, "network_keys::package"))?
.parse()?;
let interface_id: HostId = row
.try_get::<String, _>("interface")
.with_ctx(|_| (ErrorKind::Database, "network_keys::interface"))?
.parse()?;
let key = TorSecretKey::from_bytes(ed25519_expand_key(
&<[u8; 32]>::try_from(
row.try_get::<Vec<u8>, _>("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::<String, _>("package")
.with_ctx(|_| (ErrorKind::Database, "tor::package"))?
.parse()?;
let interface_id: HostId = row
.try_get::<String, _>("interface")
.with_ctx(|_| (ErrorKind::Database, "tor::interface"))?
.parse()?;
let key = TorSecretKey::from_bytes(
<[u8; 64]>::try_from(
row.try_get::<Vec<u8>, _>("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)
}

View File

@@ -8,7 +8,6 @@ use super::v0_3_5::V0_3_0_COMPAT;
use super::{VersionT, v0_3_6_alpha_9}; use super::{VersionT, v0_3_6_alpha_9};
use crate::GatewayId; use crate::GatewayId;
use crate::net::host::address::PublicDomainConfig; use crate::net::host::address::PublicDomainConfig;
use crate::net::tor::OnionAddress;
use crate::prelude::*; use crate::prelude::*;
lazy_static::lazy_static! { lazy_static::lazy_static! {
@@ -22,7 +21,7 @@ lazy_static::lazy_static! {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[serde(tag = "kind")] #[serde(tag = "kind")]
enum HostAddress { enum HostAddress {
Onion { address: OnionAddress }, Onion { address: String },
Domain { address: InternedString }, Domain { address: InternedString },
} }

View File

@@ -1,11 +1,7 @@
use std::collections::BTreeSet;
use exver::{PreReleaseSegment, VersionRange}; use exver::{PreReleaseSegment, VersionRange};
use imbl_value::InternedString;
use super::v0_3_5::V0_3_0_COMPAT; use super::v0_3_5::V0_3_0_COMPAT;
use super::{VersionT, v0_4_0_alpha_11}; use super::{VersionT, v0_4_0_alpha_11};
use crate::net::tor::TorSecretKey;
use crate::prelude::*; use crate::prelude::*;
lazy_static::lazy_static! { lazy_static::lazy_static! {
@@ -33,48 +29,6 @@ impl VersionT for Version {
} }
#[instrument(skip_all)] #[instrument(skip_all)]
fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> { fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
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::<TorSecretKey>(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::<BTreeSet<_>>();
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() { if db["private"]["keyStore"]["localCerts"].is_null() {
db["private"]["keyStore"]["localCerts"] = db["private"]["keyStore"]["localCerts"] =
db["private"]["keyStore"]["local_certs"].clone(); db["private"]["keyStore"]["local_certs"].clone();

View File

@@ -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<Self::PreUpRes, Error> {
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<Value, Error> {
// 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"));
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // 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 }

View File

@@ -5,7 +5,6 @@ import type { PublicDomainConfig } from './PublicDomainConfig'
export type Host = { export type Host = {
bindings: { [key: number]: BindInfo } bindings: { [key: number]: BindInfo }
onions: string[]
publicDomains: { [key: string]: PublicDomainConfig } publicDomains: { [key: string]: PublicDomainConfig }
privateDomains: Array<string> privateDomains: Array<string>
/** /**

View File

@@ -1,8 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // 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 { GatewayInfo } from './GatewayInfo'
import type { IpHostname } from './IpHostname' import type { IpHostname } from './IpHostname'
import type { OnionHostname } from './OnionHostname'
export type HostnameInfo = export type HostnameInfo = {
| { kind: 'ip'; gateway: GatewayInfo; public: boolean; hostname: IpHostname } kind: 'ip'
| { kind: 'onion'; hostname: OnionHostname } gateway: GatewayInfo
public: boolean
hostname: IpHostname
}

View File

@@ -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
}

View File

@@ -149,7 +149,6 @@ export { NetInfo } from './NetInfo'
export { NetworkInfo } from './NetworkInfo' export { NetworkInfo } from './NetworkInfo'
export { NetworkInterfaceInfo } from './NetworkInterfaceInfo' export { NetworkInterfaceInfo } from './NetworkInterfaceInfo'
export { NetworkInterfaceType } from './NetworkInterfaceType' export { NetworkInterfaceType } from './NetworkInterfaceType'
export { OnionHostname } from './OnionHostname'
export { OsIndex } from './OsIndex' export { OsIndex } from './OsIndex'
export { OsVersionInfoMap } from './OsVersionInfoMap' export { OsVersionInfoMap } from './OsVersionInfoMap'
export { OsVersionInfo } from './OsVersionInfo' export { OsVersionInfo } from './OsVersionInfo'

View File

@@ -20,7 +20,6 @@ export const getHostname = (url: string): Hostname | null => {
} }
type FilterKinds = type FilterKinds =
| 'onion'
| 'mdns' | 'mdns'
| 'domain' | 'domain'
| 'ip' | 'ip'
@@ -42,27 +41,25 @@ type VisibilityFilter<V extends 'public' | 'private'> = V extends 'public'
| (HostnameInfo & { public: false }) | (HostnameInfo & { public: false })
| VisibilityFilter<Exclude<V, 'private'>> | VisibilityFilter<Exclude<V, 'private'>>
: never : never
type KindFilter<K extends FilterKinds> = K extends 'onion' type KindFilter<K extends FilterKinds> = K extends 'mdns'
? (HostnameInfo & { kind: 'onion' }) | KindFilter<Exclude<K, 'onion'>> ?
: K extends 'mdns' | (HostnameInfo & { kind: 'ip'; hostname: { kind: 'local' } })
| KindFilter<Exclude<K, 'mdns'>>
: K extends 'domain'
? ?
| (HostnameInfo & { kind: 'ip'; hostname: { kind: 'local' } }) | (HostnameInfo & { kind: 'ip'; hostname: { kind: 'domain' } })
| KindFilter<Exclude<K, 'mdns'>> | KindFilter<Exclude<K, 'domain'>>
: K extends 'domain' : K extends 'ipv4'
? ?
| (HostnameInfo & { kind: 'ip'; hostname: { kind: 'domain' } }) | (HostnameInfo & { kind: 'ip'; hostname: { kind: 'ipv4' } })
| KindFilter<Exclude<K, 'domain'>> | KindFilter<Exclude<K, 'ipv4'>>
: K extends 'ipv4' : K extends 'ipv6'
? ?
| (HostnameInfo & { kind: 'ip'; hostname: { kind: 'ipv4' } }) | (HostnameInfo & { kind: 'ip'; hostname: { kind: 'ipv6' } })
| KindFilter<Exclude<K, 'ipv4'>> | KindFilter<Exclude<K, 'ipv6'>>
: K extends 'ipv6' : K extends 'ip'
? ? KindFilter<Exclude<K, 'ip'> | 'ipv4' | 'ipv6'>
| (HostnameInfo & { kind: 'ip'; hostname: { kind: 'ipv6' } }) : never
| KindFilter<Exclude<K, 'ipv6'>>
: K extends 'ip'
? KindFilter<Exclude<K, 'ip'> | 'ipv4' | 'ipv6'>
: never
type FilterReturnTy<F extends Filter> = F extends { type FilterReturnTy<F extends Filter> = F extends {
visibility: infer V extends 'public' | 'private' visibility: infer V extends 'public' | 'private'
@@ -90,10 +87,6 @@ const nonLocalFilter = {
const publicFilter = { const publicFilter = {
visibility: 'public', visibility: 'public',
} as const } as const
const onionFilter = {
kind: 'onion',
} as const
type Formats = 'hostname-info' | 'urlstring' | 'url' type Formats = 'hostname-info' | 'urlstring' | 'url'
type FormatReturnTy< type FormatReturnTy<
F extends Filter, F extends Filter,
@@ -124,7 +117,6 @@ export type Filled<F extends Filter = {}> = {
nonLocal: Filled<typeof nonLocalFilter & Filter> nonLocal: Filled<typeof nonLocalFilter & Filter>
public: Filled<typeof publicFilter & Filter> public: Filled<typeof publicFilter & Filter>
onion: Filled<typeof onionFilter & Filter>
} }
export type FilledAddressInfo = AddressInfo & Filled export type FilledAddressInfo = AddressInfo & Filled
export type ServiceInterfaceFilled = { export type ServiceInterfaceFilled = {
@@ -162,9 +154,7 @@ export const addressHostToUrl = (
scheme in knownProtocols && scheme in knownProtocols &&
port === knownProtocols[scheme as keyof typeof knownProtocols].defaultPort port === knownProtocols[scheme as keyof typeof knownProtocols].defaultPort
let hostname let hostname
if (host.kind === 'onion') { if (host.kind === 'ip') {
hostname = host.hostname.value
} else if (host.kind === 'ip') {
if (host.hostname.kind === 'domain') { if (host.hostname.kind === 'domain') {
hostname = host.hostname.value hostname = host.hostname.value
} else if (host.hostname.kind === 'ipv6') { } else if (host.hostname.kind === 'ipv6') {
@@ -201,13 +191,9 @@ function filterRec(
hostnames = hostnames.filter((h) => invert !== pred(h)) hostnames = hostnames.filter((h) => invert !== pred(h))
} }
if (filter.visibility === 'public') if (filter.visibility === 'public')
hostnames = hostnames.filter( hostnames = hostnames.filter((h) => invert !== h.public)
(h) => invert !== (h.kind === 'onion' || h.public),
)
if (filter.visibility === 'private') if (filter.visibility === 'private')
hostnames = hostnames.filter( hostnames = hostnames.filter((h) => invert !== !h.public)
(h) => invert !== (h.kind !== 'onion' && !h.public),
)
if (filter.kind) { if (filter.kind) {
const kind = new Set( const kind = new Set(
Array.isArray(filter.kind) ? filter.kind : [filter.kind], Array.isArray(filter.kind) ? filter.kind : [filter.kind],
@@ -219,10 +205,7 @@ function filterRec(
hostnames = hostnames.filter( hostnames = hostnames.filter(
(h) => (h) =>
invert !== 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') && (kind.has('domain') &&
h.kind === 'ip' && h.kind === 'ip' &&
h.hostname.kind === 'domain') || h.hostname.kind === 'domain') ||
@@ -266,11 +249,6 @@ export const filledAddress = (
filterRec(hostnames, publicFilter, false), filterRec(hostnames, publicFilter, false),
), ),
) )
const getOnion = once(() =>
filledAddressFromHostnames<typeof onionFilter & F>(
filterRec(hostnames, onionFilter, false),
),
)
return { return {
...addressInfo, ...addressInfo,
hostnames, hostnames,
@@ -294,9 +272,6 @@ export const filledAddress = (
get public(): Filled<typeof publicFilter & F> { get public(): Filled<typeof publicFilter & F> {
return getPublic() return getPublic()
}, },
get onion(): Filled<typeof onionFilter & F> {
return getOnion()
},
} }
} }

View File

@@ -21,11 +21,6 @@ export const localHostname: Pattern = {
description: 'Must be a valid ".local" hostname', 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 = { export const url: Pattern = {
regex: regexes.url.matches(), regex: regexes.url.matches(),
description: 'Must be a valid URL', description: 'Must be a valid URL',
@@ -36,11 +31,6 @@ export const localUrl: Pattern = {
description: 'Must be a valid ".local" URL', 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 = { export const ascii: Pattern = {
regex: regexes.ascii.matches(), regex: regexes.ascii.matches(),
description: description:

View File

@@ -39,10 +39,6 @@ export const localHostname = new ComposableRegex(
/[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local/, /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local/,
) )
export const torHostname = new ComposableRegex(
/[-a-zA-Z0-9@:%._\+~#=]{1,256}\.onion/,
)
// https://ihateregex.io/expr/url/ // https://ihateregex.io/expr/url/
export const url = new ComposableRegex( export const url = new ComposableRegex(
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/, /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()!@:%_\+.~#?&\/\/=]*)/, /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/ // https://ihateregex.io/expr/ascii/
export const ascii = new ComposableRegex(/[ -~]*/) export const ascii = new ComposableRegex(/[ -~]*/)

View File

@@ -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 // https://stackoverflow.com/questions/2970525/converting-any-string-into-camel-case
export function camelCase(value: string) { export function camelCase(value: string) {
return value return value
.replace(/([\(\)\[\]])/g, "") .replace(/([\(\)\[\]])/g, '')
.replace(/^([A-Z])|[\s-_](\w)/g, function (match, p1, p2, offset) { .replace(/^([A-Z])|[\s-_](\w)/g, function (match, p1, p2, offset) {
if (p2) return p2.toUpperCase() if (p2) return p2.toUpperCase()
return p1.toLowerCase() return p1.toLowerCase()
@@ -23,12 +23,12 @@ export async function oldSpecToBuilder(
} }
function isString(x: unknown): x is string { function isString(x: unknown): x is string {
return typeof x === "string" return typeof x === 'string'
} }
export default async function makeFileContentFromOld( export default async function makeFileContentFromOld(
inputData: Promise<any> | any, inputData: Promise<any> | any,
{ StartSdk = "start-sdk", nested = true } = {}, { StartSdk = 'start-sdk', nested = true } = {},
) { ) {
const outputLines: string[] = [] const outputLines: string[] = []
outputLines.push(` outputLines.push(`
@@ -37,16 +37,16 @@ const {InputSpec, List, Value, Variants} = sdk
`) `)
const data = await inputData const data = await inputData
const namedConsts = new Set(["InputSpec", "Value", "List"]) const namedConsts = new Set(['InputSpec', 'Value', 'List'])
const inputSpecName = newConst("inputSpecSpec", convertInputSpec(data)) const inputSpecName = newConst('inputSpecSpec', convertInputSpec(data))
outputLines.push(`export type InputSpecSpec = typeof ${inputSpecName}._TYPE;`) outputLines.push(`export type InputSpecSpec = typeof ${inputSpecName}._TYPE;`)
return outputLines.join("\n") return outputLines.join('\n')
function newConst(key: string, data: string, type?: string) { function newConst(key: string, data: string, type?: string) {
const variableName = getNextConstName(camelCase(key)) const variableName = getNextConstName(camelCase(key))
outputLines.push( outputLines.push(
`export const ${variableName}${!type ? "" : `: ${type}`} = ${data};`, `export const ${variableName}${!type ? '' : `: ${type}`} = ${data};`,
) )
return variableName return variableName
} }
@@ -55,7 +55,7 @@ const {InputSpec, List, Value, Variants} = sdk
return newConst(key, data) return newConst(key, data)
} }
function convertInputSpecInner(data: any) { function convertInputSpecInner(data: any) {
let answer = "{" let answer = '{'
for (const [key, value] of Object.entries(data)) { for (const [key, value] of Object.entries(data)) {
const variableName = maybeNewConst(key, convertValueSpec(value)) const variableName = maybeNewConst(key, convertValueSpec(value))
@@ -69,7 +69,7 @@ const {InputSpec, List, Value, Variants} = sdk
} }
function convertValueSpec(value: any): string { function convertValueSpec(value: any): string {
switch (value.type) { switch (value.type) {
case "string": { case 'string': {
if (value.textarea) { if (value.textarea) {
return `${rangeToTodoComment( return `${rangeToTodoComment(
value?.range, value?.range,
@@ -99,12 +99,12 @@ const {InputSpec, List, Value, Variants} = sdk
warning: value.warning || null, warning: value.warning || null,
masked: value.masked || false, masked: value.masked || false,
placeholder: value.placeholder || null, placeholder: value.placeholder || null,
inputmode: "text", inputmode: 'text',
patterns: value.pattern patterns: value.pattern
? [ ? [
{ {
regex: value.pattern, regex: value.pattern,
description: value["pattern-description"], description: value['pattern-description'],
}, },
] ]
: [], : [],
@@ -115,7 +115,7 @@ const {InputSpec, List, Value, Variants} = sdk
2, 2,
)})` )})`
} }
case "number": { case 'number': {
return `${rangeToTodoComment( return `${rangeToTodoComment(
value?.range, value?.range,
)}Value.number(${JSON.stringify( )}Value.number(${JSON.stringify(
@@ -136,7 +136,7 @@ const {InputSpec, List, Value, Variants} = sdk
2, 2,
)})` )})`
} }
case "boolean": { case 'boolean': {
return `Value.toggle(${JSON.stringify( return `Value.toggle(${JSON.stringify(
{ {
name: value.name || null, name: value.name || null,
@@ -148,15 +148,15 @@ const {InputSpec, List, Value, Variants} = sdk
2, 2,
)})` )})`
} }
case "enum": { case 'enum': {
const allValueNames = new Set([ const allValueNames = new Set([
...(value?.["values"] || []), ...(value?.['values'] || []),
...Object.keys(value?.["value-names"] || {}), ...Object.keys(value?.['value-names'] || {}),
]) ])
const values = Object.fromEntries( const values = Object.fromEntries(
Array.from(allValueNames) Array.from(allValueNames)
.filter(isString) .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( return `Value.select(${JSON.stringify(
{ {
@@ -170,9 +170,9 @@ const {InputSpec, List, Value, Variants} = sdk
2, 2,
)} as const)` )} as const)`
} }
case "object": { case 'object': {
const specName = maybeNewConst( const specName = maybeNewConst(
value.name + "_spec", value.name + '_spec',
convertInputSpec(value.spec), convertInputSpec(value.spec),
) )
return `Value.object({ return `Value.object({
@@ -180,10 +180,10 @@ const {InputSpec, List, Value, Variants} = sdk
description: ${JSON.stringify(value.description || null)}, description: ${JSON.stringify(value.description || null)},
}, ${specName})` }, ${specName})`
} }
case "union": { case 'union': {
const variants = maybeNewConst( const variants = maybeNewConst(
value.name + "_variants", value.name + '_variants',
convertVariants(value.variants, value.tag["variant-names"] || {}), convertVariants(value.variants, value.tag['variant-names'] || {}),
) )
return `Value.union({ return `Value.union({
@@ -194,18 +194,18 @@ const {InputSpec, List, Value, Variants} = sdk
variants: ${variants}, variants: ${variants},
})` })`
} }
case "list": { case 'list': {
if (value.subtype === "enum") { if (value.subtype === 'enum') {
const allValueNames = new Set([ const allValueNames = new Set([
...(value?.spec?.["values"] || []), ...(value?.spec?.['values'] || []),
...Object.keys(value?.spec?.["value-names"] || {}), ...Object.keys(value?.spec?.['value-names'] || {}),
]) ])
const values = Object.fromEntries( const values = Object.fromEntries(
Array.from(allValueNames) Array.from(allValueNames)
.filter(isString) .filter(isString)
.map((key: string) => [ .map((key: string) => [
key, key,
value?.spec?.["value-names"]?.[key] ?? key, value?.spec?.['value-names']?.[key] ?? key,
]), ]),
) )
return `Value.multiselect(${JSON.stringify( return `Value.multiselect(${JSON.stringify(
@@ -222,10 +222,10 @@ const {InputSpec, List, Value, Variants} = sdk
2, 2,
)})` )})`
} }
const list = maybeNewConst(value.name + "_list", convertList(value)) const list = maybeNewConst(value.name + '_list', convertList(value))
return `Value.list(${list})` return `Value.list(${list})`
} }
case "pointer": { case 'pointer': {
return `/* TODO deal with point removed point "${value.name}" */null as any` 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) { function convertList(value: any) {
switch (value.subtype) { switch (value.subtype) {
case "string": { case 'string': {
return `${rangeToTodoComment(value?.range)}List.text(${JSON.stringify( return `${rangeToTodoComment(value?.range)}List.text(${JSON.stringify(
{ {
name: value.name || null, name: value.name || null,
@@ -253,7 +253,7 @@ const {InputSpec, List, Value, Variants} = sdk
? [ ? [
{ {
regex: value.spec.pattern, 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, // placeholder: value?.spec?.placeholder || null,
// })})` // })})`
// } // }
case "enum": { case 'enum': {
return "/* error!! list.enum */" return '/* error!! list.enum */'
} }
case "object": { case 'object': {
const specName = maybeNewConst( const specName = maybeNewConst(
value.name + "_spec", value.name + '_spec',
convertInputSpec(value.spec.spec), convertInputSpec(value.spec.spec),
) )
return `${rangeToTodoComment(value?.range)}List.obj({ return `${rangeToTodoComment(value?.range)}List.obj({
@@ -297,20 +297,20 @@ const {InputSpec, List, Value, Variants} = sdk
description: ${JSON.stringify(value.description || null)}, description: ${JSON.stringify(value.description || null)},
}, { }, {
spec: ${specName}, spec: ${specName},
displayAs: ${JSON.stringify(value?.spec?.["display-as"] || null)}, displayAs: ${JSON.stringify(value?.spec?.['display-as'] || null)},
uniqueBy: ${JSON.stringify(value?.spec?.["unique-by"] || null)}, uniqueBy: ${JSON.stringify(value?.spec?.['unique-by'] || null)},
})` })`
} }
case "union": { case 'union': {
const variants = maybeNewConst( const variants = maybeNewConst(
value.name + "_variants", value.name + '_variants',
convertVariants( convertVariants(
value.spec.variants, value.spec.variants,
value.spec["variant-names"] || {}, value.spec['variant-names'] || {},
), ),
) )
const unionValueName = maybeNewConst( const unionValueName = maybeNewConst(
value.name + "_union", value.name + '_union',
`${rangeToTodoComment(value?.range)} `${rangeToTodoComment(value?.range)}
Value.union({ Value.union({
name: ${JSON.stringify(value?.spec?.tag?.name || null)}, name: ${JSON.stringify(value?.spec?.tag?.name || null)},
@@ -324,7 +324,7 @@ const {InputSpec, List, Value, Variants} = sdk
`, `,
) )
const listInputSpec = maybeNewConst( const listInputSpec = maybeNewConst(
value.name + "_list_inputSpec", value.name + '_list_inputSpec',
` `
InputSpec.of({ InputSpec.of({
"union": ${unionValueName} "union": ${unionValueName}
@@ -340,8 +340,8 @@ const {InputSpec, List, Value, Variants} = sdk
warning: ${JSON.stringify(value.warning || null)}, warning: ${JSON.stringify(value.warning || null)},
}, { }, {
spec: ${listInputSpec}, spec: ${listInputSpec},
displayAs: ${JSON.stringify(value?.spec?.["display-as"] || null)}, displayAs: ${JSON.stringify(value?.spec?.['display-as'] || null)},
uniqueBy: ${JSON.stringify(value?.spec?.["unique-by"] || null)}, uniqueBy: ${JSON.stringify(value?.spec?.['unique-by'] || null)},
})` })`
} }
} }
@@ -352,7 +352,7 @@ const {InputSpec, List, Value, Variants} = sdk
variants: Record<string, unknown>, variants: Record<string, unknown>,
variantNames: Record<string, string>, variantNames: Record<string, string>,
): string { ): string {
let answer = "Variants.of({" let answer = 'Variants.of({'
for (const [key, value] of Object.entries(variants)) { for (const [key, value] of Object.entries(variants)) {
const variantSpec = maybeNewConst(key, convertInputSpec(value)) const variantSpec = maybeNewConst(key, convertInputSpec(value))
answer += `"${key}": {name: "${ answer += `"${key}": {name: "${
@@ -373,7 +373,7 @@ const {InputSpec, List, Value, Variants} = sdk
} }
function rangeToTodoComment(range: string | undefined) { function rangeToTodoComment(range: string | undefined) {
if (!range) return "" if (!range) return ''
return `/* TODO: Convert range for this value (${range})*/` return `/* TODO: Convert range for this value (${range})*/`
} }

View File

@@ -1,5 +1,4 @@
export type AccessType = export type AccessType =
| 'tor'
| 'mdns' | 'mdns'
| 'localhost' | 'localhost'
| 'ipv4' | 'ipv4'

View File

@@ -2,7 +2,6 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core'
import { tuiButtonOptionsProvider } from '@taiga-ui/core' import { tuiButtonOptionsProvider } from '@taiga-ui/core'
import { MappedServiceInterface } from './interface.service' import { MappedServiceInterface } from './interface.service'
import { InterfaceGatewaysComponent } from './gateways.component' import { InterfaceGatewaysComponent } from './gateways.component'
import { InterfaceTorDomainsComponent } from './tor-domains.component'
import { PublicDomainsComponent } from './public-domains/pd.component' import { PublicDomainsComponent } from './public-domains/pd.component'
import { InterfacePrivateDomainsComponent } from './private-domains.component' import { InterfacePrivateDomainsComponent } from './private-domains.component'
import { InterfaceAddressesComponent } from './addresses/addresses.component' import { InterfaceAddressesComponent } from './addresses/addresses.component'
@@ -16,7 +15,6 @@ import { InterfaceAddressesComponent } from './addresses/addresses.component'
[publicDomains]="value()?.publicDomains" [publicDomains]="value()?.publicDomains"
[addSsl]="value()?.addSsl || false" [addSsl]="value()?.addSsl || false"
></section> ></section>
<section [torDomains]="value()?.torDomains"></section>
<section [privateDomains]="value()?.privateDomains"></section> <section [privateDomains]="value()?.privateDomains"></section>
</div> </div>
<hr [style.width.rem]="10" /> <hr [style.width.rem]="10" />
@@ -52,7 +50,6 @@ import { InterfaceAddressesComponent } from './addresses/addresses.component'
providers: [tuiButtonOptionsProvider({ size: 'xs' })], providers: [tuiButtonOptionsProvider({ size: 'xs' })],
imports: [ imports: [
InterfaceGatewaysComponent, InterfaceGatewaysComponent,
InterfaceTorDomainsComponent,
PublicDomainsComponent, PublicDomainsComponent,
InterfacePrivateDomainsComponent, InterfacePrivateDomainsComponent,
InterfaceAddressesComponent, InterfaceAddressesComponent,

View File

@@ -27,14 +27,6 @@ function cmpWithRankedPredicates<T extends AddressWithInfo>(
return 0 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 } } type LanAddress = AddressWithInfo & { info: { kind: 'ip'; public: false } }
function filterLan(a: AddressWithInfo): a is LanAddress { function filterLan(a: AddressWithInfo): a is LanAddress {
return a.info.kind === 'ip' && !a.info.public 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 const lanAddrs = allAddressesWithInfo
.filter(filterLan) .filter(filterLan)
.sort((a, b) => cmpLan(host, a, b)) .sort((a, b) => cmpLan(host, a, b))
@@ -188,7 +179,6 @@ export class InterfaceService {
clearnetAddrs[0], clearnetAddrs[0],
lanAddrs[0], lanAddrs[0],
vpnAddrs[0], vpnAddrs[0],
torAddrs[0],
] ]
.filter(a => !!a) .filter(a => !!a)
.reduce((acc, x) => { .reduce((acc, x) => {
@@ -214,9 +204,8 @@ export class InterfaceService {
kind: 'domain', kind: 'domain',
visibility: 'public', visibility: 'public',
}) })
const tor = addresses.filter({ kind: 'onion' })
const wanIp = addresses.filter({ kind: 'ipv4', visibility: 'public' }) const wanIp = addresses.filter({ kind: 'ipv4', visibility: 'public' })
const bestPublic = [publicDomains, tor, wanIp].flatMap(h => const bestPublic = [publicDomains, wanIp].flatMap(h =>
h.format('urlstring'), h.format('urlstring'),
)[0] )[0]
const privateDomains = addresses.filter({ const privateDomains = addresses.filter({
@@ -254,9 +243,6 @@ export class InterfaceService {
.format('urlstring')[0] .format('urlstring')[0]
onLan = true onLan = true
break break
case 'tor':
matching = tor.format('urlstring')[0]
break
case 'mdns': case 'mdns':
matching = mdns.format('urlstring')[0] matching = mdns.format('urlstring')[0]
onLan = true onLan = true
@@ -302,31 +288,7 @@ export class InterfaceService {
"Requires trusting your server's Root CA", "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 const port = info.hostname.sslPort || info.hostname.port
gatewayName = info.gateway.name gatewayName = info.gateway.name
@@ -479,7 +441,6 @@ export class InterfaceService {
export type MappedServiceInterface = T.ServiceInterface & { export type MappedServiceInterface = T.ServiceInterface & {
gateways: InterfaceGateway[] gateways: InterfaceGateway[]
torDomains: string[]
publicDomains: PublicDomain[] publicDomains: PublicDomain[]
privateDomains: string[] privateDomains: string[]
addresses: { addresses: {

View File

@@ -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: `
<header>
Tor Domains
<a
tuiIconButton
docsLink
path="/user-manual/connecting-remotely/tor.html"
appearance="icon"
iconStart="@tui.external-link"
>
{{ 'Documentation' | i18n }}
</a>
<button
tuiButton
iconStart="@tui.plus"
[style.margin-inline-start]="'auto'"
(click)="add()"
[disabled]="!torDomains()"
>
{{ 'Add' | i18n }}
</button>
</header>
@for (domain of torDomains(); track domain) {
<div tuiCell="s">
<span tuiTitle>{{ domain }}</span>
<button
tuiIconButton
iconStart="@tui.trash"
appearance="action-destructive"
(click)="remove(domain)"
>
{{ 'Delete' | i18n }}
</button>
</div>
} @empty {
@if (torDomains()) {
<app-placeholder icon="@tui.target">
{{ 'No Tor domains' | i18n }}
</app-placeholder>
} @else {
@for (_ of [0, 1]; track $index) {
<label tuiCell="s">
<span tuiTitle [tuiSkeleton]="true">{{ 'Loading' | i18n }}</span>
</label>
}
}
}
`,
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<readonly string[] | undefined>()
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<FormContext<OnionForm>>(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<boolean> {
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()
}
}
}

View File

@@ -13,10 +13,6 @@ export const ROUTES: Routes = [
path: 'os', path: 'os',
loadComponent: () => import('./routes/os.component'), loadComponent: () => import('./routes/os.component'),
}, },
{
path: 'tor',
loadComponent: () => import('./routes/tor.component'),
},
] ]
export default ROUTES export default ROUTES

View File

@@ -79,12 +79,6 @@ export default class SystemLogsComponent {
subtitle: 'Raw, unfiltered operating system logs', subtitle: 'Raw, unfiltered operating system logs',
icon: '@tui.square-dashed-bottom-code', 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', link: 'kernel',
title: 'Kernel Logs', title: 'Kernel Logs',

View File

@@ -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: `
<logs-header [title]="'Tor Logs' | i18n">
{{ 'Diagnostics for the Tor daemon on this server' | i18n }}
</logs-header>
<logs context="tor" [followLogs]="follow" [fetchLogs]="fetch" />
`,
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)
}

View File

@@ -139,7 +139,6 @@ export default class ServiceInterfaceRoute {
: !binding?.net.privateDisabled.includes(g.id)) ?? false, : !binding?.net.privateDisabled.includes(g.id)) ?? false,
...g, ...g,
})) || [], })) || [],
torDomains: host.onions,
publicDomains: getPublicDomains(host.publicDomains, gateways), publicDomains: getPublicDomains(host.publicDomains, gateways),
privateDomains: host.privateDomains, privateDomains: host.privateDomains,
addSsl: !!binding?.options.addSsl, addSsl: !!binding?.options.addSsl,

View File

@@ -13,7 +13,6 @@ import {
TuiFiles, TuiFiles,
tuiInputFilesOptionsProvider, tuiInputFilesOptionsProvider,
} from '@taiga-ui/kit' } from '@taiga-ui/kit'
import { ConfigService } from 'src/app/services/config.service'
import { TitleDirective } from 'src/app/services/title.service' import { TitleDirective } from 'src/app/services/title.service'
import { SideloadPackageComponent } from './package.component' import { SideloadPackageComponent } from './package.component'
@@ -55,11 +54,6 @@ import { MarketplacePkgSideload, validateS9pk } from './sideload.utils'
<div> <div>
<tui-avatar appearance="secondary" src="@tui.upload" /> <tui-avatar appearance="secondary" src="@tui.upload" />
<p>{{ 'Upload .s9pk package file' | i18n }}</p> <p>{{ 'Upload .s9pk package file' | i18n }}</p>
@if (isTor) {
<p class="g-warning">
{{ 'Warning: package upload will be slow over Tor.' | i18n }}
</p>
}
<button tuiButton>{{ 'Select' | i18n }}</button> <button tuiButton>{{ 'Select' | i18n }}</button>
</div> </div>
} }
@@ -92,8 +86,6 @@ import { MarketplacePkgSideload, validateS9pk } from './sideload.utils'
], ],
}) })
export default class SideloadComponent { export default class SideloadComponent {
readonly isTor = inject(ConfigService).accessType === 'tor'
file: File | null = null file: File | null = null
readonly package = signal<MarketplacePkgSideload | null>(null) readonly package = signal<MarketplacePkgSideload | null>(null)
readonly error = signal<i18nKey | null>(null) readonly error = signal<i18nKey | null>(null)

View File

@@ -47,13 +47,11 @@ import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { filter } from 'rxjs' import { filter } from 'rxjs'
import { ApiService } from 'src/app/services/api/embassy-api.service' 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 { OSService } from 'src/app/services/os.service'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
import { TitleDirective } from 'src/app/services/title.service' import { TitleDirective } from 'src/app/services/title.service'
import { SnekDirective } from './snek.directive' import { SnekDirective } from './snek.directive'
import { UPDATE } from './update.component' import { UPDATE } from './update.component'
import { SystemWipeComponent } from './wipe.component'
import { KeyboardSelectComponent } from './keyboard-select.component' import { KeyboardSelectComponent } from './keyboard-select.component'
@Component({ @Component({
@@ -66,7 +64,7 @@ import { KeyboardSelectComponent } from './keyboard-select.component'
</ng-container> </ng-container>
@if (server(); as server) { @if (server(); as server) {
<div tuiCell tuiAppearance="outline-grayscale"> <div tuiCell tuiAppearance="outline-grayscale">
<tui-icon icon="@tui.zap" /> <tui-icon icon="@tui.zap" (click)="count = count + 1" />
<span tuiTitle> <span tuiTitle>
<strong>{{ 'Software Update' | i18n }}</strong> <strong>{{ 'Software Update' | i18n }}</strong>
<span tuiSubtitle [style.flex-wrap]="'wrap'"> <span tuiSubtitle [style.flex-wrap]="'wrap'">
@@ -178,18 +176,6 @@ import { KeyboardSelectComponent } from './keyboard-select.component'
</button> </button>
} }
</div> </div>
<div tuiCell tuiAppearance="outline-grayscale">
<tui-icon icon="@tui.rotate-cw" (click)="count = count + 1" />
<span tuiTitle>
<strong>{{ 'Restart Tor' | i18n }}</strong>
<span tuiSubtitle>
{{ 'Restart the Tor daemon on your server' | i18n }}
</span>
</span>
<button tuiButton appearance="glass" (click)="onTorRestart()">
{{ 'Restart' | i18n }}
</button>
</div>
@if (count > 4) { @if (count > 4) {
<div tuiCell tuiAppearance="outline-grayscale" tuiAnimated> <div tuiCell tuiAppearance="outline-grayscale" tuiAnimated>
<tui-icon icon="@tui.briefcase-medical" /> <tui-icon icon="@tui.briefcase-medical" />
@@ -283,12 +269,10 @@ export default class SystemGeneralComponent {
private readonly errorService = inject(ErrorService) private readonly errorService = inject(ErrorService)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB) private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
private readonly api = inject(ApiService) private readonly api = inject(ApiService)
private readonly isTor = inject(ConfigService).accessType === 'tor'
private readonly dialog = inject(DialogService) private readonly dialog = inject(DialogService)
private readonly i18n = inject(i18nPipe) private readonly i18n = inject(i18nPipe)
private readonly injector = inject(INJECTOR) private readonly injector = inject(INJECTOR)
wipe = false
count = 0 count = 0
readonly server = toSignal(this.patch.watch$('serverInfo')) 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() { async onRepair() {
this.dialog this.dialog
.openConfirm({ .openConfirm({
@@ -532,19 +498,6 @@ export default class SystemGeneralComponent {
.subscribe(() => this.restart()) .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() { private update() {
this.dialogs this.dialogs
.open(UPDATE, { .open(UPDATE, {

View File

@@ -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) {
<p>
{{
'You are currently connected over Tor. If you restart the Tor daemon, you will lose connectivity until it comes back online.'
| i18n
}}
</p>
}
<p>
{{
'Optionally wipe state to forcibly acquire new guard nodes. It is recommended to try without wiping state first.'
| i18n
}}
</p>
<label tuiLabel>
<input type="checkbox" tuiCheckbox [(ngModel)]="component.wipe" />
{{ 'Wipe state' | i18n }}
</label>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiLabel, FormsModule, TuiCheckbox, i18nPipe],
})
export class SystemWipeComponent {
readonly isTor = inject(ConfigService).accessType === 'tor'
readonly component = inject(SystemGeneralComponent)
}

View File

@@ -100,7 +100,6 @@ export default class StartOsUiComponent {
: !binding?.net.privateDisabled.includes(g.id)) ?? false, : !binding?.net.privateDisabled.includes(g.id)) ?? false,
...g, ...g,
})), })),
torDomains: network.host.onions,
publicDomains: getPublicDomains(network.host.publicDomains, gateways), publicDomains: getPublicDomains(network.host.publicDomains, gateways),
privateDomains: network.host.privateDomains, privateDomains: network.host.privateDomains,
addSsl: true, addSsl: true,

View File

@@ -2138,7 +2138,6 @@ export namespace Mock {
}, },
publicDomains: {}, publicDomains: {},
privateDomains: [], privateDomains: [],
onions: [],
hostnameInfo: { hostnameInfo: {
80: [ 80: [
{ {
@@ -2209,14 +2208,6 @@ export namespace Mock {
sslPort: 1234, sslPort: 1234,
}, },
}, },
{
kind: 'onion',
hostname: {
value: 'bitcoin-p2p.onion',
port: 80,
sslPort: 443,
},
},
], ],
}, },
}, },
@@ -2239,7 +2230,6 @@ export namespace Mock {
}, },
publicDomains: {}, publicDomains: {},
privateDomains: [], privateDomains: [],
onions: [],
hostnameInfo: { hostnameInfo: {
8332: [], 8332: [],
}, },
@@ -2263,7 +2253,6 @@ export namespace Mock {
}, },
publicDomains: {}, publicDomains: {},
privateDomains: [], privateDomains: [],
onions: [],
hostnameInfo: { hostnameInfo: {
8333: [], 8333: [],
}, },

View File

@@ -79,14 +79,14 @@ export namespace RR {
uptime: number // seconds 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 GetServerLogsRes = FetchLogsRes
export type FollowServerLogsReq = { export type FollowServerLogsReq = {
limit?: number // (optional) default is 50. Ignored if cursor provided 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 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 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 = { export type FollowServerLogsRes = {
startCursor: string startCursor: string
guid: string guid: string
@@ -120,12 +120,6 @@ export namespace RR {
} // net.dns.query } // net.dns.query
export type QueryDnsRes = string | null 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 SetKeyboardReq = FullKeyboard // server.set-keyboard
export type SetKeyboardRes = null export type SetKeyboardRes = null
@@ -287,13 +281,6 @@ export namespace RR {
} }
export type RemoveAcmeRes = null 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 = { export type ServerBindingToggleGatewayReq = {
// server.host.binding.set-gateway-enabled // server.host.binding.set-gateway-enabled
gateway: T.GatewayId gateway: T.GatewayId
@@ -302,15 +289,6 @@ export namespace RR {
} }
export type ServerBindingToggleGatewayRes = null 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 = { export type OsUiAddPublicDomainReq = {
// server.host.address.domain.public.add // server.host.address.domain.public.add
fqdn: string // FQDN fqdn: string // FQDN
@@ -348,13 +326,6 @@ export namespace RR {
} }
export type PkgBindingToggleGatewayRes = null 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 & { export type PkgAddPublicDomainReq = OsUiAddPublicDomainReq & {
// package.host.address.domain.public.add // package.host.address.domain.public.add
package: T.PackageId // string package: T.PackageId // string

View File

@@ -81,8 +81,6 @@ export abstract class ApiService {
params: RR.GetServerLogsReq, params: RR.GetServerLogsReq,
): Promise<RR.GetServerLogsRes> ): Promise<RR.GetServerLogsRes>
abstract getTorLogs(params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes>
abstract getKernelLogs( abstract getKernelLogs(
params: RR.GetServerLogsReq, params: RR.GetServerLogsReq,
): Promise<RR.GetServerLogsRes> ): Promise<RR.GetServerLogsRes>
@@ -91,10 +89,6 @@ export abstract class ApiService {
params: RR.FollowServerLogsReq, params: RR.FollowServerLogsReq,
): Promise<RR.FollowServerLogsRes> ): Promise<RR.FollowServerLogsRes>
abstract followTorLogs(
params: RR.FollowServerLogsReq,
): Promise<RR.FollowServerLogsRes>
abstract followKernelLogs( abstract followKernelLogs(
params: RR.FollowServerLogsReq, params: RR.FollowServerLogsReq,
): Promise<RR.FollowServerLogsRes> ): Promise<RR.FollowServerLogsRes>
@@ -125,8 +119,6 @@ export abstract class ApiService {
abstract queryDns(params: RR.QueryDnsReq): Promise<RR.QueryDnsRes> abstract queryDns(params: RR.QueryDnsReq): Promise<RR.QueryDnsRes>
abstract resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes>
// smtp // smtp
abstract setSmtp(params: RR.SetSMTPReq): Promise<RR.SetSMTPRes> abstract setSmtp(params: RR.SetSMTPReq): Promise<RR.SetSMTPRes>
@@ -344,22 +336,10 @@ export abstract class ApiService {
abstract removeAcme(params: RR.RemoveAcmeReq): Promise<RR.RemoveAcmeRes> abstract removeAcme(params: RR.RemoveAcmeReq): Promise<RR.RemoveAcmeRes>
abstract addTorKey(params: RR.AddTorKeyReq): Promise<RR.AddTorKeyRes>
abstract generateTorKey(
params: RR.GenerateTorKeyReq,
): Promise<RR.AddTorKeyRes>
abstract serverBindingToggleGateway( abstract serverBindingToggleGateway(
params: RR.ServerBindingToggleGatewayReq, params: RR.ServerBindingToggleGatewayReq,
): Promise<RR.ServerBindingToggleGatewayRes> ): Promise<RR.ServerBindingToggleGatewayRes>
abstract serverAddOnion(params: RR.ServerAddOnionReq): Promise<RR.AddOnionRes>
abstract serverRemoveOnion(
params: RR.ServerRemoveOnionReq,
): Promise<RR.RemoveOnionRes>
abstract osUiAddPublicDomain( abstract osUiAddPublicDomain(
params: RR.OsUiAddPublicDomainReq, params: RR.OsUiAddPublicDomainReq,
): Promise<RR.OsUiAddPublicDomainRes> ): Promise<RR.OsUiAddPublicDomainRes>
@@ -380,12 +360,6 @@ export abstract class ApiService {
params: RR.PkgBindingToggleGatewayReq, params: RR.PkgBindingToggleGatewayReq,
): Promise<RR.PkgBindingToggleGatewayRes> ): Promise<RR.PkgBindingToggleGatewayRes>
abstract pkgAddOnion(params: RR.PkgAddOnionReq): Promise<RR.AddOnionRes>
abstract pkgRemoveOnion(
params: RR.PkgRemoveOnionReq,
): Promise<RR.RemoveOnionRes>
abstract pkgAddPublicDomain( abstract pkgAddPublicDomain(
params: RR.PkgAddPublicDomainReq, params: RR.PkgAddPublicDomainReq,
): Promise<RR.PkgAddPublicDomainRes> ): Promise<RR.PkgAddPublicDomainRes>

View File

@@ -195,10 +195,6 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'server.logs', params }) return this.rpcRequest({ method: 'server.logs', params })
} }
async getTorLogs(params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes> {
return this.rpcRequest({ method: 'net.tor.logs', params })
}
async getKernelLogs( async getKernelLogs(
params: RR.GetServerLogsReq, params: RR.GetServerLogsReq,
): Promise<RR.GetServerLogsRes> { ): Promise<RR.GetServerLogsRes> {
@@ -211,12 +207,6 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'server.logs.follow', params }) return this.rpcRequest({ method: 'server.logs.follow', params })
} }
async followTorLogs(
params: RR.FollowServerLogsReq,
): Promise<RR.FollowServerLogsRes> {
return this.rpcRequest({ method: 'net.tor.logs.follow', params })
}
async followKernelLogs( async followKernelLogs(
params: RR.FollowServerLogsReq, params: RR.FollowServerLogsReq,
): Promise<RR.FollowServerLogsRes> { ): Promise<RR.FollowServerLogsRes> {
@@ -278,10 +268,6 @@ export class LiveApiService extends ApiService {
}) })
} }
async resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes> {
return this.rpcRequest({ method: 'net.tor.reset', params })
}
// marketplace URLs // marketplace URLs
async checkOSUpdate( async checkOSUpdate(
@@ -621,20 +607,6 @@ export class LiveApiService extends ApiService {
}) })
} }
async addTorKey(params: RR.AddTorKeyReq): Promise<RR.AddTorKeyRes> {
return this.rpcRequest({
method: 'net.tor.key.add',
params,
})
}
async generateTorKey(params: RR.GenerateTorKeyReq): Promise<RR.AddTorKeyRes> {
return this.rpcRequest({
method: 'net.tor.key.generate',
params,
})
}
async serverBindingToggleGateway( async serverBindingToggleGateway(
params: RR.ServerBindingToggleGatewayReq, params: RR.ServerBindingToggleGatewayReq,
): Promise<RR.ServerBindingToggleGatewayRes> { ): Promise<RR.ServerBindingToggleGatewayRes> {
@@ -644,22 +616,6 @@ export class LiveApiService extends ApiService {
}) })
} }
async serverAddOnion(params: RR.ServerAddOnionReq): Promise<RR.AddOnionRes> {
return this.rpcRequest({
method: 'server.host.address.onion.add',
params,
})
}
async serverRemoveOnion(
params: RR.ServerRemoveOnionReq,
): Promise<RR.RemoveOnionRes> {
return this.rpcRequest({
method: 'server.host.address.onion.remove',
params,
})
}
async osUiAddPublicDomain( async osUiAddPublicDomain(
params: RR.OsUiAddPublicDomainReq, params: RR.OsUiAddPublicDomainReq,
): Promise<RR.OsUiAddPublicDomainRes> { ): Promise<RR.OsUiAddPublicDomainRes> {
@@ -705,22 +661,6 @@ export class LiveApiService extends ApiService {
}) })
} }
async pkgAddOnion(params: RR.PkgAddOnionReq): Promise<RR.AddOnionRes> {
return this.rpcRequest({
method: 'package.host.address.onion.add',
params,
})
}
async pkgRemoveOnion(
params: RR.PkgRemoveOnionReq,
): Promise<RR.RemoveOnionRes> {
return this.rpcRequest({
method: 'package.host.address.onion.remove',
params,
})
}
async pkgAddPublicDomain( async pkgAddPublicDomain(
params: RR.PkgAddPublicDomainReq, params: RR.PkgAddPublicDomainReq,
): Promise<RR.PkgAddPublicDomainRes> { ): Promise<RR.PkgAddPublicDomainRes> {

View File

@@ -281,17 +281,6 @@ export class MockApiService extends ApiService {
} }
} }
async getTorLogs(params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes> {
await pauseFor(2000)
const entries = this.randomLogs(params.limit)
return {
entries,
startCursor: 'start-cursor',
endCursor: 'end-cursor',
}
}
async getKernelLogs( async getKernelLogs(
params: RR.GetServerLogsReq, params: RR.GetServerLogsReq,
): Promise<RR.GetServerLogsRes> { ): Promise<RR.GetServerLogsRes> {
@@ -315,16 +304,6 @@ export class MockApiService extends ApiService {
} }
} }
async followTorLogs(
params: RR.FollowServerLogsReq,
): Promise<RR.FollowServerLogsRes> {
await pauseFor(2000)
return {
startCursor: 'start-cursor',
guid: 'logs-guid',
}
}
async followKernelLogs( async followKernelLogs(
params: RR.FollowServerLogsReq, params: RR.FollowServerLogsReq,
): Promise<RR.FollowServerLogsRes> { ): Promise<RR.FollowServerLogsRes> {
@@ -504,11 +483,6 @@ export class MockApiService extends ApiService {
return null return null
} }
async resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes> {
await pauseFor(2000)
return null
}
// marketplace URLs // marketplace URLs
async checkOSUpdate( async checkOSUpdate(
@@ -1374,16 +1348,6 @@ export class MockApiService extends ApiService {
return null return null
} }
async addTorKey(params: RR.AddTorKeyReq): Promise<RR.AddTorKeyRes> {
await pauseFor(2000)
return 'vanityabcdefghijklmnop.onion'
}
async generateTorKey(params: RR.GenerateTorKeyReq): Promise<RR.AddTorKeyRes> {
await pauseFor(2000)
return 'abcdefghijklmnopqrstuv.onion'
}
async serverBindingToggleGateway( async serverBindingToggleGateway(
params: RR.ServerBindingToggleGatewayReq, params: RR.ServerBindingToggleGatewayReq,
): Promise<RR.ServerBindingToggleGatewayRes> { ): Promise<RR.ServerBindingToggleGatewayRes> {
@@ -1401,53 +1365,6 @@ export class MockApiService extends ApiService {
return null return null
} }
async serverAddOnion(params: RR.ServerAddOnionReq): Promise<RR.AddOnionRes> {
await pauseFor(2000)
const patch: Operation<any>[] = [
{
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<RR.RemoveOnionRes> {
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( async osUiAddPublicDomain(
params: RR.OsUiAddPublicDomainReq, params: RR.OsUiAddPublicDomainReq,
): Promise<RR.OsUiAddPublicDomainRes> { ): Promise<RR.OsUiAddPublicDomainRes> {
@@ -1574,53 +1491,6 @@ export class MockApiService extends ApiService {
return null return null
} }
async pkgAddOnion(params: RR.PkgAddOnionReq): Promise<RR.AddOnionRes> {
await pauseFor(2000)
const patch: Operation<any>[] = [
{
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<RR.RemoveOnionRes> {
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( async pkgAddPublicDomain(
params: RR.PkgAddPublicDomainReq, params: RR.PkgAddPublicDomainReq,
): Promise<RR.PkgAddPublicDomainRes> { ): Promise<RR.PkgAddPublicDomainRes> {

View File

@@ -54,7 +54,6 @@ export const mockPatchData: DataModel = {
}, },
publicDomains: {}, publicDomains: {},
privateDomains: [], privateDomains: [],
onions: ['myveryownspecialtoraddress'],
hostnameInfo: { hostnameInfo: {
80: [ 80: [
{ {
@@ -125,14 +124,6 @@ export const mockPatchData: DataModel = {
sslPort: 443, sslPort: 443,
}, },
}, },
{
kind: 'onion',
hostname: {
value: 'myveryownspecialtoraddress.onion',
port: 80,
sslPort: 443,
},
},
], ],
}, },
}, },
@@ -524,7 +515,6 @@ export const mockPatchData: DataModel = {
}, },
publicDomains: {}, publicDomains: {},
privateDomains: [], privateDomains: [],
onions: [],
hostnameInfo: { hostnameInfo: {
80: [ 80: [
{ {
@@ -595,14 +585,6 @@ export const mockPatchData: DataModel = {
sslPort: 1234, sslPort: 1234,
}, },
}, },
{
kind: 'onion',
hostname: {
value: 'bitcoin-p2p.onion',
port: 80,
sslPort: 443,
},
},
], ],
}, },
}, },
@@ -625,7 +607,6 @@ export const mockPatchData: DataModel = {
}, },
publicDomains: {}, publicDomains: {},
privateDomains: [], privateDomains: [],
onions: [],
hostnameInfo: { hostnameInfo: {
8332: [], 8332: [],
}, },
@@ -649,7 +630,6 @@ export const mockPatchData: DataModel = {
}, },
publicDomains: {}, publicDomains: {},
privateDomains: [], privateDomains: [],
onions: [],
hostnameInfo: { hostnameInfo: {
8333: [], 8333: [],
}, },

View File

@@ -32,7 +32,6 @@ export class ConfigService {
private getAccessType = utils.once(() => { private getAccessType = utils.once(() => {
if (useMocks) return mocks.maskAs if (useMocks) return mocks.maskAs
if (this.hostname === 'localhost') return 'localhost' if (this.hostname === 'localhost') return 'localhost'
if (this.hostname.endsWith('.onion')) return 'tor'
if (this.hostname.endsWith('.local')) return 'mdns' if (this.hostname.endsWith('.local')) return 'mdns'
let ip = null let ip = null
try { try {
@@ -51,7 +50,7 @@ export class ConfigService {
} }
isLanHttp(): boolean { isLanHttp(): boolean {
return !this.isHttps() && !['localhost', 'tor'].includes(this.accessType) return !this.isHttps() && this.accessType !== 'localhost'
} }
isHttps(): boolean { isHttps(): boolean {