@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "accountd"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
airup = []
|
||||
embed_init_file = []
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
cfg-if = "1"
|
||||
bitflags = "2"
|
||||
clap = { workspace = true }
|
||||
rusqlite = "0.39"
|
||||
rustc-hash = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
semios_account = { path = "../../lib/semios_account" }
|
||||
uuid = { version = "1", features = ["v7"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1"
|
||||
features = ["rt", "net", "macros", "io-util", "process", "signal", "sync", "time"]
|
||||
@@ -0,0 +1 @@
|
||||
// Sandbox rules for running an authentication method.
|
||||
@@ -0,0 +1,387 @@
|
||||
//! Implementation of the API server.
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
util::{
|
||||
ipc::{Connection, Listener},
|
||||
time::timestamp_s,
|
||||
},
|
||||
};
|
||||
use rustc_hash::FxHashMap;
|
||||
use semios_account::{
|
||||
auth::{UserAuthMethodFlags, UserAuthMethodRecord},
|
||||
error::Error,
|
||||
protocol::*,
|
||||
record::{GroupInfo, SecureTag, UserInfo},
|
||||
secret::Secret,
|
||||
};
|
||||
use std::{fmt::Debug, path::PathBuf, pin::Pin, sync::Arc};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn launch_server(app_state: Arc<AppState>) -> anyhow::Result<()> {
|
||||
let context = Arc::new(ApiContext::new(app_state));
|
||||
let listener = Listener::new(&semios_account::protocol::uri()).await?;
|
||||
let server = Server { context, listener };
|
||||
tokio::spawn(server.run());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
macro_rules! compose {
|
||||
($orig:ident) => {
|
||||
Box::new(|state, caller, args| {
|
||||
Box::pin(async move {
|
||||
let args = match serde_json::from_value(args) {
|
||||
Ok(x) => x,
|
||||
Err(e) => return Err(Error::InvalidParams(e.to_string())),
|
||||
};
|
||||
$orig(state, caller, args)
|
||||
.await
|
||||
.map(|x| serde_json::to_value(x).expect("invalid api return value"))
|
||||
}) as FutureMethod
|
||||
}) as FnMethod
|
||||
};
|
||||
}
|
||||
|
||||
type FutureMethod = Pin<Box<dyn Future<Output = Result<serde_json::Value, Error>> + Send>>;
|
||||
type FnMethod = Box<dyn Fn(Arc<AppState>, Caller, serde_json::Value) -> FutureMethod + Send + Sync>;
|
||||
|
||||
/// A context for API users.
|
||||
pub struct ApiContext {
|
||||
methods: FxHashMap<&'static str, FnMethod>,
|
||||
app_state: Arc<AppState>,
|
||||
}
|
||||
impl ApiContext {
|
||||
pub fn new(app_state: Arc<AppState>) -> Self {
|
||||
let mut methods = FxHashMap::default();
|
||||
methods.insert(GET_USER_INFO, compose!(get_user_info));
|
||||
methods.insert(GET_GROUP_INFO, compose!(get_group_info));
|
||||
methods.insert(LIST_USER, compose!(list_user));
|
||||
methods.insert(LIST_GROUP, compose!(list_group));
|
||||
methods.insert(CREATE_USER, compose!(create_user));
|
||||
methods.insert(REMOVE_USER, compose!(remove_user));
|
||||
methods.insert(CREATE_GROUP, compose!(create_group));
|
||||
methods.insert(GET_SECRET, compose!(get_secret));
|
||||
methods.insert(SET_SECRET, compose!(set_secret));
|
||||
methods.insert(GET_USER_AUTH_METHODS, compose!(get_user_auth_methods));
|
||||
methods.insert(USER_ADD_AUTH_METHOD, compose!(user_add_auth_method));
|
||||
methods.insert(GET_AUTH_METHOD_PATH, compose!(get_auth_method_path));
|
||||
Self { methods, app_state }
|
||||
}
|
||||
|
||||
pub async fn invoke(&self, method: &str, caller: Caller, args: serde_json::Value) -> Response {
|
||||
let Some(method) = self.methods.get(&method) else {
|
||||
return Response::from_result::<()>(Err(Error::NotImplemented(method.into())));
|
||||
};
|
||||
Response::from_result(method(self.app_state.clone(), caller, args).await)
|
||||
}
|
||||
}
|
||||
impl Debug for ApiContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ApiContext")
|
||||
.field("methods", &self.methods.keys())
|
||||
.field("app_state", &self.app_state)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Caller {
|
||||
/// Unknown user. No privileges are granted.
|
||||
Unknown,
|
||||
|
||||
/// Explicit user.
|
||||
User(Uuid),
|
||||
|
||||
/// All privileges are granted, but no user is specified.
|
||||
Immortal,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Server {
|
||||
context: Arc<ApiContext>,
|
||||
listener: Listener,
|
||||
}
|
||||
impl Server {
|
||||
pub async fn run(self) -> anyhow::Result<()> {
|
||||
loop {
|
||||
let connection = match self.listener.accept().await {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
tracing::warn!("ipc_accept() failed: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let session = Session {
|
||||
context: self.context.clone(),
|
||||
connection,
|
||||
caller: Caller::Unknown,
|
||||
};
|
||||
tokio::spawn(session.run());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Session {
|
||||
context: Arc<ApiContext>,
|
||||
connection: Connection,
|
||||
caller: Caller,
|
||||
}
|
||||
impl Session {
|
||||
pub async fn run(mut self) -> anyhow::Result<()> {
|
||||
let peer_uuid = match self.connection.peer_host_uid() {
|
||||
Some(uid) => self.context.app_state.local_db.find_user_by_host_uid(uid),
|
||||
None => None,
|
||||
};
|
||||
self.caller = match peer_uuid {
|
||||
Some(uuid) => Caller::User(uuid),
|
||||
None => Caller::Unknown,
|
||||
};
|
||||
let mut buf = Vec::with_capacity(1024);
|
||||
loop {
|
||||
self.connection.recv(&mut buf).await?;
|
||||
let req: Request = serde_json::from_slice(&buf)?;
|
||||
let resp = self
|
||||
.context
|
||||
.invoke(&req.method, self.caller, req.params)
|
||||
.await;
|
||||
buf.clear();
|
||||
serde_json::to_writer(&mut buf, &resp).expect("invalid response emitted");
|
||||
self.connection.send(&buf).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_user(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: ListUserArgs,
|
||||
) -> Result<Vec<Uuid>, Error> {
|
||||
Ok(state.local_db.list_user(args.start, args.len))
|
||||
}
|
||||
|
||||
async fn list_group(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: ListGroupArgs,
|
||||
) -> Result<Vec<Uuid>, Error> {
|
||||
Ok(state.local_db.list_group(args.start, args.len))
|
||||
}
|
||||
|
||||
async fn get_user_info(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: GetUserInfoArgs,
|
||||
) -> Result<UserInfo, Error> {
|
||||
let uuid = match args {
|
||||
GetUserInfoArgs::Uuid(x) => x,
|
||||
GetUserInfoArgs::HostUid(uid) => state
|
||||
.local_db
|
||||
.find_user_by_host_uid(uid)
|
||||
.ok_or(Error::NoSuchUser)?,
|
||||
GetUserInfoArgs::Username(name) => state
|
||||
.local_db
|
||||
.find_user_by_name(&name)
|
||||
.ok_or(Error::NoSuchUser)?,
|
||||
};
|
||||
state.local_db.get_user_info(&uuid).ok_or(Error::NoSuchUser)
|
||||
}
|
||||
|
||||
async fn get_group_info(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: GetGroupInfoArgs,
|
||||
) -> Result<GroupInfo, Error> {
|
||||
let uuid = match args {
|
||||
GetGroupInfoArgs::Uuid(x) => x,
|
||||
GetGroupInfoArgs::HostGid(gid) => state
|
||||
.local_db
|
||||
.find_group_by_host_gid(gid)
|
||||
.ok_or(Error::NoSuchGroup)?,
|
||||
GetGroupInfoArgs::Groupname(name) => state
|
||||
.local_db
|
||||
.find_group_by_name(&name)
|
||||
.ok_or(Error::NoSuchGroup)?,
|
||||
};
|
||||
state
|
||||
.local_db
|
||||
.get_group_info(&uuid)
|
||||
.ok_or(Error::NoSuchGroup)
|
||||
}
|
||||
|
||||
async fn create_user(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: CreateUserArgs,
|
||||
) -> Result<(), Error> {
|
||||
require_secure_tags(&state, caller, &[SecureTag::CreateUser]).await?;
|
||||
let user_info = UserInfo {
|
||||
uuid: Uuid::new_v7(uuid::Timestamp::now(uuid::ContextV7::new())),
|
||||
fullname: args.fullname.unwrap_or_else(|| args.username.clone()),
|
||||
username: args.username,
|
||||
host_uid: args.host_uid,
|
||||
description: args.description,
|
||||
secure_tags: args.secure_tags,
|
||||
extra_records: args.extra_records,
|
||||
defaults: args.defaults,
|
||||
home_directory: args.home_directory,
|
||||
creation_time: timestamp_s(),
|
||||
last_login_time: 0,
|
||||
};
|
||||
state
|
||||
.local_db
|
||||
.insert_user_info(user_info)
|
||||
.map_err(|_| Error::AlreadyExists)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_group(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: CreateGroupArgs,
|
||||
) -> Result<(), Error> {
|
||||
require_secure_tags(&state, caller, &[SecureTag::CreateUser]).await?;
|
||||
let group_info = GroupInfo {
|
||||
uuid: Uuid::new_v7(uuid::Timestamp::now(uuid::ContextV7::new())),
|
||||
fullname: args.fullname.unwrap_or_else(|| args.groupname.clone()),
|
||||
groupname: args.groupname,
|
||||
host_gid: args.host_gid,
|
||||
description: args.description,
|
||||
creation_time: timestamp_s(),
|
||||
};
|
||||
state
|
||||
.local_db
|
||||
.insert_group_info(group_info)
|
||||
.map_err(|_| Error::AlreadyExists)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_user(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: RemoveUserArgs,
|
||||
) -> Result<(), Error> {
|
||||
require_secure_tags(&state, caller, &[SecureTag::RemoveUser]).await?;
|
||||
todo!();
|
||||
}
|
||||
|
||||
async fn get_secret(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: GetSecretArgs,
|
||||
) -> Result<Secret, Error> {
|
||||
require_secure_tags(&state, caller, &[SecureTag::ReadSecret])
|
||||
.await
|
||||
.or(require_same_user(&state, caller, args.user).await)?;
|
||||
|
||||
let raw_secret = state
|
||||
.local_db
|
||||
.get_secret(&args.user, &args.name)
|
||||
.ok_or(Error::NoSuchSecret)?;
|
||||
|
||||
Ok(raw_secret)
|
||||
}
|
||||
|
||||
async fn set_secret(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: SetSecretArgs,
|
||||
) -> Result<(), Error> {
|
||||
require_secure_tags(&state, caller, &[SecureTag::WriteSecret])
|
||||
.await
|
||||
.or(require_same_user(&state, caller, args.user).await)?;
|
||||
|
||||
let raw_secret = args.secret;
|
||||
|
||||
_ = state.local_db.delete_secret(&args.user, &raw_secret.name);
|
||||
state.local_db.insert_secret(&args.user, raw_secret)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_user_auth_methods(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: GetUserAuthMethodsArgs,
|
||||
) -> Result<Vec<UserAuthMethodRecord>, Error> {
|
||||
require_secure_tags(&state, Caller::User(args.user), &[SecureTag::Login]).await?;
|
||||
state.local_db.get_user_auth_methods(&args.user)
|
||||
}
|
||||
|
||||
async fn user_add_auth_method(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: UserAddAuthMethodArgs,
|
||||
) -> Result<(), Error> {
|
||||
require_secure_tags(&state, Caller::User(args.user), &[SecureTag::Login]).await?;
|
||||
|
||||
let current = state.local_db.get_user_auth_methods(&args.user)?;
|
||||
let exec = state
|
||||
.auth_method(&args.auth_method)
|
||||
.ok_or(Error::NoSuchAuthMethod)?;
|
||||
let info = exec
|
||||
.info()
|
||||
.await
|
||||
.map_err(|e| Error::Internal(e.to_string()))?;
|
||||
let mut flags = UserAuthMethodFlags::empty();
|
||||
if info.provides_master_key && current.is_empty() {
|
||||
flags |= UserAuthMethodFlags::MAIN;
|
||||
}
|
||||
state
|
||||
.local_db
|
||||
.insert_user_auth_method(&args.user, &args.auth_method, flags)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_auth_method_path(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: GetAuthMethodPathArgs,
|
||||
) -> Result<PathBuf, Error> {
|
||||
Ok(state
|
||||
.auth_method(&args.name)
|
||||
.ok_or(Error::NoSuchAuthMethod)?
|
||||
.path()
|
||||
.into())
|
||||
}
|
||||
|
||||
// ==- Helpers -==
|
||||
|
||||
async fn require_same_user(
|
||||
state: &AppState,
|
||||
caller: Caller,
|
||||
requested_user: Uuid,
|
||||
) -> Result<(), Error> {
|
||||
let peer_user = match caller {
|
||||
Caller::Unknown => return Err(Error::PermissionDenied),
|
||||
Caller::User(uuid) => uuid,
|
||||
Caller::Immortal => return Ok(()),
|
||||
};
|
||||
if requested_user == peer_user {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::PermissionDenied)
|
||||
}
|
||||
}
|
||||
|
||||
async fn require_secure_tags(
|
||||
state: &AppState,
|
||||
caller: Caller,
|
||||
tags: &[SecureTag],
|
||||
) -> Result<(), Error> {
|
||||
let peer_user = match caller {
|
||||
Caller::Unknown => return Err(Error::PermissionDenied),
|
||||
Caller::User(uuid) => uuid,
|
||||
Caller::Immortal => return Ok(()),
|
||||
};
|
||||
let user_info = state
|
||||
.local_db
|
||||
.get_user_info(&peer_user)
|
||||
.ok_or(Error::PermissionDenied)?;
|
||||
for tag in tags {
|
||||
if !user_info.secure_tags.contains(tag) {
|
||||
return Err(Error::PermissionDenied);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use anyhow::anyhow;
|
||||
use semios_account::auth::{AuthCli, AuthMethodInfo};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::Stdio,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AuthExec(PathBuf);
|
||||
impl AuthExec {
|
||||
pub fn from_path_buf(path_buf: PathBuf) -> Self {
|
||||
Self(path_buf)
|
||||
}
|
||||
|
||||
pub fn name(&self) -> anyhow::Result<String> {
|
||||
let err = || anyhow!("invalid auth exec \"{}\"", self.0.display());
|
||||
self.0
|
||||
.file_name()
|
||||
.ok_or_else(err)?
|
||||
.to_string_lossy()
|
||||
.strip_prefix("auth_")
|
||||
.ok_or_else(err)
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
pub async fn info(&self) -> anyhow::Result<AuthMethodInfo> {
|
||||
let wait = tokio::process::Command::new(&self.0)
|
||||
.args(AuthCli::QueryInformation.compose())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?
|
||||
.wait_with_output()
|
||||
.await?;
|
||||
|
||||
if !wait.status.success() {
|
||||
return Err(anyhow!(
|
||||
"process \"{}\" exited with status code {}",
|
||||
self.0.display(),
|
||||
wait.status
|
||||
));
|
||||
}
|
||||
let parsed: AuthMethodInfo = serde_json::from_slice(&wait.stdout)?;
|
||||
|
||||
if parsed.name != self.name()? {
|
||||
return Err(anyhow!(
|
||||
"mismatched filename \"{}\" for auth method \"{}\"",
|
||||
self.name()?,
|
||||
parsed.name
|
||||
));
|
||||
}
|
||||
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//! Initialization on first run.
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
api::{ApiContext, Caller},
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use semios_account::protocol::Request;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "embed_init_file")]
|
||||
const EMBEDDED: &str = include_str!("../../../build_config/init.json");
|
||||
|
||||
#[cfg(not(feature = "embed_init_file"))]
|
||||
const EMBEDDED: &str = "[]";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct InitCommand {
|
||||
pub on_failure: OnFailure,
|
||||
pub request: Request,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum OnFailure {
|
||||
Terminate,
|
||||
Warn,
|
||||
Skip,
|
||||
}
|
||||
|
||||
pub async fn run(app_state: Arc<AppState>) -> anyhow::Result<()> {
|
||||
let init_file: Vec<InitCommand> = serde_json::from_str(EMBEDDED)?;
|
||||
let api_context = ApiContext::new(app_state);
|
||||
for cmd in init_file {
|
||||
let resp = api_context
|
||||
.invoke(
|
||||
&cmd.request.method,
|
||||
Caller::Immortal,
|
||||
cmd.request.params.clone(),
|
||||
)
|
||||
.await;
|
||||
if resp.success {
|
||||
tracing::debug!(
|
||||
"Successfully ran init_command {:?} with response {:?}",
|
||||
cmd,
|
||||
resp
|
||||
);
|
||||
} else {
|
||||
match cmd.on_failure {
|
||||
OnFailure::Terminate => {
|
||||
tracing::error!("Failed to run init_command {:?}: {:?}", cmd, resp);
|
||||
return Err(anyhow!(
|
||||
"An init command failed to run, see logs for details"
|
||||
));
|
||||
}
|
||||
OnFailure::Warn => {
|
||||
tracing::warn!("Failed to run init_command {:?}: {:?}", cmd, resp);
|
||||
}
|
||||
OnFailure::Skip => {
|
||||
tracing::debug!("Failed to run init_command {:?}: {:?}", cmd, resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
use rusqlite::params;
|
||||
use semios_account::{
|
||||
auth::{UserAuthMethodFlags, UserAuthMethodRecord},
|
||||
error::Error,
|
||||
record::{GroupInfo, HostGid, HostUid, UserInfo},
|
||||
secret::{Secret, SecurityFlags},
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::{path::Path, sync::Mutex};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Schema of the database.
|
||||
///
|
||||
/// The "user" table has columns:
|
||||
/// - "user_uuid": UUID of the user.
|
||||
/// - "user_name": Name of the user.
|
||||
/// - "user_host_uid": Host UID of the user.
|
||||
/// - "user_info": User information JSON (contains host specific information).
|
||||
///
|
||||
/// The "group" table has columns:
|
||||
/// - "group_uuid": UUID of the group.
|
||||
/// - "group_name": Name of the group.
|
||||
/// - "group_host_gid": Host GID of the group.
|
||||
/// - "group_info": Group information JSON (contains host specific information).
|
||||
///
|
||||
/// The "kv_store" table is used to store automatic configurations that are not edited by user.
|
||||
///
|
||||
/// The "secret" table has columns:
|
||||
/// - "secret_owner": Owner user of the secret, in UUID text.
|
||||
/// - "secret_name": Name of the secret.
|
||||
/// - "secret_data": Data of the secret.
|
||||
/// - "secret_security_flags": Secret security flags.
|
||||
/// - "secret_creation_time": Timestamp the secret is created.
|
||||
/// - "secret_expiration_time": Timestamp the secret will be expired.
|
||||
///
|
||||
/// The "group_membership" table has columns:
|
||||
/// - "group_membership_user": UUID of the user in the record.
|
||||
/// - "group_membership_group": UUID of the group in the record.
|
||||
///
|
||||
/// The "user_auth" table has columns:
|
||||
/// - "user_auth_user": UUID of the user in the record.
|
||||
/// - "user_auth_method": Name of the auth method specified in the record.
|
||||
/// - "user_auth_flags": Flags of the record.
|
||||
const SCHEMA: &str = "
|
||||
CREATE TABLE IF NOT EXISTS \"user\"(
|
||||
\"user_uuid\" TEXT PRIMARY KEY,
|
||||
\"user_name\" TEXT NOT NULL UNIQUE,
|
||||
\"user_host_uid\" TEXT NOT NULL UNIQUE,
|
||||
\"user_info\" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS \"group\"(
|
||||
\"group_uuid\" TEXT PRIMARY KEY,
|
||||
\"group_name\" TEXT NOT NULL UNIQUE,
|
||||
\"group_host_gid\" TEXT NOT NULL UNIQUE,
|
||||
\"group_info\" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS \"kv_store\"(
|
||||
\"kv_store_key\" TEXT PRIMARY KEY,
|
||||
\"kv_store_value\" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS \"secret\"(
|
||||
\"secret_owner\" TEXT,
|
||||
\"secret_name\" TEXT,
|
||||
\"secret_data\" BLOB NOT NULL,
|
||||
\"secret_security_flags\" INTEGER NOT NULL,
|
||||
\"secret_creation_time\" INTEGER NOT NULL,
|
||||
\"secret_expiration_time\" INTEGER NOT NULL,
|
||||
PRIMARY KEY (\"secret_owner\", \"secret_name\")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS \"group_membership\"(
|
||||
\"group_membership_user\" TEXT NOT NULL,
|
||||
\"group_membership_group\" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS \"user_auth\"(
|
||||
\"user_auth_user\" TEXT NOT NULL,
|
||||
\"user_auth_method\" TEXT NOT NULL,
|
||||
\"user_auth_flags\" INTEGER NOT NULL
|
||||
);
|
||||
";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LocalDb(Mutex<rusqlite::Connection>);
|
||||
impl LocalDb {
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
|
||||
let connection = rusqlite::Connection::open(path).map_err(Error::make_internal)?;
|
||||
connection
|
||||
.execute_batch(SCHEMA)
|
||||
.map_err(Error::make_internal)?;
|
||||
Ok(Self(Mutex::new(connection)))
|
||||
}
|
||||
|
||||
pub fn get_secret(&self, owner: &Uuid, name: &str) -> Option<Secret> {
|
||||
let lock = self.0.lock().unwrap();
|
||||
let mut stmt = lock
|
||||
.prepare_cached(
|
||||
"SELECT * FROM \"secret\" WHERE \"secret_owner\" = ?1 AND \"secret_name\" = ?2",
|
||||
)
|
||||
.ok()?;
|
||||
stmt.query_one(params![owner.to_string(), name], |row| {
|
||||
Ok(Secret {
|
||||
name: name.into(),
|
||||
data: row.get("secret_data")?,
|
||||
security_flags: SecurityFlags::from_bits_retain(row.get("secret_security_flags")?),
|
||||
creation_time: row.get("secret_creation_time")?,
|
||||
expiration_time: row.get("secret_expiration_time")?,
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub fn insert_secret(&self, owner: &Uuid, secret: Secret) -> Result<(), Error> {
|
||||
let mut lock = self.0.lock().unwrap();
|
||||
execute(
|
||||
&mut *lock,
|
||||
"INSERT INTO \"secret\"(
|
||||
\"secret_owner\",
|
||||
\"secret_name\",
|
||||
\"secret_data\",
|
||||
\"secret_security_flags\",
|
||||
\"secret_creation_time\",
|
||||
\"secret_expiration_time\"
|
||||
) VALUES(?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![
|
||||
owner.to_string(),
|
||||
secret.name,
|
||||
secret.data,
|
||||
secret.security_flags.bits(),
|
||||
secret.creation_time,
|
||||
secret.expiration_time
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn delete_secret(&self, uuid: &Uuid, name: &str) -> Result<(), Error> {
|
||||
execute(
|
||||
&mut *self.0.lock().unwrap(),
|
||||
"DELETE FROM \"secret\" WHERE \"secret_owner\" = ?1 AND \"secret_name\" = ?2",
|
||||
params![uuid.to_string(), name],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn list_user(&self, start: u32, len: u32) -> Vec<Uuid> {
|
||||
self._list_user_group("user", start, len)
|
||||
}
|
||||
|
||||
pub fn list_group(&self, start: u32, len: u32) -> Vec<Uuid> {
|
||||
self._list_user_group("group", start, len)
|
||||
}
|
||||
|
||||
pub fn get_user_info(&self, uuid: &Uuid) -> Option<UserInfo> {
|
||||
self._get_user_group_info("user", uuid)
|
||||
}
|
||||
|
||||
pub fn get_group_info(&self, uuid: &Uuid) -> Option<GroupInfo> {
|
||||
self._get_user_group_info("group", uuid)
|
||||
}
|
||||
|
||||
pub fn insert_user_info(&self, user_info: UserInfo) -> Result<(), Error> {
|
||||
let mut lock = self.0.lock().unwrap();
|
||||
execute(
|
||||
&mut *lock,
|
||||
"INSERT INTO \"user\"(\"user_uuid\", \"user_name\", \"user_host_uid\", \"user_info\")
|
||||
VALUES(?1, ?2, ?3, ?4)",
|
||||
params![
|
||||
user_info.uuid.to_string(),
|
||||
user_info.username,
|
||||
user_info
|
||||
.host_uid
|
||||
.ok_or_else(|| Error::make_internal("host_uid not specified"))?,
|
||||
serde_json::to_string(&user_info).map_err(Error::make_internal)?,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn delete_user(&self, uuid: &Uuid) -> Result<(), Error> {
|
||||
let mut lock = self.0.lock().unwrap();
|
||||
execute(
|
||||
&mut *lock,
|
||||
"DELETE FROM \"user\" WHERE \"user_uuid\" = ?1",
|
||||
params![uuid.to_string()],
|
||||
)?;
|
||||
execute(
|
||||
&mut *lock,
|
||||
"DELETE FROM \"secret\" WHERE \"secret_owner\" = ?1",
|
||||
params![uuid.to_string()],
|
||||
)?;
|
||||
execute(
|
||||
&mut *lock,
|
||||
"DELETE FROM \"group_membership\" WHERE \"group_membership_user\" = ?1",
|
||||
params![uuid.to_string()],
|
||||
)?;
|
||||
execute(
|
||||
&mut *lock,
|
||||
"DELETE FROM \"user_auth\" WHERE \"user_auth_user\" = ?1",
|
||||
params![uuid.to_string()],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn insert_group_info(&self, group_info: GroupInfo) -> Result<(), Error> {
|
||||
let mut lock = self.0.lock().unwrap();
|
||||
execute(
|
||||
&mut *lock,
|
||||
"INSERT INTO \"group\"(\"group_uuid\", \"group_name\", \"group_host_gid\", \"group_info\")
|
||||
VALUES(?1, ?2, ?3, ?4)",
|
||||
params![
|
||||
group_info.uuid.to_string(),
|
||||
group_info.groupname,
|
||||
group_info
|
||||
.host_gid
|
||||
.ok_or_else(|| Error::make_internal("host_gid not specified"))?,
|
||||
serde_json::to_string(&group_info).map_err(Error::make_internal)?,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn delete_group(&self, uuid: &Uuid) -> Result<(), Error> {
|
||||
let mut lock = self.0.lock().unwrap();
|
||||
execute(
|
||||
&mut *lock,
|
||||
"DELETE FROM \"group\" WHERE \"group_uuid\" = ?1",
|
||||
params![uuid.to_string()],
|
||||
)?;
|
||||
execute(
|
||||
&mut *lock,
|
||||
"DELETE FROM \"group_membership\" WHERE \"group_membership_group\" = ?1",
|
||||
params![uuid.to_string()],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_user_auth_methods(&self, uuid: &Uuid) -> Result<Vec<UserAuthMethodRecord>, Error> {
|
||||
let lock = self.0.lock().unwrap();
|
||||
let mut stmt = lock
|
||||
.prepare_cached("SELECT * FROM \"user_auth\" WHERE \"user_auth_user\" = ?1")
|
||||
.unwrap();
|
||||
let mut rows = stmt
|
||||
.query(params![uuid.to_string()])
|
||||
.map_err(Error::make_internal)?;
|
||||
let mut all = Vec::new();
|
||||
while let Ok(Some(i)) = rows.next() {
|
||||
let name = i.get("user_auth_method").map_err(Error::make_internal)?;
|
||||
let flags = i.get("user_auth_flags").map_err(Error::make_internal)?;
|
||||
all.push(UserAuthMethodRecord {
|
||||
name,
|
||||
flags: UserAuthMethodFlags::from_bits_retain(flags),
|
||||
});
|
||||
}
|
||||
Ok(all)
|
||||
}
|
||||
|
||||
pub fn insert_user_auth_method(
|
||||
&self,
|
||||
uuid: &Uuid,
|
||||
method: &str,
|
||||
flags: UserAuthMethodFlags,
|
||||
) -> Result<(), Error> {
|
||||
let mut lock = self.0.lock().unwrap();
|
||||
execute(
|
||||
&mut *lock,
|
||||
"INSERT INTO \"user_auth\"(\"user_auth_user\", \"user_auth_method\", \"user_auth_flags\")
|
||||
VALUES(?1, ?2, ?3)",
|
||||
params![uuid.to_string(), method, flags.bits()],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn find_user_by_name(&self, username: &str) -> Option<Uuid> {
|
||||
self._find_uuid_by("user", "user_name", &username)
|
||||
}
|
||||
|
||||
pub fn find_group_by_name(&self, groupname: &str) -> Option<Uuid> {
|
||||
self._find_uuid_by("group", "group_name", &groupname)
|
||||
}
|
||||
|
||||
pub fn find_user_by_host_uid(&self, host_uid: HostUid) -> Option<Uuid> {
|
||||
self._find_uuid_by("user", "user_host_uid", &host_uid.to_string())
|
||||
}
|
||||
|
||||
pub fn find_group_by_host_gid(&self, host_gid: HostGid) -> Option<Uuid> {
|
||||
self._find_uuid_by("group", "group_host_gid", &host_gid.to_string())
|
||||
}
|
||||
|
||||
pub fn is_first_run(&self) -> bool {
|
||||
!self._kv_get("initialized").is_some()
|
||||
}
|
||||
|
||||
pub fn unset_first_run(&self) -> Result<(), Error> {
|
||||
self._kv_set("initialized", "1")
|
||||
.map_err(Error::make_internal)
|
||||
}
|
||||
|
||||
fn _list_user_group(&self, table: &'static str, start: u32, len: u32) -> Vec<Uuid> {
|
||||
let lock = self.0.lock().unwrap();
|
||||
let stmt = lock.prepare_cached(&format!(
|
||||
"SELECT \"{table}_uuid\" FROM \"{table}\" ORDER BY \"{table}_uuid\" LIMIT ?1 OFFSET ?2"
|
||||
));
|
||||
let Ok(mut stmt) = stmt else {
|
||||
return Vec::new();
|
||||
};
|
||||
stmt.query_map(params![len, start], |x| {
|
||||
x.get::<_, String>(&format!("{table}_uuid")[..])?
|
||||
.parse::<Uuid>()
|
||||
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))
|
||||
})
|
||||
.map(|x| x.filter_map(|y| y.ok()).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn _get_user_group_info<T: DeserializeOwned>(
|
||||
&self,
|
||||
table: &'static str,
|
||||
uuid: &Uuid,
|
||||
) -> Option<T> {
|
||||
let lock = self.0.lock().unwrap();
|
||||
let mut stmt = lock
|
||||
.prepare_cached(&format!(
|
||||
"SELECT * FROM \"{table}\" WHERE \"{table}_uuid\" = ?1"
|
||||
))
|
||||
.ok()?;
|
||||
stmt.query_one(params![uuid.to_string()], |rows| {
|
||||
serde_json::from_str(&rows.get::<_, String>(&format!("{table}_info")[..])?)
|
||||
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn _find_uuid_by<T: rusqlite::types::ToSql>(
|
||||
&self,
|
||||
table: &'static str,
|
||||
column: &'static str,
|
||||
val: &T,
|
||||
) -> Option<Uuid> {
|
||||
let lock = self.0.lock().unwrap();
|
||||
let mut stmt = lock
|
||||
.prepare_cached(&format!(
|
||||
"SELECT \"{table}_uuid\" FROM \"{table}\" WHERE \"{column}\" = ?1"
|
||||
))
|
||||
.ok()?;
|
||||
stmt.query_one(params![val], |rows| {
|
||||
rows.get::<_, String>(&format!("{table}_uuid")[..])
|
||||
})
|
||||
.ok()?
|
||||
.parse::<Uuid>()
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn _kv_get(&self, key: &str) -> Option<String> {
|
||||
let lock = self.0.lock().unwrap();
|
||||
let mut stmt = lock
|
||||
.prepare_cached(
|
||||
"SELECT \"kv_store_value\" FROM \"kv_store\" WHERE \"kv_store_key\" = ?1",
|
||||
)
|
||||
.ok()?;
|
||||
stmt.query_one(params![key], |rows| rows.get("kv_store_value"))
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn _kv_set(&self, key: &str, val: &str) -> rusqlite::Result<()> {
|
||||
let lock = self.0.lock().unwrap();
|
||||
let mut stmt_get = lock.prepare_cached(
|
||||
"SELECT \"kv_store_key\" FROM \"kv_store\" WHERE \"kv_store_key\" = ?1",
|
||||
)?;
|
||||
let mut stmt_set = lock.prepare_cached(
|
||||
"UPDATE \"kv_store\" SET \"kv_store_value\" = ?1 WHERE \"kv_store_key\" = ?2",
|
||||
)?;
|
||||
let mut stmt_insert = lock.prepare_cached(
|
||||
"INSERT INTO \"kv_store\"(\"kv_store_key\", \"kv_store_value\") VALUES(?1, ?2)",
|
||||
)?;
|
||||
if stmt_get.query_one(params![key], |_| Ok(())).is_ok() {
|
||||
stmt_set.execute(params![val, key])?;
|
||||
} else {
|
||||
stmt_insert.execute(params![key, val])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn execute(
|
||||
conn: &mut rusqlite::Connection,
|
||||
sql: &str,
|
||||
params: &[&dyn rusqlite::ToSql],
|
||||
) -> Result<(), Error> {
|
||||
let mut stmt = conn.prepare_cached(sql).map_err(Error::make_internal)?;
|
||||
stmt.execute(params).map_err(Error::make_internal)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
mod api;
|
||||
mod auth;
|
||||
mod init_file;
|
||||
mod local;
|
||||
mod secret;
|
||||
mod util;
|
||||
|
||||
use crate::{local::LocalDb, secret::ResidentKeys};
|
||||
use clap::Parser;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
const DEFAULT_LIBEXEC_DIR: &str = match std::option_env!("DEFAULT_LIBEXEC_DIR") {
|
||||
Some(x) => x,
|
||||
None => "/usr/libexec/accountd",
|
||||
};
|
||||
const DEFAULT_DATA_DIR: &str = match std::option_env!("DEFAULT_DATA_DIR") {
|
||||
Some(x) => x,
|
||||
None => "/var/lib/accountd",
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AppState {
|
||||
/// The `accountd` dedicated libexec directory, e.g. `/usr/libexec/accountd`.
|
||||
libexec_dir: PathBuf,
|
||||
|
||||
/// The `accountd` dedicated data directory, e.g. `/var/lib/accountd`.
|
||||
data_dir: PathBuf,
|
||||
|
||||
/// Local database.
|
||||
local_db: LocalDb,
|
||||
|
||||
/// Resident keys.
|
||||
resident_keys: ResidentKeys,
|
||||
}
|
||||
impl AppState {
|
||||
fn auth_methods(&self) -> Box<dyn Iterator<Item = auth::AuthExec>> {
|
||||
let Ok(tree_dir) = util::fs::TreeDir::open(&self.libexec_dir) else {
|
||||
return Box::new(std::iter::empty());
|
||||
};
|
||||
Box::new(
|
||||
tree_dir
|
||||
.filter(|x| x.file_name().to_string_lossy().starts_with("auth_"))
|
||||
.map(|x| auth::AuthExec::from_path_buf(x.path())),
|
||||
)
|
||||
}
|
||||
|
||||
fn auth_method(&self, name: &str) -> Option<auth::AuthExec> {
|
||||
self.auth_methods()
|
||||
.find(|x| matches!(x.name().as_deref(), Ok(x) if x == name))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
struct Cli {
|
||||
/// Specify libexec directory
|
||||
#[arg(long)]
|
||||
libexec_dir: Option<PathBuf>,
|
||||
|
||||
/// Specify data directory
|
||||
#[arg(long)]
|
||||
data_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
let cli = Cli::parse();
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let libexec_dir = cli
|
||||
.libexec_dir
|
||||
.unwrap_or_else(|| DEFAULT_LIBEXEC_DIR.into());
|
||||
let data_dir = cli.data_dir.unwrap_or_else(|| DEFAULT_DATA_DIR.into());
|
||||
_ = std::fs::create_dir_all(&data_dir);
|
||||
|
||||
let local_db_path = data_dir.join("local.db");
|
||||
let local_db = match LocalDb::open(&local_db_path) {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
tracing::error!("failed to open local database: {e}",);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let app_state = Arc::new(AppState {
|
||||
libexec_dir,
|
||||
data_dir,
|
||||
local_db,
|
||||
resident_keys: ResidentKeys::new(),
|
||||
});
|
||||
|
||||
if app_state.local_db.is_first_run() {
|
||||
if init_file::run(app_state.clone()).await.is_err() {
|
||||
std::process::exit(1);
|
||||
}
|
||||
if let Err(e) = app_state.local_db.unset_first_run() {
|
||||
tracing::error!("failed to unset first run: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = api::launch_server(app_state).await {
|
||||
tracing::error!("failed to launch api server: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
while tokio::signal::ctrl_c().await.is_err() {}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use crate::util::time::timestamp_s;
|
||||
use rustc_hash::FxHashMap;
|
||||
use semios_account::secret::MasterKeyProvision;
|
||||
use std::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ResidentKeys(Mutex<FxHashMap<Uuid, MasterKeyProvision>>);
|
||||
impl ResidentKeys {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn set(&self, uuid: Uuid, provision: MasterKeyProvision) {
|
||||
self.0.lock().unwrap().insert(uuid, provision);
|
||||
}
|
||||
|
||||
pub fn get(&self, uuid: &Uuid) -> Option<MasterKeyProvision> {
|
||||
let mut lock = self.0.lock().unwrap();
|
||||
let provision = lock.get(uuid)?;
|
||||
if provision.expiration_time < timestamp_s() {
|
||||
lock.remove(uuid);
|
||||
return None;
|
||||
}
|
||||
Some(provision.clone())
|
||||
}
|
||||
|
||||
pub fn remove(&self, uuid: &Uuid) {
|
||||
self.0.lock().unwrap().remove(uuid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use std::{
|
||||
fs::{DirEntry, ReadDir},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TreeDir(Vec<ReadDir>);
|
||||
impl TreeDir {
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
|
||||
Ok(Self(vec![std::fs::read_dir(path)?]))
|
||||
}
|
||||
}
|
||||
impl Iterator for TreeDir {
|
||||
type Item = DirEntry;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
while let Some(current_dir) = self.0.last_mut() {
|
||||
match current_dir.next() {
|
||||
Some(Ok(entry)) => {
|
||||
if let Ok(file_type) = entry.file_type() {
|
||||
if file_type.is_dir() {
|
||||
if let Ok(sub_dir) = std::fs::read_dir(entry.path()) {
|
||||
self.0.push(sub_dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Some(entry);
|
||||
}
|
||||
Some(Err(_)) => {
|
||||
continue;
|
||||
}
|
||||
None => {
|
||||
self.0.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//! Local IPC implementation -- Unix-like Operating Systems.
|
||||
|
||||
use anyhow::anyhow;
|
||||
use cfg_if::cfg_if;
|
||||
use semios_account::{
|
||||
protocol::{MAX_MESSAGE_LEN, Uri},
|
||||
record::HostUid,
|
||||
};
|
||||
use std::os::unix::net::{SocketAddr, UnixListener as StdUnixListener};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::{UnixListener, UnixStream},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Listener(UnixListener);
|
||||
impl Listener {
|
||||
pub async fn new(uri: &str) -> anyhow::Result<Self> {
|
||||
let uri: Uri = uri.parse()?;
|
||||
let sockaddr = match uri {
|
||||
Uri::Unix(path) => {
|
||||
if path.exists() && tokio::net::UnixStream::connect(&path).await.is_err() {
|
||||
std::fs::remove_file(&path)?;
|
||||
}
|
||||
SocketAddr::from_pathname(path)?
|
||||
}
|
||||
Uri::UnixAbstract(_name) => {
|
||||
cfg_if! {
|
||||
if #[cfg(target_os = "linux")] {
|
||||
use std::os::linux::net::SocketAddrExt;
|
||||
SocketAddr::from_abstract_name(_name.as_bytes())?
|
||||
} else {
|
||||
return Err(anyhow!("abstract socket names are only supported on Linux"));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => return Err(anyhow!("only \"unix://\" ipc is supported on unix")),
|
||||
};
|
||||
let listener = StdUnixListener::bind_addr(&sockaddr)?;
|
||||
listener.set_nonblocking(true)?;
|
||||
Ok(Self(UnixListener::from_std(listener)?))
|
||||
}
|
||||
|
||||
pub async fn accept(&self) -> anyhow::Result<Connection> {
|
||||
Ok(Connection(self.0.accept().await?.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Connection(UnixStream);
|
||||
impl Connection {
|
||||
pub async fn recv(&mut self, data: &mut Vec<u8>) -> anyhow::Result<()> {
|
||||
let len = self.0.read_u32_le().await? as usize;
|
||||
if len > MAX_MESSAGE_LEN {
|
||||
return Err(anyhow!("message too large: {len} bytes"));
|
||||
}
|
||||
data.resize(len, 0);
|
||||
self.0.read_exact(data).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send(&mut self, data: &[u8]) -> anyhow::Result<()> {
|
||||
if data.len() > MAX_MESSAGE_LEN {
|
||||
return Err(anyhow!("message too large: {} bytes", data.len()));
|
||||
}
|
||||
self.0.write_u32_le(data.len() as u32).await?;
|
||||
self.0.write_all(data).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn peer_host_uid(&self) -> Option<HostUid> {
|
||||
Some(self.0.peer_cred().ok()?.uid())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
pub mod fs;
|
||||
pub mod time;
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(target_family = "unix")] {
|
||||
#[path = "ipc_unix.rs"]
|
||||
pub mod ipc;
|
||||
} else {
|
||||
std::compile_error!("target not supported");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
use std::time::SystemTime;
|
||||
|
||||
macro_rules! timestamp {
|
||||
($f:ident) => {{
|
||||
let now = SystemTime::now();
|
||||
match now.duration_since(SystemTime::UNIX_EPOCH) {
|
||||
Ok(x) => x.$f() as i64,
|
||||
Err(_) => -(SystemTime::UNIX_EPOCH.duration_since(now).unwrap().$f() as i64),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
pub fn timestamp_s() -> i64 {
|
||||
timestamp!(as_secs)
|
||||
}
|
||||
Reference in New Issue
Block a user