WIP: IP, pubkey, system time, system uptime, ca fingerprint (#2091)

* closes #923, #2063, #2012, #1153

* add ca fingerprint

* add `server.time`

* add `ip-info` to `server-info`

* add ssh pubkey

* support multiple IPs

* rename key

* add `ca-fingerprint` and `system-start-time`

* fix off-by-one

* update compat cargo lock

Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Matt Hill
2023-01-16 14:54:35 -07:00
committed by Aiden McClelland
parent 673e5af030
commit 06cf83b901
40 changed files with 2244 additions and 925 deletions

43
backend/src/net/dhcp.rs Normal file
View File

@@ -0,0 +1,43 @@
use std::collections::BTreeMap;
use futures::TryStreamExt;
use rpc_toolkit::command;
use crate::context::RpcContext;
use crate::db::model::IpInfo;
use crate::net::net_utils::{iface_is_physical, list_interfaces};
use crate::util::display_none;
use crate::Error;
pub async fn init_ips() -> Result<BTreeMap<String, IpInfo>, Error> {
let mut res = BTreeMap::new();
let mut ifaces = list_interfaces();
while let Some(iface) = ifaces.try_next().await? {
if iface_is_physical(&iface).await {
let ip_info = IpInfo::for_interface(&iface).await?;
res.insert(iface, ip_info);
}
}
Ok(res)
}
#[command(subcommands(update))]
pub async fn dhcp() -> Result<(), Error> {
Ok(())
}
#[command(display(display_none))]
pub async fn update(#[context] ctx: RpcContext, #[arg] interface: String) -> Result<(), Error> {
if iface_is_physical(&interface).await {
crate::db::DatabaseModel::new()
.server_info()
.ip_info()
.idx_model(&interface)
.put(
&mut ctx.db.handle(),
&IpInfo::for_interface(&interface).await?,
)
.await?;
}
Ok(())
}

View File

@@ -12,6 +12,7 @@ use crate::util::serde::Port;
use crate::Error;
pub mod cert_resolver;
pub mod dhcp;
pub mod dns;
pub mod embassy_service_http_server;
pub mod interface;
@@ -28,7 +29,7 @@ pub mod wifi;
const PACKAGE_CERT_PATH: &str = "/var/lib/embassy/ssl";
#[command(subcommands(tor::tor))]
#[command(subcommands(tor::tor, dhcp::dhcp))]
pub fn net() -> Result<(), Error> {
Ok(())
}

View File

@@ -1,13 +1,115 @@
use std::fmt;
use std::net::IpAddr;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::path::Path;
use std::str::FromStr;
use async_stream::try_stream;
use color_eyre::eyre::eyre;
use futures::stream::BoxStream;
use futures::{StreamExt, TryStreamExt};
use http::{Request, Uri};
use hyper::Body;
use tokio::process::Command;
use crate::util::Invoke;
use crate::Error;
fn parse_iface_ip(output: &str) -> Result<Option<&str>, Error> {
let output = output.trim();
if output.is_empty() {
return Ok(None);
}
if let Some(ip) = output
.split_ascii_whitespace()
.nth(3)
.and_then(|range| range.split("/").next())
{
Ok(Some(ip))
} else {
Err(Error::new(
eyre!("malformed output from `ip`"),
crate::ErrorKind::Network,
))
}
}
pub async fn get_iface_ipv4_addr(iface: &str) -> Result<Option<Ipv4Addr>, Error> {
Ok(parse_iface_ip(&String::from_utf8(
Command::new("ip")
.arg("-4")
.arg("-o")
.arg("addr")
.arg("show")
.arg(iface)
.invoke(crate::ErrorKind::Network)
.await?,
)?)?
.map(|s| s.parse())
.transpose()?)
}
pub async fn get_iface_ipv6_addr(iface: &str) -> Result<Option<Ipv6Addr>, Error> {
Ok(parse_iface_ip(&String::from_utf8(
Command::new("ip")
.arg("-6")
.arg("-o")
.arg("addr")
.arg("show")
.arg(iface)
.invoke(crate::ErrorKind::Network)
.await?,
)?)?
.map(|s| s.parse())
.transpose()?)
}
pub async fn iface_is_physical(iface: &str) -> bool {
tokio::fs::metadata(Path::new("/sys/class/net").join(iface).join("device"))
.await
.is_ok()
}
pub async fn iface_is_wireless(iface: &str) -> bool {
tokio::fs::metadata(Path::new("/sys/class/net").join(iface).join("wireless"))
.await
.is_ok()
}
pub fn list_interfaces() -> BoxStream<'static, Result<String, Error>> {
try_stream! {
let mut ifaces = tokio::fs::read_dir("/sys/class/net").await?;
while let Some(iface) = ifaces.next_entry().await? {
if let Some(iface) = iface.file_name().into_string().ok() {
yield iface;
}
}
}
.boxed()
}
pub async fn find_wifi_iface() -> Result<Option<String>, Error> {
let mut ifaces = list_interfaces();
while let Some(iface) = ifaces.try_next().await? {
if iface_is_wireless(&iface).await {
return Ok(Some(iface));
}
}
Ok(None)
}
pub async fn find_eth_iface() -> Result<String, Error> {
let mut ifaces = list_interfaces();
while let Some(iface) = ifaces.try_next().await? {
if iface_is_physical(&iface).await && !iface_is_wireless(&iface).await {
return Ok(iface);
}
}
Err(Error::new(
eyre!("Could not detect ethernet interface"),
crate::ErrorKind::Network,
))
}
pub fn host_addr_fqdn(req: &Request<Body>) -> Result<ResourceFqdn, Error> {
let host = req.headers().get(http::header::HOST);

View File

@@ -16,7 +16,10 @@ use crate::context::{DiagnosticContext, InstallContext, RpcContext, SetupContext
use crate::core::rpc_continuations::RequestGuid;
use crate::db::subscribe;
use crate::install::PKG_PUBLIC_DIR;
use crate::middleware::auth::HasValidSession;
use crate::middleware::auth::{auth as auth_middleware, HasValidSession};
use crate::middleware::cors::cors;
use crate::middleware::db::db as db_middleware;
use crate::middleware::diagnostic::diagnostic as diagnostic_middleware;
use crate::net::HttpHandler;
use crate::{diagnostic_api, install_api, main_api, setup_api, Error, ErrorKind, ResultExt};
@@ -48,8 +51,14 @@ pub async fn setup_ui_file_router(ctx: SetupContext) -> Result<HttpHandler, Erro
async move {
let res = match req.uri().path() {
path if path.starts_with("/rpc/") => {
let rpc_handler =
rpc_handler!({command: setup_api, context: ctx, status: status_fn});
let rpc_handler = rpc_handler!({
command: setup_api,
context: ctx,
status: status_fn,
middleware: [
cors,
]
});
rpc_handler(req)
.await
@@ -76,8 +85,15 @@ pub async fn diag_ui_file_router(ctx: DiagnosticContext) -> Result<HttpHandler,
async move {
let res = match req.uri().path() {
path if path.starts_with("/rpc/") => {
let rpc_handler =
rpc_handler!({command: diagnostic_api, context: ctx, status: status_fn});
let rpc_handler = rpc_handler!({
command: diagnostic_api,
context: ctx,
status: status_fn,
middleware: [
cors,
diagnostic_middleware,
]
});
rpc_handler(req)
.await
@@ -104,8 +120,14 @@ pub async fn install_ui_file_router(ctx: InstallContext) -> Result<HttpHandler,
async move {
let res = match req.uri().path() {
path if path.starts_with("/rpc/") => {
let rpc_handler =
rpc_handler!({command: install_api, context: ctx, status: status_fn});
let rpc_handler = rpc_handler!({
command: install_api,
context: ctx,
status: status_fn,
middleware: [
cors,
]
});
rpc_handler(req)
.await
@@ -132,8 +154,18 @@ pub async fn main_ui_server_router(ctx: RpcContext) -> Result<HttpHandler, Error
async move {
let res = match req.uri().path() {
path if path.starts_with("/rpc/") => {
let rpc_handler =
rpc_handler!({command: main_api, context: ctx, status: status_fn});
let auth_middleware = auth_middleware(ctx.clone());
let db_middleware = db_middleware(ctx.clone());
let rpc_handler = rpc_handler!({
command: main_api,
context: ctx,
status: status_fn,
middleware: [
cors,
auth_middleware,
db_middleware,
]
});
rpc_handler(req)
.await