Feature/lxc container runtime (#2514)

* wip: static-server errors

* wip: fix wifi

* wip: Fix the service_effects

* wip: Fix cors in the middleware

* wip(chore): Auth clean up the lint.

* wip(fix): Vhost

* wip: continue manager refactor

Co-authored-by: J H <Blu-J@users.noreply.github.com>

* wip: service manager refactor

* wip: Some fixes

* wip(fix): Fix the lib.rs

* wip

* wip(fix): Logs

* wip: bins

* wip(innspect): Add in the inspect

* wip: config

* wip(fix): Diagnostic

* wip(fix): Dependencies

* wip: context

* wip(fix) Sorta auth

* wip: warnings

* wip(fix): registry/admin

* wip(fix) marketplace

* wip(fix) Some more converted and fixed with the linter and config

* wip: Working on the static server

* wip(fix)static server

* wip: Remove some asynnc

* wip: Something about the request and regular rpc

* wip: gut install

Co-authored-by: J H <Blu-J@users.noreply.github.com>

* wip: Convert the static server into the new system

* wip delete file

* test

* wip(fix) vhost does not need the with safe defaults

* wip: Adding in the wifi

* wip: Fix the developer and the verify

* wip: new install flow

Co-authored-by: J H <Blu-J@users.noreply.github.com>

* fix middleware

* wip

* wip: Fix the auth

* wip

* continue service refactor

* feature: Service get_config

* feat: Action

* wip: Fighting the great fight against the borrow checker

* wip: Remove an error in a file that I just need to deel with later

* chore: Add in some more lifetime stuff to the services

* wip: Install fix on lifetime

* cleanup

* wip: Deal with the borrow later

* more cleanup

* resolve borrowchecker errors

* wip(feat): add in the handler for the socket, for now

* wip(feat): Update the service_effect_handler::action

* chore: Add in the changes to make sure the from_service goes to context

* chore: Change the

* refactor service map

* fix references to service map

* fill out restore

* wip: Before I work on the store stuff

* fix backup module

* handle some warnings

* feat: add in the ui components on the rust side

* feature: Update the procedures

* chore: Update the js side of the main and a few of the others

* chore: Update the rpc listener to match the persistant container

* wip: Working on updating some things to have a better name

* wip(feat): Try and get the rpc to return the correct shape?

* lxc wip

* wip(feat): Try and get the rpc to return the correct shape?

* build for container runtime wip

* remove container-init

* fix build

* fix error

* chore: Update to work I suppose

* lxc wip

* remove docker module and feature

* download alpine squashfs automatically

* overlays effect

Co-authored-by: Jade <Blu-J@users.noreply.github.com>

* chore: Add the overlay effect

* feat: Add the mounter in the main

* chore: Convert to use the mounts, still need to work with the sandbox

* install fixes

* fix ssl

* fixes from testing

* implement tmpfile for upload

* wip

* misc fixes

* cleanup

* cleanup

* better progress reporting

* progress for sideload

* return real guid

* add devmode script

* fix lxc rootfs path

* fix percentage bar

* fix progress bar styling

* fix build for unstable

* tweaks

* label progress

* tweaks

* update progress more often

* make symlink in rpc_client

* make socket dir

* fix parent path

* add start-cli to container

* add echo and gitInfo commands

* wip: Add the init + errors

* chore: Add in the exit effect for the system

* chore: Change the type to null for failure to parse

* move sigterm timeout to stopping status

* update order

* chore: Update the return type

* remove dbg

* change the map error

* chore: Update the thing to capture id

* chore add some life changes

* chore: Update the loging

* chore: Update the package to run module

* us From for RpcError

* chore: Update to use import instead

* chore: update

* chore: Use require for the backup

* fix a default

* update the type that is wrong

* chore: Update the type of the manifest

* chore: Update to make null

* only symlink if not exists

* get rid of double result

* better debug info for ErrorCollection

* chore: Update effects

* chore: fix

* mount assets and volumes

* add exec instead of spawn

* fix mounting in image

* fix overlay mounts

Co-authored-by: Jade <Blu-J@users.noreply.github.com>

* misc fixes

* feat: Fix two

* fix: systemForEmbassy main

* chore: Fix small part of main loop

* chore: Modify the bundle

* merge

* fixMain loop"

* move tsc to makefile

* chore: Update the return types of the health check

* fix client

* chore: Convert the todo to use tsmatches

* add in the fixes for the seen and create the hack to allow demo

* chore: Update to include the systemForStartOs

* chore UPdate to the latest types from the expected outout

* fixes

* fix typo

* Don't emit if failure on tsc

* wip

Co-authored-by: Jade <Blu-J@users.noreply.github.com>

* add s9pk api

* add inspection

* add inspect manifest

* newline after display serializable

* fix squashfs in image name

* edit manifest

Co-authored-by: Jade <Blu-J@users.noreply.github.com>

* wait for response on repl

* ignore sig for now

* ignore sig for now

* re-enable sig verification

* fix

* wip

* env and chroot

* add profiling logs

* set uid & gid in squashfs to 100000

* set uid of sqfs to 100000

* fix mksquashfs args

* add env to compat

* fix

* re-add docker feature flag

* fix docker output format being stupid

* here be dragons

* chore: Add in the cross compiling for something

* fix npm link

* extract logs from container on exit

* chore: Update for testing

* add log capture to drop trait

* chore: add in the modifications that I make

* chore: Update small things for no updates

* chore: Update the types of something

* chore: Make main not complain

* idmapped mounts

* idmapped volumes

* re-enable kiosk

* chore: Add in some logging for the new system

* bring in start-sdk

* remove avahi

* chore: Update the deps

* switch to musl

* chore: Update the version of prettier

* chore: Organize'

* chore: Update some of the headers back to the standard of fetch

* fix musl build

* fix idmapped mounts

* fix cross build

* use cross compiler for correct arch

* feat: Add in the faked ssl stuff for the effects

* @dr_bonez Did a solution here

* chore: Something that DrBonez

* chore: up

* wip: We have a working server!!!

* wip

* uninstall

* wip

* tes

---------

Co-authored-by: J H <dragondef@gmail.com>
Co-authored-by: J H <Blu-J@users.noreply.github.com>
Co-authored-by: J H <2364004+Blu-J@users.noreply.github.com>
This commit is contained in:
Aiden McClelland
2024-02-17 11:14:14 -07:00
committed by GitHub
parent 65009e2f69
commit fab13db4b4
326 changed files with 31708 additions and 13987 deletions

View File

@@ -2,32 +2,34 @@ use std::borrow::Borrow;
use std::sync::Arc;
use std::time::{Duration, Instant};
use axum::extract::Request;
use axum::response::Response;
use basic_cookies::Cookie;
use color_eyre::eyre::eyre;
use digest::Digest;
use futures::future::BoxFuture;
use futures::FutureExt;
use http::StatusCode;
use rpc_toolkit::command_helpers::prelude::RequestParts;
use rpc_toolkit::hyper::header::COOKIE;
use rpc_toolkit::hyper::http::Error as HttpError;
use rpc_toolkit::hyper::{Body, Request, Response};
use rpc_toolkit::rpc_server_helpers::{
noop4, to_response, DynMiddleware, DynMiddlewareStage2, DynMiddlewareStage3,
};
use rpc_toolkit::yajrc::RpcMethod;
use rpc_toolkit::Metadata;
use helpers::const_true;
use http::header::COOKIE;
use http::HeaderValue;
use imbl_value::InternedString;
use rpc_toolkit::yajrc::INTERNAL_ERROR;
use rpc_toolkit::{Middleware, RpcRequest, RpcResponse};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use tokio::sync::Mutex;
use crate::context::RpcContext;
use crate::{Error, ResultExt};
use crate::prelude::*;
pub const LOCAL_AUTH_COOKIE_PATH: &str = "/run/embassy/rpc.authcookie";
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct LoginRes {
pub session: InternedString,
}
pub trait AsLogoutSessionId {
fn as_logout_session_id(self) -> String;
fn as_logout_session_id(self) -> InternedString;
}
/// Will need to know when we have logged out from a route
@@ -43,13 +45,14 @@ impl HasLoggedOutSessions {
let mut sqlx_conn = ctx.secret_store.acquire().await?;
for session in logged_out_sessions {
let session = session.as_logout_session_id();
let session = &*session;
sqlx::query!(
"UPDATE session SET logged_out = CURRENT_TIMESTAMP WHERE id = $1",
session
)
.execute(sqlx_conn.as_mut())
.await?;
for socket in open_authed_websockets.remove(&session).unwrap_or_default() {
for socket in open_authed_websockets.remove(session).unwrap_or_default() {
let _ = socket.send(());
}
}
@@ -58,15 +61,21 @@ impl HasLoggedOutSessions {
}
/// Used when we need to know that we have logged in with a valid user
#[derive(Clone, Copy)]
pub struct HasValidSession(());
#[derive(Clone)]
pub struct HasValidSession(SessionType);
#[derive(Clone)]
enum SessionType {
Local,
Session(HashSessionToken),
}
impl HasValidSession {
pub async fn from_request_parts(
request_parts: &RequestParts,
pub async fn from_header(
header: Option<&HeaderValue>,
ctx: &RpcContext,
) -> Result<Self, Error> {
if let Some(cookie_header) = request_parts.headers.get(COOKIE) {
if let Some(cookie_header) = header {
let cookies = Cookie::parse(
cookie_header
.to_str()
@@ -79,7 +88,7 @@ impl HasValidSession {
}
}
if let Some(cookie) = cookies.iter().find(|c| c.get_name() == "session") {
if let Ok(s) = Self::from_session(&HashSessionToken::from_cookie(cookie), ctx).await
if let Ok(s) = Self::from_session(HashSessionToken::from_cookie(cookie), ctx).await
{
return Ok(s);
}
@@ -91,8 +100,11 @@ impl HasValidSession {
))
}
pub async fn from_session(session: &HashSessionToken, ctx: &RpcContext) -> Result<Self, Error> {
let session_hash = session.hashed();
pub async fn from_session(
session_token: HashSessionToken,
ctx: &RpcContext,
) -> Result<Self, Error> {
let session_hash = session_token.hashed();
let session = sqlx::query!("UPDATE session SET last_active = CURRENT_TIMESTAMP WHERE id = $1 AND logged_out IS NULL OR logged_out > CURRENT_TIMESTAMP", session_hash)
.execute(ctx.secret_store.acquire().await?.as_mut())
.await?;
@@ -102,13 +114,13 @@ impl HasValidSession {
crate::ErrorKind::Authorization,
));
}
Ok(Self(()))
Ok(Self(SessionType::Session(session_token)))
}
pub async fn from_local(local: &Cookie<'_>) -> Result<Self, Error> {
let token = tokio::fs::read_to_string(LOCAL_AUTH_COOKIE_PATH).await?;
if local.get_value() == &*token {
Ok(Self(()))
Ok(Self(SessionType::Local))
} else {
Err(Error::new(
eyre!("UNAUTHORIZED"),
@@ -122,27 +134,31 @@ impl HasValidSession {
/// Or when we are using internal valid authenticated service.
#[derive(Debug, Clone)]
pub struct HashSessionToken {
hashed: String,
token: String,
hashed: InternedString,
token: InternedString,
}
impl HashSessionToken {
pub fn new() -> Self {
let token = base32::encode(
base32::Alphabet::RFC4648 { padding: false },
&rand::random::<[u8; 16]>(),
)
.to_lowercase();
let hashed = Self::hash(&token);
Self { hashed, token }
Self::from_token(InternedString::intern(
base32::encode(
base32::Alphabet::RFC4648 { padding: false },
&rand::random::<[u8; 16]>(),
)
.to_lowercase(),
))
}
pub fn from_cookie(cookie: &Cookie) -> Self {
let token = cookie.get_value().to_owned();
let hashed = Self::hash(&token);
pub fn from_token(token: InternedString) -> Self {
let hashed = Self::hash(&*token);
Self { hashed, token }
}
pub fn from_request_parts(request_parts: &RequestParts) -> Result<Self, Error> {
if let Some(cookie_header) = request_parts.headers.get(COOKIE) {
pub fn from_cookie(cookie: &Cookie) -> Self {
Self::from_token(InternedString::intern(cookie.get_value()))
}
pub fn from_header(header: Option<&HeaderValue>) -> Result<Self, Error> {
if let Some(cookie_header) = header {
let cookies = Cookie::parse(
cookie_header
.to_str()
@@ -159,33 +175,30 @@ impl HashSessionToken {
))
}
pub fn header_value(&self) -> Result<http::HeaderValue, Error> {
http::HeaderValue::from_str(&format!(
"session={}; Path=/; SameSite=Lax; Expires=Fri, 31 Dec 9999 23:59:59 GMT;",
self.token
))
.with_kind(crate::ErrorKind::Unknown)
pub fn to_login_res(&self) -> LoginRes {
LoginRes {
session: self.token.clone(),
}
}
pub fn hashed(&self) -> &str {
self.hashed.as_str()
&*self.hashed
}
pub fn as_hash(self) -> String {
self.hashed
}
fn hash(token: &str) -> String {
fn hash(token: &str) -> InternedString {
let mut hasher = Sha256::new();
hasher.update(token.as_bytes());
base32::encode(
base32::Alphabet::RFC4648 { padding: false },
hasher.finalize().as_slice(),
InternedString::intern(
base32::encode(
base32::Alphabet::RFC4648 { padding: false },
hasher.finalize().as_slice(),
)
.to_lowercase(),
)
.to_lowercase()
}
}
impl AsLogoutSessionId for HashSessionToken {
fn as_logout_session_id(self) -> String {
fn as_logout_session_id(self) -> InternedString {
self.hashed
}
}
@@ -205,80 +218,120 @@ impl Ord for HashSessionToken {
self.hashed.cmp(&other.hashed)
}
}
impl Borrow<String> for HashSessionToken {
fn borrow(&self) -> &String {
&self.hashed
impl Borrow<str> for HashSessionToken {
fn borrow(&self) -> &str {
&*self.hashed
}
}
pub fn auth<M: Metadata>(ctx: RpcContext) -> DynMiddleware<M> {
let rate_limiter = Arc::new(Mutex::new((0_usize, Instant::now())));
Box::new(
move |req: &mut Request<Body>,
metadata: M|
-> BoxFuture<Result<Result<DynMiddlewareStage2, Response<Body>>, HttpError>> {
let ctx = ctx.clone();
let rate_limiter = rate_limiter.clone();
async move {
let mut header_stub = Request::new(Body::empty());
*header_stub.headers_mut() = req.headers().clone();
let m2: DynMiddlewareStage2 = Box::new(move |req, rpc_req| {
async move {
if let Err(e) = HasValidSession::from_request_parts(req, &ctx).await {
if metadata
.get(rpc_req.method.as_str(), "authenticated")
.unwrap_or(true)
{
let (res_parts, _) = Response::new(()).into_parts();
return Ok(Err(to_response(
&req.headers,
res_parts,
Err(e.into()),
|_| StatusCode::OK,
)?));
} else if rpc_req.method.as_str() == "auth.login" {
let guard = rate_limiter.lock().await;
if guard.1.elapsed() < Duration::from_secs(20) {
if guard.0 >= 3 {
let (res_parts, _) = Response::new(()).into_parts();
return Ok(Err(to_response(
&req.headers,
res_parts,
Err(Error::new(
eyre!(
"Please limit login attempts to 3 per 20 seconds."
),
crate::ErrorKind::RateLimited,
)
.into()),
|_| StatusCode::OK,
)?));
}
}
}
}
let m3: DynMiddlewareStage3 = Box::new(move |_, res| {
async move {
let mut guard = rate_limiter.lock().await;
if guard.1.elapsed() < Duration::from_secs(20) {
if res.is_err() {
guard.0 += 1;
}
} else {
guard.0 = 0;
}
guard.1 = Instant::now();
Ok(Ok(noop4()))
}
.boxed()
});
Ok(Ok(m3))
}
.boxed()
});
Ok(Ok(m2))
}
.boxed()
},
)
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Metadata {
#[serde(default = "const_true")]
authenticated: bool,
#[serde(default)]
login: bool,
#[serde(default)]
get_session: bool,
}
#[derive(Clone)]
pub struct Auth {
rate_limiter: Arc<Mutex<(usize, Instant)>>,
cookie: Option<HeaderValue>,
is_login: bool,
set_cookie: Option<HeaderValue>,
}
impl Auth {
pub fn new() -> Self {
Self {
rate_limiter: Arc::new(Mutex::new((0, Instant::now()))),
cookie: None,
is_login: false,
set_cookie: None,
}
}
}
#[async_trait::async_trait]
impl Middleware<RpcContext> for Auth {
type Metadata = Metadata;
async fn process_http_request(
&mut self,
_: &RpcContext,
request: &mut Request,
) -> Result<(), Response> {
self.cookie = request.headers_mut().get(COOKIE).cloned();
Ok(())
}
async fn process_rpc_request(
&mut self,
context: &RpcContext,
metadata: Self::Metadata,
request: &mut RpcRequest,
) -> Result<(), RpcResponse> {
if metadata.login {
self.is_login = true;
let guard = self.rate_limiter.lock().await;
if guard.1.elapsed() < Duration::from_secs(20) && guard.0 >= 3 {
return Err(RpcResponse {
id: request.id.take(),
result: Err(Error::new(
eyre!("Please limit login attempts to 3 per 20 seconds."),
crate::ErrorKind::RateLimited,
)
.into()),
});
}
} else if metadata.authenticated {
match HasValidSession::from_header(self.cookie.as_ref(), &context).await {
Err(e) => {
return Err(RpcResponse {
id: request.id.take(),
result: Err(e.into()),
})
}
Ok(HasValidSession(SessionType::Session(s))) if metadata.get_session => {
request.params["session"] = Value::String(Arc::new(s.hashed().into()));
// TODO: will this panic?
}
_ => (),
}
}
Ok(())
}
async fn process_rpc_response(&mut self, _: &RpcContext, response: &mut RpcResponse) {
if self.is_login {
let mut guard = self.rate_limiter.lock().await;
if guard.1.elapsed() < Duration::from_secs(20) {
if response.result.is_err() {
guard.0 += 1;
}
} else {
guard.0 = 0;
}
guard.1 = Instant::now();
if response.result.is_ok() {
let res = std::mem::replace(&mut response.result, Err(INTERNAL_ERROR));
response.result = async {
let res = res?;
let login_res = from_value::<LoginRes>(res.clone())?;
self.set_cookie = Some(
HeaderValue::from_str(&format!(
"session={}; Path=/; SameSite=Lax; Expires=Fri, 31 Dec 9999 23:59:59 GMT;",
login_res.session
))
.with_kind(crate::ErrorKind::Network)?,
);
Ok(res)
}
.await;
}
}
}
async fn process_http_response(&mut self, _: &RpcContext, response: &mut Response) {
if let Some(set_cookie) = self.set_cookie.take() {
response.headers_mut().insert("set-cookie", set_cookie);
}
}
}

View File

@@ -1,61 +1,63 @@
use futures::FutureExt;
use http::HeaderValue;
use hyper::header::HeaderMap;
use rpc_toolkit::hyper::http::Error as HttpError;
use rpc_toolkit::hyper::{Body, Method, Request, Response};
use rpc_toolkit::rpc_server_helpers::{
DynMiddlewareStage2, DynMiddlewareStage3, DynMiddlewareStage4,
};
use rpc_toolkit::Metadata;
use axum::extract::Request;
use axum::response::Response;
use http::{HeaderMap, HeaderValue};
use rpc_toolkit::{Empty, Middleware};
fn get_cors_headers(req: &Request<Body>) -> HeaderMap {
let mut res = HeaderMap::new();
if let Some(origin) = req.headers().get("Origin") {
res.insert("Access-Control-Allow-Origin", origin.clone());
}
if let Some(method) = req.headers().get("Access-Control-Request-Method") {
res.insert("Access-Control-Allow-Methods", method.clone());
}
if let Some(headers) = req.headers().get("Access-Control-Request-Headers") {
res.insert("Access-Control-Allow-Headers", headers.clone());
}
res.insert(
"Access-Control-Allow-Credentials",
HeaderValue::from_static("true"),
);
res
#[derive(Clone)]
pub struct Cors {
headers: HeaderMap,
}
pub async fn cors<M: Metadata>(
req: &mut Request<Body>,
_metadata: M,
) -> Result<Result<DynMiddlewareStage2, Response<Body>>, HttpError> {
let headers = get_cors_headers(req);
if req.method() == Method::OPTIONS {
Ok(Err({
let mut res = Response::new(Body::empty());
res.headers_mut().extend(headers.into_iter());
res
}))
} else {
Ok(Ok(Box::new(|_, _| {
async move {
let res: DynMiddlewareStage3 = Box::new(|_, _| {
async move {
let res: DynMiddlewareStage4 = Box::new(|res| {
async move {
res.headers_mut().extend(headers.into_iter());
Ok::<_, HttpError>(())
}
.boxed()
});
Ok::<_, HttpError>(Ok(res))
}
.boxed()
});
Ok::<_, HttpError>(Ok(res))
}
.boxed()
})))
impl Cors {
pub fn new() -> Self {
let mut headers = HeaderMap::new();
headers.insert(
"Access-Control-Allow-Credentials",
HeaderValue::from_static("true"),
);
Self { headers }
}
fn get_cors_headers(&mut self, req: &Request) {
if let Some(origin) = req.headers().get("Origin") {
self.headers
.insert("Access-Control-Allow-Origin", origin.clone());
} else {
self.headers
.insert("Access-Control-Allow-Origin", HeaderValue::from_static("*"));
}
if let Some(method) = req.headers().get("Access-Control-Request-Method") {
self.headers
.insert("Access-Control-Allow-Methods", method.clone());
} else {
self.headers.insert(
"Access-Control-Allow-Methods",
HeaderValue::from_static("*"),
);
}
if let Some(headers) = req.headers().get("Access-Control-Request-Headers") {
self.headers
.insert("Access-Control-Allow-Headers", headers.clone());
} else {
self.headers.insert(
"Access-Control-Allow-Headers",
HeaderValue::from_static("*"),
);
}
}
}
#[async_trait::async_trait]
impl<Context: Send + 'static> Middleware<Context> for Cors {
type Metadata = Empty;
async fn process_http_request(
&mut self,
_: &Context,
request: &mut Request,
) -> Result<(), Response> {
self.get_cors_headers(request);
Ok(())
}
async fn process_http_response(&mut self, _: &Context, response: &mut Response) {
response
.headers_mut()
.extend(std::mem::take(&mut self.headers))
}
}

View File

@@ -1,50 +1,54 @@
use futures::future::BoxFuture;
use futures::FutureExt;
use axum::response::Response;
use http::header::InvalidHeaderValue;
use http::HeaderValue;
use rpc_toolkit::hyper::http::Error as HttpError;
use rpc_toolkit::hyper::{Body, Request, Response};
use rpc_toolkit::rpc_server_helpers::{
noop4, DynMiddleware, DynMiddlewareStage2, DynMiddlewareStage3,
};
use rpc_toolkit::yajrc::RpcMethod;
use rpc_toolkit::Metadata;
use rpc_toolkit::{Middleware, RpcRequest, RpcResponse};
use serde::Deserialize;
use crate::context::RpcContext;
pub fn db<M: Metadata>(ctx: RpcContext) -> DynMiddleware<M> {
Box::new(
move |_: &mut Request<Body>,
metadata: M|
-> BoxFuture<Result<Result<DynMiddlewareStage2, Response<Body>>, HttpError>> {
let ctx = ctx.clone();
async move {
let m2: DynMiddlewareStage2 = Box::new(move |_req, rpc_req| {
async move {
let sync_db = metadata
.get(rpc_req.method.as_str(), "sync_db")
.unwrap_or(false);
let m3: DynMiddlewareStage3 = Box::new(move |res, _| {
async move {
if sync_db {
res.headers.append(
"X-Patch-Sequence",
HeaderValue::from_str(
&ctx.db.sequence().await.to_string(),
)?,
);
}
Ok(Ok(noop4()))
}
.boxed()
});
Ok(Ok(m3))
}
.boxed()
});
Ok(Ok(m2))
}
.boxed()
},
)
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Metadata {
#[serde(default)]
sync_db: bool,
}
#[derive(Clone)]
pub struct SyncDb {
sync_db: bool,
}
impl SyncDb {
pub fn new() -> Self {
SyncDb { sync_db: false }
}
}
#[async_trait::async_trait]
impl Middleware<RpcContext> for SyncDb {
type Metadata = Metadata;
async fn process_rpc_request(
&mut self,
_: &RpcContext,
metadata: Self::Metadata,
_: &mut RpcRequest,
) -> Result<(), RpcResponse> {
self.sync_db = metadata.sync_db;
Ok(())
}
async fn process_http_response(&mut self, context: &RpcContext, response: &mut Response) {
if let Err(e) = async {
if self.sync_db {
response.headers_mut().append(
"X-Patch-Sequence",
HeaderValue::from_str(&context.db.sequence().await.to_string())?,
);
}
Ok::<_, InvalidHeaderValue>(())
}
.await
{
tracing::error!("error writing X-Patch-Sequence header: {e}");
tracing::debug!("{e:?}");
}
}
}

View File

@@ -1,39 +1,43 @@
use futures::FutureExt;
use rpc_toolkit::hyper::http::Error as HttpError;
use rpc_toolkit::hyper::{Body, Request, Response};
use rpc_toolkit::rpc_server_helpers::{noop4, DynMiddlewareStage2, DynMiddlewareStage3};
use rpc_toolkit::yajrc::RpcMethod;
use rpc_toolkit::Metadata;
use rpc_toolkit::{Empty, Middleware, RpcRequest, RpcResponse};
use crate::Error;
use crate::context::DiagnosticContext;
use crate::prelude::*;
pub async fn diagnostic<M: Metadata>(
_req: &mut Request<Body>,
_metadata: M,
) -> Result<Result<DynMiddlewareStage2, Response<Body>>, HttpError> {
Ok(Ok(Box::new(|_, rpc_req| {
let method = rpc_req.method.as_str().to_owned();
async move {
let res: DynMiddlewareStage3 = Box::new(|_, rpc_res| {
async move {
if let Err(e) = rpc_res {
if e.code == -32601 {
*e = Error::new(
color_eyre::eyre::eyre!(
"{} is not available on the Diagnostic API",
method
),
crate::ErrorKind::DiagnosticMode,
)
.into();
}
}
Ok(Ok(noop4()))
}
.boxed()
});
Ok::<_, HttpError>(Ok(res))
}
.boxed()
})))
#[derive(Clone)]
pub struct DiagnosticMode {
method: Option<String>,
}
impl DiagnosticMode {
pub fn new() -> Self {
Self { method: None }
}
}
#[async_trait::async_trait]
impl Middleware<DiagnosticContext> for DiagnosticMode {
type Metadata = Empty;
async fn process_rpc_request(
&mut self,
_: &DiagnosticContext,
_: Self::Metadata,
request: &mut RpcRequest,
) -> Result<(), RpcResponse> {
self.method = Some(request.method.as_str().to_owned());
Ok(())
}
async fn process_rpc_response(&mut self, _: &DiagnosticContext, response: &mut RpcResponse) {
if let Err(e) = &mut response.result {
if e.code == -32601 {
*e = Error::new(
eyre!(
"{} is not available on the Diagnostic API",
self.method.as_ref().map(|s| s.as_str()).unwrap_or_default()
),
crate::ErrorKind::DiagnosticMode,
)
.into();
}
}
}
}

View File

@@ -1,115 +0,0 @@
use aes::cipher::{CipherKey, NewCipher, Nonce, StreamCipher};
use aes::Aes256Ctr;
use hmac::Hmac;
use josekit::jwk::Jwk;
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use tracing::instrument;
pub fn pbkdf2(password: impl AsRef<[u8]>, salt: impl AsRef<[u8]>) -> CipherKey<Aes256Ctr> {
let mut aeskey = CipherKey::<Aes256Ctr>::default();
pbkdf2::pbkdf2::<Hmac<Sha256>>(
password.as_ref(),
salt.as_ref(),
1000,
aeskey.as_mut_slice(),
)
.unwrap();
aeskey
}
pub fn encrypt_slice(input: impl AsRef<[u8]>, password: impl AsRef<[u8]>) -> Vec<u8> {
let prefix: [u8; 32] = rand::random();
let aeskey = pbkdf2(password.as_ref(), &prefix[16..]);
let ctr = Nonce::<Aes256Ctr>::from_slice(&prefix[..16]);
let mut aes = Aes256Ctr::new(&aeskey, ctr);
let mut res = Vec::with_capacity(32 + input.as_ref().len());
res.extend_from_slice(&prefix[..]);
res.extend_from_slice(input.as_ref());
aes.apply_keystream(&mut res[32..]);
res
}
pub fn decrypt_slice(input: impl AsRef<[u8]>, password: impl AsRef<[u8]>) -> Vec<u8> {
if input.as_ref().len() < 32 {
return Vec::new();
}
let (prefix, rest) = input.as_ref().split_at(32);
let aeskey = pbkdf2(password.as_ref(), &prefix[16..]);
let ctr = Nonce::<Aes256Ctr>::from_slice(&prefix[..16]);
let mut aes = Aes256Ctr::new(&aeskey, ctr);
let mut res = rest.to_vec();
aes.apply_keystream(&mut res);
res
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct EncryptedWire {
encrypted: serde_json::Value,
}
impl EncryptedWire {
#[instrument(skip_all)]
pub fn decrypt(self, current_secret: impl AsRef<Jwk>) -> Option<String> {
let current_secret = current_secret.as_ref();
let decrypter = match josekit::jwe::alg::ecdh_es::EcdhEsJweAlgorithm::EcdhEs
.decrypter_from_jwk(current_secret)
{
Ok(a) => a,
Err(e) => {
tracing::warn!("Could not setup awk");
tracing::debug!("{:?}", e);
return None;
}
};
let encrypted = match serde_json::to_string(&self.encrypted) {
Ok(a) => a,
Err(e) => {
tracing::warn!("Could not deserialize");
tracing::debug!("{:?}", e);
return None;
}
};
let (decoded, _) = match josekit::jwe::deserialize_json(&encrypted, &decrypter) {
Ok(a) => a,
Err(e) => {
tracing::warn!("Could not decrypt");
tracing::debug!("{:?}", e);
return None;
}
};
match String::from_utf8(decoded) {
Ok(a) => Some(a),
Err(e) => {
tracing::warn!("Could not decrypt into utf8");
tracing::debug!("{:?}", e);
return None;
}
}
}
}
/// We created this test by first making the private key, then restoring from this private key for recreatability.
/// After this the frontend then encoded an password, then we are testing that the output that we got (hand coded)
/// will be the shape we want.
#[test]
fn test_gen_awk() {
let private_key: Jwk = serde_json::from_str(
r#"{
"kty": "EC",
"crv": "P-256",
"d": "3P-MxbUJtEhdGGpBCRFXkUneGgdyz_DGZWfIAGSCHOU",
"x": "yHTDYSfjU809fkSv9MmN4wuojf5c3cnD7ZDN13n-jz4",
"y": "8Mpkn744A5KDag0DmX2YivB63srjbugYZzWc3JOpQXI"
}"#,
)
.unwrap();
let encrypted: EncryptedWire = serde_json::from_str(r#"{
"encrypted": { "protected": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiRUNESC1FUyIsImtpZCI6ImgtZnNXUVh2Tm95dmJEazM5dUNsQ0NUdWc5N3MyZnJockJnWUVBUWVtclUiLCJlcGsiOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJmRkF0LXNWYWU2aGNkdWZJeUlmVVdUd3ZvWExaTkdKRHZIWVhIckxwOXNNIiwieSI6IjFvVFN6b00teHlFZC1SLUlBaUFHdXgzS1dJZmNYZHRMQ0JHLUh6MVkzY2sifX0", "iv": "NbwvfvWOdLpZfYRIZUrkcw", "ciphertext": "Zc5Br5kYOlhPkIjQKOLMJw", "tag": "EPoch52lDuCsbUUulzZGfg" }
}"#).unwrap();
assert_eq!(
"testing12345",
&encrypted.decrypt(std::sync::Arc::new(private_key)).unwrap()
);
}

View File

@@ -2,4 +2,3 @@ pub mod auth;
pub mod cors;
pub mod db;
pub mod diagnostic;
pub mod encrypt;