diff --git a/appmgr/migrations/20210629193146_Init.sql b/appmgr/migrations/20210629193146_Init.sql index 9668764df..fbf8a8ae4 100644 --- a/appmgr/migrations/20210629193146_Init.sql +++ b/appmgr/migrations/20210629193146_Init.sql @@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS tor ( package TEXT NOT NULL, interface TEXT NOT NULL, - key BLOB NOT NULL, + key BLOB NOT NULL CHECK (length(key) = 64), PRIMARY KEY (package, interface) ); CREATE TABLE IF NOT EXISTS session @@ -19,7 +19,7 @@ CREATE TABLE IF NOT EXISTS account ( id INTEGER PRIMARY KEY CHECK (id = 0), password TEXT NOT NULL, - tor_key BLOB NOT NULL + tor_key BLOB NOT NULL CHECK (length(tor_key) = 64) ); CREATE TABLE IF NOT EXISTS ssh_keys ( diff --git a/appmgr/src/action/docker.rs b/appmgr/src/action/docker.rs index 977daf3f3..026c627f9 100644 --- a/appmgr/src/action/docker.rs +++ b/appmgr/src/action/docker.rs @@ -10,7 +10,7 @@ use serde_json::Value; use crate::id::{Id, ImageId}; use crate::s9pk::manifest::{PackageId, SYSTEM_PACKAGE_ID}; use crate::util::{IoFormat, Version}; -use crate::volume::{Volume, VolumeId, Volumes}; +use crate::volume::{VolumeId, Volumes}; use crate::{Error, ResultExt, HOST_IP}; pub const NET_TLD: &'static str = "embassy"; @@ -195,7 +195,7 @@ impl DockerAction { let mut res = Vec::with_capacity( (2 * self.mounts.len()) // --mount + (2 * self.shm_size_mb.is_some() as usize) // --shm-size - + 3 // --entrypoint + + 4 // --log-driver=journald --entrypoint + self.args.len(), // [ARG...] ); for (volume_id, dst) in &self.mounts { @@ -227,6 +227,7 @@ impl DockerAction { res.push(OsString::from(Self::container_name(pkg_id, None)).into()); res.push(OsStr::new(&self.entrypoint).into()); } else { + res.push(OsStr::new("--log-driver=journald").into()); res.push(OsStr::new("--entrypoint").into()); res.push(OsStr::new(&self.entrypoint).into()); if self.system { diff --git a/appmgr/src/auth.rs b/appmgr/src/auth.rs index 2edc406d7..5b884b1fa 100644 --- a/appmgr/src/auth.rs +++ b/appmgr/src/auth.rs @@ -117,7 +117,7 @@ pub async fn login( res.headers.insert( "set-cookie", HeaderValue::from_str(&format!( - "session={}; HttpOnly; SameSite=Strict; Expires=Fri, 31 Dec 9999 23:59:59 GMT;", + "session={}; SameSite=Strict; Expires=Fri, 31 Dec 9999 23:59:59 GMT;", token )) .with_kind(crate::ErrorKind::Unknown)?, // Should be impossible, but don't want to panic diff --git a/appmgr/src/daemon/tor_health_check.rs b/appmgr/src/daemon/tor_health_check.rs index ae6a384b8..239ac7d41 100644 --- a/appmgr/src/daemon/tor_health_check.rs +++ b/appmgr/src/daemon/tor_health_check.rs @@ -19,7 +19,7 @@ pub async fn tor_health_check_daemon(tor_controller: &TorController) { json!({ "jsonrpc": "2.0", "method": "echo", - "params": [{ "message": "Follow the orange rabbit" }], + "params": { "message": "Follow the orange rabbit" }, }) .to_string() .into_bytes(), @@ -28,7 +28,7 @@ pub async fn tor_health_check_daemon(tor_controller: &TorController) { .await; match result { // if success, do nothing - Ok(response) => {} + Ok(_) => {} // if failure, disconnect tor control port, and restart tor controller Err(e) => { log::error!("Unable to reach self over tor: {}", e); diff --git a/appmgr/src/db/mod.rs b/appmgr/src/db/mod.rs index bfb033c70..03cc8e1be 100644 --- a/appmgr/src/db/mod.rs +++ b/appmgr/src/db/mod.rs @@ -4,7 +4,7 @@ pub mod util; use std::future::Future; use std::sync::Arc; -use futures::{SinkExt, StreamExt}; +use futures::{FutureExt, SinkExt, StreamExt}; use patch_db::json_ptr::JsonPointer; use patch_db::{Dump, Revision}; use rpc_toolkit::command; @@ -34,7 +34,17 @@ async fn ws_handler< .await .with_kind(crate::ErrorKind::Network)? .with_kind(crate::ErrorKind::Unknown)?; - stream.next().await; + loop { + if let Some(Message::Text(_)) = stream + .next() + .await + .transpose() + .with_kind(crate::ErrorKind::Network)? + { + // TODO: check auth + break; + } + } stream .send(Message::Text( rpc_toolkit::serde_json::to_string(&dump).with_kind(crate::ErrorKind::Serialization)?, @@ -43,20 +53,44 @@ async fn ws_handler< .with_kind(crate::ErrorKind::Network)?; loop { - let rev = sub.recv().await.with_kind(crate::ErrorKind::Database)?; - stream - .send(Message::Text( - rpc_toolkit::serde_json::to_string(&rev) - .with_kind(crate::ErrorKind::Serialization)?, - )) - .await - .with_kind(crate::ErrorKind::Network)?; + futures::select! { + new_rev = sub.recv().fuse() => { + let rev = new_rev.with_kind(crate::ErrorKind::Database)?; + stream + .send(Message::Text( + rpc_toolkit::serde_json::to_string(&rev) + .with_kind(crate::ErrorKind::Serialization)?, + )) + .await + .with_kind(crate::ErrorKind::Network)?; + } + message = stream.next().fuse() => { + match message.transpose().with_kind(crate::ErrorKind::Network)? { + Some(Message::Ping(a)) => { + stream + .send(Message::Pong(a)) + .await + .with_kind(crate::ErrorKind::Network)?; + } + Some(Message::Close(frame)) => { + if let Some(reason) = frame.as_ref() { + log::info!("Closing WebSocket: Reason: {} {}", reason.code, reason.reason); + } + stream + .send(Message::Close(frame)) + .await + .with_kind(crate::ErrorKind::Network)?; + return Ok(()) + } + _ => (), + } + } + } } } pub async fn subscribe(ctx: RpcContext, req: Request) -> Result, Error> { let (parts, body) = req.into_parts(); - // is_authed(&ctx, &parts).await?; let req = Request::from_parts(parts, body); let (res, ws_fut) = hyper_ws_listener::create_ws(req).with_kind(crate::ErrorKind::Network)?; if let Some(ws_fut) = ws_fut { diff --git a/appmgr/src/lib.rs b/appmgr/src/lib.rs index bd435d3f1..d7b330ca5 100644 --- a/appmgr/src/lib.rs +++ b/appmgr/src/lib.rs @@ -59,11 +59,12 @@ pub fn echo(#[arg] message: String) -> Result { #[command(subcommands( version::git_info, echo, + developer::init, s9pk::pack, s9pk::verify, - developer::init, inspect::inspect, package, + net::net, auth::auth, db::db, ))] diff --git a/appmgr/src/net/mod.rs b/appmgr/src/net/mod.rs index 785b78a06..1c5ab60ea 100644 --- a/appmgr/src/net/mod.rs +++ b/appmgr/src/net/mod.rs @@ -1,5 +1,6 @@ use std::net::{Ipv4Addr, SocketAddr}; +use rpc_toolkit::command; use torut::onion::TorSecretKeyV3; use self::interface::{Interface, InterfaceId}; @@ -16,6 +17,11 @@ pub mod mdns; pub mod tor; pub mod wifi; +#[command(subcommands(tor::tor))] +pub fn net() -> Result<(), Error> { + Ok(()) +} + pub struct NetController { pub tor: TorController, #[cfg(feature = "avahi")] diff --git a/appmgr/src/net/tor.rs b/appmgr/src/net/tor.rs index b894316db..8f5eac5ae 100644 --- a/appmgr/src/net/tor.rs +++ b/appmgr/src/net/tor.rs @@ -3,8 +3,10 @@ use std::net::{Ipv4Addr, SocketAddr}; use std::time::Duration; use anyhow::anyhow; +use clap::ArgMatches; use futures::future::BoxFuture; use futures::FutureExt; +use rpc_toolkit::command; use sqlx::{Executor, Sqlite}; use tokio::net::TcpStream; use tokio::sync::Mutex; @@ -12,7 +14,9 @@ use torut::control::{AsyncEvent, AuthenticatedConn, ConnError}; use torut::onion::{OnionAddressV3, TorSecretKeyV3}; use super::interface::{InterfaceId, TorConfig}; +use crate::context::RpcContext; use crate::s9pk::manifest::PackageId; +use crate::util::{display_serializable, IoFormat}; use crate::{Error, ErrorKind, ResultExt as _}; #[test] @@ -20,6 +24,36 @@ fn random_key() { println!("'0x{}'", hex::encode(TorSecretKeyV3::generate().as_bytes())); } +#[command(subcommands(list_services))] +pub fn tor() -> Result<(), Error> { + Ok(()) +} + +fn display_services(services: Vec, matches: &ArgMatches<'_>) { + use prettytable::*; + + if matches.is_present("format") { + return display_serializable(services, matches); + } + + let mut table = Table::new(); + for service in services { + let row = row![&service.to_string()]; + table.add_row(row); + } + table.print_tty(false); +} + +#[command(rename = "list-services", display(display_services))] +pub async fn list_services( + #[context] ctx: RpcContext, + #[allow(unused_variables)] + #[arg(long = "format")] + format: Option, +) -> Result, Error> { + ctx.net_controller.tor.list_services().await +} + pub async fn os_key(secrets: &mut Ex) -> Result where for<'a> &'a mut Ex: Executor<'a, Database = Sqlite>, @@ -79,6 +113,10 @@ impl TorController { pub async fn embassyd_onion(&self) -> OnionAddressV3 { self.0.lock().await.embassyd_onion() } + + pub async fn list_services(&self) -> Result, Error> { + self.0.lock().await.list_services().await + } } type AuthenticatedConnection = AuthenticatedConn< @@ -154,10 +192,7 @@ impl TorControllerInner { self.connection .as_mut() .ok_or_else(|| { - Error::new( - anyhow!("Missing Tor Control Connection"), - ErrorKind::Unknown, - ) + Error::new(anyhow!("Missing Tor Control Connection"), ErrorKind::Tor) })? .del_onion( &key.public() @@ -200,9 +235,13 @@ impl TorControllerInner { } async fn add_embassyd_onion(&mut self) -> Result<(), Error> { + log::info!( + "Registering Main Tor Service: {}", + self.embassyd_tor_key.public().get_onion_address() + ); self.connection .as_mut() - .expect("Tor Connection is None") + .ok_or_else(|| Error::new(anyhow!("Missing Tor Control Connection"), ErrorKind::Tor))? .add_onion_v3( &self.embassyd_tor_key, false, @@ -212,6 +251,10 @@ impl TorControllerInner { &mut std::iter::once(&(self.embassyd_addr.port(), self.embassyd_addr)), ) .await?; + log::info!( + "Registered Main Tor Service: {}", + self.embassyd_tor_key.public().get_onion_address() + ); Ok(()) } @@ -223,6 +266,7 @@ impl TorControllerInner { let uptime = c.get_info("uptime").await?.parse::()?; // we never want to restart the tor daemon if it hasn't been up for at least a half hour if uptime < 1800 { + self.connection = Some(c); // put it back return Ok(false); } // when connection closes below, tor daemon is restarted @@ -251,11 +295,11 @@ impl TorControllerInner { let uptime_new = new_connection.get_info("uptime").await?.parse::()?; // if the new uptime exceeds the one we got at the beginning, it's the same tor daemon, do not proceed match uptime { - Some(uptime) if uptime_new < uptime => { + Some(uptime) if uptime_new > uptime => (), + _ => { new_connection.set_async_event_handler(Some(event_handler)); break; } - _ => (), } } Err(e) => { @@ -289,6 +333,17 @@ impl TorControllerInner { fn embassyd_onion(&self) -> OnionAddressV3 { self.embassyd_tor_key.public().get_onion_address() } + + async fn list_services(&mut self) -> Result, Error> { + self.connection + .as_mut() + .ok_or_else(|| Error::new(anyhow!("Missing Tor Control Connection"), ErrorKind::Tor))? + .get_info("onions/current") + .await? + .lines() + .map(|l| l.trim().parse().with_kind(ErrorKind::Tor)) + .collect() + } } #[tokio::test] diff --git a/ui/package-lock.json b/ui/package-lock.json index e28d4127e..21a2fe213 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -16820,7 +16820,9 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.0.tgz", "integrity": "sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q==", "dev": true, - "requires": {} + "requires": { + "ajv": "^8.0.0" + } }, "core-js": { "version": "3.16.0", @@ -16898,7 +16900,9 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.0.tgz", "integrity": "sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q==", "dev": true, - "requires": {} + "requires": { + "ajv": "^8.0.0" + } }, "json-schema-traverse": { "version": "1.0.0", @@ -16942,7 +16946,9 @@ "integrity": "sha512-Brah4Uo5/U8v76c6euTwtjVFFaVishwnJrQBYpev1JRh4vjA1F4HY3UzQez41YUCszUCXKagG8v6eVRBHV1gkw==", "dev": true, "peer": true, - "requires": {} + "requires": { + "ajv": "^8.0.0" + } }, "json-schema-traverse": { "version": "1.0.0", @@ -16995,7 +17001,9 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.0.tgz", "integrity": "sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q==", "dev": true, - "requires": {} + "requires": { + "ajv": "^8.0.0" + } }, "json-schema-traverse": { "version": "1.0.0", @@ -17073,7 +17081,9 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.0.tgz", "integrity": "sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q==", "dev": true, - "requires": {} + "requires": { + "ajv": "^8.0.0" + } }, "json-schema-traverse": { "version": "1.0.0", @@ -18622,7 +18632,9 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.0.tgz", "integrity": "sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q==", "dev": true, - "requires": {} + "requires": { + "ajv": "^8.0.0" + } }, "json-schema-traverse": { "version": "1.0.0",