Compare commits
41 Commits
@ -1,9 +1,5 @@
|
||||
/target
|
||||
*~
|
||||
|
||||
|
||||
# Added by cargo
|
||||
#
|
||||
# already existing elements were commented out
|
||||
|
||||
#/target
|
||||
bower_components/
|
||||
node_modules/
|
||||
vendor/
|
||||
|
@ -0,0 +1,10 @@
|
||||
.PHONY: all
|
||||
all: client
|
||||
|
||||
dirs:
|
||||
@mkdir -p vendor
|
||||
|
||||
client: dirs
|
||||
npm install
|
||||
bower install crypto-js
|
||||
cd node_modules/node-rsa/src && browserify -r ./NodeRSA.js:node-rsa > ../../../vendor/NodeRSA.js
|
@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "yuurei",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"asn1": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
|
||||
"integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
|
||||
"requires": {
|
||||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"crypto-js": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.0.0.tgz",
|
||||
"integrity": "sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg=="
|
||||
},
|
||||
"jsencrypt": {
|
||||
"version": "3.0.0-rc.1",
|
||||
"resolved": "https://registry.npmjs.org/jsencrypt/-/jsencrypt-3.0.0-rc.1.tgz",
|
||||
"integrity": "sha512-gcvGaqerlUJy1Kq6tNgPYteVEoWNemu+9hBe2CdsCIz4rVcwjoTQ72iD1W76/PRMlnkzG0yVh7nwOOMOOUfKmg=="
|
||||
},
|
||||
"node-rsa": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz",
|
||||
"integrity": "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==",
|
||||
"requires": {
|
||||
"asn1": "^0.2.4"
|
||||
}
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "yuurei",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"crypto-js": "^4.0.0",
|
||||
"jsencrypt": "^3.0.0-rc.1",
|
||||
"node-rsa": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@git.flanchan.moe:flanchan/yuurei.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "GPL-3.0-or-later"
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
use crate::mnemonic::MnemonicSaltKind;
|
||||
use crate::{version, version::Version};
|
||||
|
||||
/// Default anonymous name
|
||||
pub const ANON_NAME: &'static str = "名無し";
|
||||
/// Max length of `name` and `email` feilds in posts.
|
||||
pub const POST_ID_MAX_LEN: usize = 32;
|
||||
|
||||
/// The salt to use for tripcode hashes
|
||||
pub const TRIPCODE_SALT: khash::salt::Salt = khash::salt::Salt::internal();
|
||||
/// The tripcode algorithm to use
|
||||
pub const TRIPCODE_ALGO: khash::ctx::Algorithm = khash::ctx::Algorithm::Sha256Truncated;
|
||||
|
||||
/// What timezone to represent data in.
|
||||
pub type Timezone = chrono::offset::Utc;
|
||||
|
||||
/// Default post expiry duration (120 days)
|
||||
///
|
||||
/// # Notes
|
||||
/// This is specified as a std `Duration`, the chrono `Duration` calculated from the post's offset must be converted into this type to be used for anything.
|
||||
/// This conversion can fail.
|
||||
/// If it does fail, then the post should just be treated as expired.
|
||||
pub const POST_EXPIRE: tokio::time::Duration = tokio::time::Duration::from_secs(
|
||||
60 // Minute
|
||||
* 60 // Hour
|
||||
* 24 // Day
|
||||
* 120
|
||||
);
|
||||
|
||||
/// Max size of encrypted body
|
||||
pub const POST_BODY_MAX_SIZE: usize = (1024 * 1024) * 20; // 20MB
|
||||
|
||||
/// What encoded PEM formats are recognised
|
||||
pub const RECOGNISABLE_PEM_ENCODES: &'static [&'static str] = &[
|
||||
"PUBLIC KEY",
|
||||
"RSA PUBLIC KEY",
|
||||
];
|
||||
|
||||
/// Size of the menmonic salt
|
||||
pub const MNEMONIC_SALT_SIZE: usize = 16;
|
||||
|
||||
/// Mnemonic salt to use
|
||||
pub const MNEMONIC_SALT: MnemonicSaltKind = MnemonicSaltKind::Random;
|
||||
|
||||
/// Max state image read size
|
||||
///
|
||||
/// Set to `0` for unlimited.
|
||||
pub const MAX_IMAGE_READ_SIZE: usize = (1024 * 1024 * 1024) * 3; // 3GB
|
||||
|
||||
/// The size of state stream message passing buffers.
|
||||
///
|
||||
/// Must be 1 or larger.
|
||||
pub const STATE_STREAM_BUFFER_SIZE: usize = 1;
|
||||
static_assert!(STATE_STREAM_BUFFER_SIZE > 0);
|
||||
|
||||
/// The current version of the file formats for saving state
|
||||
///
|
||||
/// TODO: Create `Version` type that takes from `env!(version)` at compile time.
|
||||
pub const VERSION: Version = version!(0);
|
||||
|
||||
/// Max size of a HTTP request body to process.
|
||||
pub const MAX_CONTENT_LENGTH: u64 = (1024 * 1024) * 15; //15MB
|
@ -0,0 +1,583 @@
|
||||
use super::*;
|
||||
use std::marker::PhantomData;
|
||||
use std::{fmt,error};
|
||||
use std::ops::Deref;
|
||||
use std::borrow::{Borrow, ToOwned};
|
||||
|
||||
/// A spec to validate the formatting of a string for.
|
||||
pub trait FormatSpec
|
||||
{
|
||||
type Error: Into<eyre::Report>;
|
||||
fn validate(s: &str) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
|
||||
/// A strongly validated string slice
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[repr(transparent)]
|
||||
pub struct FormattedStr<F: FormatSpec + ?Sized>(PhantomData<*const F>, str);
|
||||
|
||||
impl<F: FormatSpec + ?Sized> std::marker::Unpin for FormattedStr<F>{}
|
||||
unsafe impl<F: FormatSpec + ?Sized> std::marker::Send for FormattedStr<F>{}
|
||||
unsafe impl<F: FormatSpec + ?Sized> std::marker::Sync for FormattedStr<F>{}
|
||||
|
||||
/// A strongly validated string
|
||||
// TODO: How to perform validation on deserialise? Custom `Deserialize` impl? might have to.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct FormattedString<F: FormatSpec + ?Sized>(String, PhantomData<F>);
|
||||
|
||||
impl<'a, F> fmt::Display for &'a FormattedStr<F>
|
||||
where F: ?Sized + FormatSpec
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
||||
{
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Deserialising
|
||||
const _:() = {
|
||||
use serde::{
|
||||
de::Visitor,
|
||||
de::Error,
|
||||
de,
|
||||
Serialize,
|
||||
ser::Serializer,
|
||||
};
|
||||
|
||||
impl<F: FormatSpec + ?Sized> Serialize for FormattedStr<F>
|
||||
{
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.1)
|
||||
}
|
||||
}
|
||||
impl<F: FormatSpec + ?Sized> Serialize for FormattedString<F>
|
||||
{
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct FormattedStringVisitor<F: FormatSpec + ?Sized>(PhantomData<F>);
|
||||
|
||||
impl<'de, F: FormatSpec + ?Sized> Visitor<'de> for FormattedStringVisitor<F>
|
||||
{
|
||||
type Value = FormattedString<F>;
|
||||
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
||||
{
|
||||
write!(f, "a string satisfying the requirements of {}", std::any::type_name::<F>())
|
||||
}
|
||||
fn visit_str<E: Error>(self, s: &str) -> Result<Self::Value, E>
|
||||
{
|
||||
FormattedStr::<F>::new(s).map_err(|x| E::custom(x.into())).map(ToOwned::to_owned)
|
||||
}
|
||||
fn visit_string<E: Error>(self, s: String) -> Result<Self::Value, E>
|
||||
{
|
||||
FormattedString::<F>::new(s).map_err(|x| E::custom(x.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, F: FormatSpec + ?Sized> de::Deserialize<'de> for FormattedString<F> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
{
|
||||
let vis = FormattedStringVisitor::<F>(PhantomData);
|
||||
deserializer.deserialize_string(vis)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
impl<F: FormatSpec + ?Sized> FormattedStr<F>
|
||||
{
|
||||
/// Create a new instance without validating the input.
|
||||
///
|
||||
/// # Safety
|
||||
/// You must be sure the input is of a valid state to `F`.
|
||||
#[inline(always)] pub unsafe fn new_unchecked<'a>(s: &'a str) -> &'a Self
|
||||
{
|
||||
std::mem::transmute(s)
|
||||
}
|
||||
|
||||
/// Create and validate a the format of a new instance.
|
||||
pub fn new<'a>(s: &'a str) -> Result<&'a Self, F::Error>
|
||||
{
|
||||
F::validate(s).map(move |_| unsafe {Self::new_unchecked(s)})
|
||||
}
|
||||
|
||||
/// Get the inner str
|
||||
#[inline] pub fn as_str<'a>(&'a self) -> &'a str
|
||||
{
|
||||
&self.1
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FormatSpec + ?Sized> FormattedString<F>
|
||||
{
|
||||
/// Create a new instance without validating the input.
|
||||
///
|
||||
/// # Safety
|
||||
/// You must be sure the input is of a valid state to `F`.
|
||||
#[inline(always)] pub unsafe fn new_unchecked(s: String) -> Self
|
||||
{
|
||||
std::mem::transmute(s)
|
||||
}
|
||||
|
||||
/// Create and validate a the format of a new instance.
|
||||
pub fn new(s: String) -> Result<Self, F::Error>
|
||||
{
|
||||
F::validate(&s)
|
||||
.map(move |_| unsafe {Self::new_unchecked(s)})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl<F: FormatSpec + ?Sized> FormattedString<F>
|
||||
{
|
||||
/// As a formatted str
|
||||
#[inline] pub fn as_ref<'a>(&'a self) -> &'a FormattedStr<F>
|
||||
{
|
||||
unsafe { FormattedStr::new_unchecked(&self.0) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FormatSpec + ?Sized> AsRef<str> for FormattedStr<F>
|
||||
{
|
||||
#[inline] fn as_ref(&self) -> &str
|
||||
{
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FormatSpec + ?Sized> Borrow<FormattedStr<F>> for FormattedString<F>
|
||||
{
|
||||
#[inline] fn borrow(&self) -> &FormattedStr<F> {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FormatSpec + ?Sized> ToOwned for FormattedStr<F>
|
||||
{
|
||||
type Owned = FormattedString<F>;
|
||||
#[inline] fn to_owned(&self) -> Self::Owned {
|
||||
FormattedString(String::from(&self.1), PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FormatSpec + ?Sized> AsRef<FormattedStr<F>> for FormattedString<F>
|
||||
{
|
||||
#[inline] fn as_ref(&self) -> &FormattedStr<F> {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FormatSpec + ?Sized> Deref for FormattedString<F>
|
||||
{
|
||||
type Target = FormattedStr<F>;
|
||||
#[inline] fn deref(&self) -> &Self::Target {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a,F: FormatSpec + ?Sized> From<&'a FormattedStr<F>> for &'a str
|
||||
{
|
||||
#[inline] fn from(from: &'a FormattedStr<F>) -> Self
|
||||
{
|
||||
&from.1
|
||||
}
|
||||
}
|
||||
impl<'a,F: FormatSpec + ?Sized> TryFrom<&'a str> for &'a FormattedStr<F>
|
||||
{
|
||||
type Error = F::Error;
|
||||
|
||||
#[inline] fn try_from(from: &'a str) -> Result<Self, Self::Error>
|
||||
{
|
||||
FormattedStr::new(from)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, F: FormatSpec + ?Sized> From<FormattedString<F>> for String
|
||||
{
|
||||
#[inline] fn from(from: FormattedString<F>) -> Self
|
||||
{
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: FormatSpec + ?Sized> std::str::FromStr for FormattedString<F>
|
||||
{
|
||||
type Err = F::Error;
|
||||
#[inline] fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
F::validate(s)?;
|
||||
Ok(Self(s.to_owned(), PhantomData))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, F: FormatSpec + ?Sized> TryFrom<String> for FormattedString<F>
|
||||
{
|
||||
type Error = F::Error;
|
||||
|
||||
fn try_from(from: String) -> Result<Self, Self::Error>
|
||||
{
|
||||
Self::new(from)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub mod formats
|
||||
{
|
||||
use super::*;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature="nightly")] {
|
||||
/// A format valid for any string
|
||||
pub type AnyFormat = !;
|
||||
} else {
|
||||
/// A format valid for any string
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum AnyFormat{}
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatSpec for AnyFormat
|
||||
{
|
||||
type Error = std::convert::Infallible;
|
||||
|
||||
#[inline(always)] fn validate(_: &str) -> Result<(), Self::Error>
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A format spec that must satisfy both these format specs in order
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct BothFormat<T, U = AnyFormat>(PhantomData<(T, U)>)
|
||||
where T: FormatSpec, U: FormatSpec;
|
||||
|
||||
/// A formatting error coming from an instance of `BothFormat<T,U>`.
|
||||
///
|
||||
/// This type's `T` and `U` correspond to the `Error` type of the `BothFormat`'s `T` and `U`.
|
||||
#[derive(Debug)]
|
||||
pub enum MultiFormatError<T,U>
|
||||
{
|
||||
First(T),
|
||||
Second(U),
|
||||
}
|
||||
|
||||
impl<T, U> FormatSpec for BothFormat<T,U>
|
||||
where T: FormatSpec,
|
||||
U: FormatSpec,
|
||||
T::Error : error::Error + 'static + Send + Sync,
|
||||
U::Error : error::Error + 'static + Send + Sync,
|
||||
{
|
||||
type Error = MultiFormatError<T::Error, U::Error>;
|
||||
|
||||
#[inline] fn validate(s: &str) -> Result<(), Self::Error> {
|
||||
T::validate(s).map_err(MultiFormatError::First)?;
|
||||
U::validate(s).map_err(MultiFormatError::Second)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static, U: 'static> error::Error for MultiFormatError<T,U>
|
||||
where T: error::Error,
|
||||
U: error::Error
|
||||
{
|
||||
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||
match &self {
|
||||
Self::First(f) => Some(f),
|
||||
Self::Second(n) => Some(n),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T, U> fmt::Display for MultiFormatError<T,U>
|
||||
where T: fmt::Display,
|
||||
U: fmt::Display
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
||||
{
|
||||
match self {
|
||||
Self::First(_) => write!(f, "the first condition failed"),
|
||||
Self::Second(_) => write!(f, "the second condition failed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A hex string format specifier
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum HexFormat{}
|
||||
/// A string with a constant max length
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum MaxLenFormat<const MAX: usize>{}
|
||||
|
||||
impl<const MAX: usize> FormatSpec for MaxLenFormat<MAX>
|
||||
{
|
||||
type Error = MaxLenExceededError<MAX>;
|
||||
|
||||
#[inline] fn validate(s: &str) -> Result<(), Self::Error> {
|
||||
if s.len() > MAX {
|
||||
Err(MaxLenExceededError)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MaxLenExceededError<const MAX: usize>;
|
||||
|
||||
impl<const MAX: usize> error::Error for MaxLenExceededError<MAX>{}
|
||||
impl<const MAX: usize> fmt::Display for MaxLenExceededError<MAX>
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
||||
{
|
||||
write!(f, "string length exceeds {}", MAX)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl HexFormat
|
||||
{
|
||||
const HEX_MAP: &'static [u8] = b"1234567890abcdefABCDEF";
|
||||
}
|
||||
|
||||
impl FormatSpec for HexFormat
|
||||
{
|
||||
type Error = HexFormatError;
|
||||
fn validate(s: &str) -> Result<(), Self::Error> {
|
||||
|
||||
for (i, chr) in s.char_indices()
|
||||
{
|
||||
if !chr.is_ascii_alphanumeric() || !Self::HEX_MAP.contains(&(chr as u8)) {
|
||||
return Err(HexFormatError(chr, i));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Error for an invalid hex string.
|
||||
#[derive(Debug)]
|
||||
pub struct HexFormatError(char, usize);
|
||||
|
||||
impl error::Error for HexFormatError{}
|
||||
impl fmt::Display for HexFormatError
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
||||
{
|
||||
write!(f, "invalid hex char {:?} at index {}", self.0, self.1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A PEM formatted string.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum PEMFormat{}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PEMFormatError;
|
||||
|
||||
impl error::Error for PEMFormatError{}
|
||||
impl fmt::Display for PEMFormatError
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
||||
{
|
||||
write!(f, "invalid PEM format")
|
||||
}
|
||||
}
|
||||
|
||||
impl PEMFormat
|
||||
{
|
||||
#[inline] fn is_allowed(string: &str) -> bool
|
||||
{
|
||||
lazy_static! {
|
||||
static ref ALLOWED_MAP: smallmap::Map<&'static str,()> = defaults::RECOGNISABLE_PEM_ENCODES.iter().map(|&x| (x, ())).collect();
|
||||
}
|
||||
ALLOWED_MAP.contains_key(&string)
|
||||
}
|
||||
|
||||
const BEGIN_BLOCK: &'static str = "-----BEGIN ";
|
||||
const END_BLOCK: &'static str = "-----END ";
|
||||
const END_ANY_BLOCK: &'static str = "-----";
|
||||
}
|
||||
|
||||
impl FormatSpec for PEMFormat
|
||||
{
|
||||
type Error = PEMFormatError;
|
||||
fn validate(s: &str) -> Result<(), Self::Error> {
|
||||
|
||||
macro_rules! enforce {
|
||||
($e:expr) => {
|
||||
if !$e {
|
||||
return Err(PEMFormatError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let s = s.trim();
|
||||
let mut lines = s.lines();
|
||||
let begin = lines.next().ok_or(PEMFormatError)?.trim();
|
||||
enforce!(begin.starts_with(Self::BEGIN_BLOCK));
|
||||
enforce!(begin.ends_with(Self::END_ANY_BLOCK));
|
||||
|
||||
let val = &begin[Self::BEGIN_BLOCK.len()..];
|
||||
let val = &val[..(val.len()-Self::END_ANY_BLOCK.len())];
|
||||
enforce!(Self::is_allowed(val));
|
||||
|
||||
let mut lines = lines.map(|x| x.trim()).fuse();
|
||||
while let Some(line) = lines.next()
|
||||
{
|
||||
if line.starts_with(Self::END_BLOCK) {
|
||||
let eval = &line[Self::END_BLOCK.len()..];
|
||||
let eval = &eval[..(eval.len()-Self::END_ANY_BLOCK.len())];
|
||||
enforce!(eval == val);
|
||||
break;
|
||||
} else {
|
||||
enforce!(Base64Format::validate(line.trim()).is_ok());
|
||||
}
|
||||
}
|
||||
enforce!(lines.next().is_none());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Copy)]
|
||||
pub enum Base64Format{}
|
||||
|
||||
impl Base64Format
|
||||
{
|
||||
const CHARSET_STR: &'static str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/";
|
||||
|
||||
}
|
||||
|
||||
impl FormatSpec for Base64Format
|
||||
{
|
||||
type Error = InvalidBase64Error;
|
||||
|
||||
fn validate(s: &str) -> Result<(), Self::Error> {
|
||||
lazy_static! {
|
||||
static ref CHARSET: [bool; 256] = {
|
||||
let mut map = [false; 256];
|
||||
for byte in Base64Format::CHARSET_STR.bytes()
|
||||
{
|
||||
map[byte as usize] = true;
|
||||
}
|
||||
map
|
||||
};
|
||||
}
|
||||
let mut iter = s.as_bytes().chunks(4).peekable();
|
||||
while let Some(window) = iter.next()
|
||||
{
|
||||
let is_last = iter.peek().is_none();
|
||||
// eprintln!("Window: {:?} ({})", std::str::from_utf8(window), is_last);
|
||||
for byte in window.iter().copied()
|
||||
{
|
||||
if !(CHARSET[byte as usize] || (is_last && byte == b'=')) {
|
||||
return Err(InvalidBase64Error(byte as char));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// On invalid base64 string
|
||||
#[derive(Debug)]
|
||||
pub struct InvalidBase64Error(char);
|
||||
|
||||
impl error::Error for InvalidBase64Error{}
|
||||
impl fmt::Display for InvalidBase64Error
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
||||
{
|
||||
write!(f, "invalid char {:?} in base64 string", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub type MaxLenStr<const MAX: usize> = FormattedStr<MaxLenFormat<MAX>>;
|
||||
pub type MaxLenString<const MAX: usize> = FormattedString<MaxLenFormat<MAX>>;
|
||||
|
||||
pub type HexFormattedStr = FormattedStr<HexFormat>;
|
||||
pub type HexFormattedString = FormattedString<HexFormat>;
|
||||
|
||||
pub type Base64FormattedStr = FormattedStr<Base64Format>;
|
||||
pub type Base64FormattedString = FormattedString<Base64Format>;
|
||||
|
||||
impl Base64FormattedString
|
||||
{
|
||||
/// Encode some data as base64
|
||||
pub fn encode(data: impl AsRef<[u8]>) -> Self
|
||||
{
|
||||
unsafe { Self::new_unchecked(base64::encode(data)) }
|
||||
}
|
||||
}
|
||||
impl Base64FormattedStr
|
||||
{
|
||||
/// Decode this base64 string to a byte slice, returning the length of the written bytes.
|
||||
///
|
||||
/// # Panics
|
||||
/// If the slice is not large enough
|
||||
pub fn decode_slice(&self, mut to: impl AsMut<[u8]>) -> usize
|
||||
{
|
||||
base64::decode_config_slice(self.as_str(), base64::STANDARD, to.as_mut()).unwrap()
|
||||
}
|
||||
/// Decode this base64 string to a `Vec<u8>`
|
||||
pub fn decode(&self) -> Vec<u8>
|
||||
{
|
||||
base64::decode(self.as_str()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub type PEMFormattedStr = FormattedStr<PEMFormat>;
|
||||
pub type PEMFormattedString = FormattedString<PEMFormat>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests
|
||||
{
|
||||
use super::*;
|
||||
#[test]
|
||||
fn hex_format()
|
||||
{
|
||||
let _invalid = HexFormattedStr::new("ab120982039840i ").expect_err("Invalidation");
|
||||
let _valid = HexFormattedStr::new("abc123982095830495adcfDD").expect("Validation");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64()
|
||||
{
|
||||
let mut random = [0u8; 256];
|
||||
for len in 9..101
|
||||
{
|
||||
getrandom::getrandom(&mut random[..len]).expect("rng");
|
||||
let encoded = base64::encode(&random[..len]);
|
||||
println!("String: {}", encoded);
|
||||
let _encoded = Base64FormattedString::new(encoded).expect("Encode validate failed");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pem_format()
|
||||
{
|
||||
const PUBKEY: &'static str = r#"-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD2vGFdJ+KCK6Ot8sDcaCXk/9D8
|
||||
S1InEHi5vGSLdGKJpaRdHhQ08NL5+fv6B9FGKV5KARCYXeW1JGnGNzhRXHhNyOSm
|
||||
KNi2T+L84xBCTEazlLnnnvqKGaD95rtjwMmkhsErRMfavqUMThEmVca5fwP30Sqm
|
||||
StF6Y2cSO2eUjqTeUQIDAQAB
|
||||
-----END PUBLIC KEY-----"#;
|
||||
let pem = PEMFormattedStr::new(PUBKEY).expect("PEM format");
|
||||
println!("PEM: {}", pem);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,261 @@
|
||||
//! Mnemonic hashes, incl. tripcode.
|
||||
use super::*;
|
||||
use std::borrow::Borrow;
|
||||
use khash::ctx::Context;
|
||||
use cryptohelpers::sha256::{self, Sha256Hash};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::cmp::Ordering;
|
||||
use std::str;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// A newtype tripcode string
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct Tripcode(String);
|
||||
|
||||
lazy_static!{
|
||||
static ref CONTEXT: Context = Context::new(defaults::TRIPCODE_ALGO, defaults::TRIPCODE_SALT);
|
||||
}
|
||||
|
||||
impl Tripcode
|
||||
{
|
||||
/// Generate a tripcode from this string.
|
||||
pub fn generate(from: impl AsRef<[u8]>) -> Result<Self, khash::error::Error>
|
||||
{
|
||||
khash::generate(&CONTEXT, from).map(Self)
|
||||
}
|
||||
/// Create a tripcode that *is* this string
|
||||
#[inline] pub fn special(string: String) -> Self
|
||||
{
|
||||
Self(string)
|
||||
}
|
||||
/// As a string
|
||||
#[inline] pub fn as_str(&self) -> &str
|
||||
{
|
||||
&self.0
|
||||
}
|
||||
/// Consume into regular string
|
||||
#[inline] pub fn into_inner(self) -> String
|
||||
{
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Tripcode> for String
|
||||
{
|
||||
#[inline] fn from(from: Tripcode) -> Self
|
||||
{
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Tripcode
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
||||
{
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Tripcode
|
||||
{
|
||||
fn as_ref(&self) -> &str
|
||||
{
|
||||
&self.0[..]
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<String> for Tripcode
|
||||
{
|
||||
fn borrow(&self) -> &String
|
||||
{
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
/// A mnemonic base64 hash
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MnemonicHash(Sha256Hash, OnceCell<String>);
|
||||
|
||||
impl Ord for MnemonicHash
|
||||
{
|
||||
fn cmp(&self, other: &Self) -> Ordering
|
||||
{
|
||||
self.0.cmp(&other.0)
|
||||
}
|
||||
}
|
||||
impl PartialOrd for MnemonicHash
|
||||
{
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
self.0.partial_cmp(&other.0)
|
||||
}
|
||||
}
|
||||
impl Eq for MnemonicHash{}
|
||||
impl PartialEq for MnemonicHash
|
||||
{
|
||||
fn eq(&self, other: &Self) -> bool
|
||||
{
|
||||
self.0 == other.0
|
||||
}
|
||||
}
|
||||
impl Hash for MnemonicHash {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.0.hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl str::FromStr for MnemonicHash
|
||||
{
|
||||
type Err = std::convert::Infallible;
|
||||
|
||||
#[inline] fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Self::from_str(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl MnemonicHash
|
||||
{
|
||||
/// Create mnemonic hash from this string slice
|
||||
#[inline] pub fn from_str(string: impl AsRef<str>) -> Self
|
||||
{
|
||||
Self::from_slice(string.as_ref().as_bytes())
|
||||
}
|
||||
/// Create mnemonic hash from this slice
|
||||
pub fn from_slice(data: impl AsRef<[u8]>) -> Self
|
||||
{
|
||||
Self(sha256::compute_slice_iter(iter![data.as_ref(), defaults::MNEMONIC_SALT.as_ref()]), OnceCell::new())
|
||||
}
|
||||
|
||||
/// The inner hash
|
||||
#[inline] pub fn as_hash(&self) -> &Sha256Hash
|
||||
{
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Create a mnemonic from this hash
|
||||
///
|
||||
/// # Notes
|
||||
/// This does not salt the hash, the hasher is responsible for salting the hash with `defaults::MNEMONIC_SALT`.
|
||||
#[inline] pub fn from_hash(hash: Sha256Hash) -> Self
|
||||
{
|
||||
Self(hash, OnceCell::new())
|
||||
}
|
||||
|
||||
fn render(&self) -> String
|
||||
{
|
||||
#[allow(unused_mut)] let mut end;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature="short-mnemonics")] {
|
||||
let last = &self.0.as_ref()[..(sha256::SIZE/2)];
|
||||
let first = &self.0.as_ref()[(sha256::SIZE/2)..];
|
||||
end = [0u8; sha256::SIZE/2];
|
||||
for (e, (f, l)) in end.iter_mut().zip(first.iter().copied().zip(last.iter().copied()))
|
||||
{
|
||||
*e = f ^ l;
|
||||
}
|
||||
} else {
|
||||
end = &self.0.as_ref()[..];
|
||||
}
|
||||
}
|
||||
base64::encode(end).chars().filter_map(|x| {
|
||||
Some(match x {
|
||||
'/' => 'ł',
|
||||
'+' => 'þ',
|
||||
'=' => return None,
|
||||
x => x
|
||||
})
|
||||
}).collect()
|
||||
}
|
||||
|
||||
#[inline] fn render_cache(&self) -> &String
|
||||
{
|
||||
self.1.get_or_init(|| {
|
||||
self.render()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MnemonicHash> for Sha256Hash
|
||||
{
|
||||
fn from(from: MnemonicHash) -> Self
|
||||
{
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl AsRef<Sha256Hash> for MnemonicHash
|
||||
{
|
||||
fn as_ref(&self) -> &Sha256Hash
|
||||
{
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<Sha256Hash> for MnemonicHash
|
||||
{
|
||||
fn borrow(&self) -> &Sha256Hash {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for MnemonicHash
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
||||
{
|
||||
write!(f, "{}", self.render_cache())
|
||||
}
|
||||
}
|
||||
|
||||
/// What kind of salt to use for hashing `MnemonicHash`.
|
||||
///
|
||||
/// This is intended to be global state.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum MnemonicSaltKind
|
||||
{
|
||||
None,
|
||||
Specific([u8; defaults::MNEMONIC_SALT_SIZE]),
|
||||
Random,
|
||||
}
|
||||
|
||||
impl MnemonicSaltKind
|
||||
{
|
||||
/// Get as a slice.
|
||||
pub fn as_slice(&self) -> Option<&[u8]>
|
||||
{
|
||||
lazy_static! {
|
||||
static ref RANDOM_SALT: [u8; defaults::MNEMONIC_SALT_SIZE] = {
|
||||
let mut output = [0u8; defaults::MNEMONIC_SALT_SIZE];
|
||||
getrandom::getrandom(&mut output[..]).expect("rng fatal");
|
||||
output
|
||||
};
|
||||
}
|
||||
Some(match self {
|
||||
Self::None => return None,
|
||||
Self::Random => &RANDOM_SALT[..],
|
||||
Self::Specific(u) => &u[..],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for MnemonicSaltKind
|
||||
{
|
||||
#[inline] fn as_ref(&self) -> &[u8]
|
||||
{
|
||||
self.as_slice().unwrap_or(&[])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests
|
||||
{
|
||||
#[test]
|
||||
fn mnemonics()
|
||||
{
|
||||
let _mnemonic = super::MnemonicHash::from_slice(b"hello world");
|
||||
}
|
||||
}
|
@ -0,0 +1,256 @@
|
||||
use super::*;
|
||||
use cryptohelpers::sha256::Sha256Hash;
|
||||
|
||||
mod render;
|
||||
pub use render::*;
|
||||
|
||||
use hard_format::formats::{
|
||||
MaxLenString,
|
||||
Base64FormattedString,
|
||||
Base64FormattedStr,
|
||||
self,
|
||||
};
|
||||
use mnemonic::Tripcode;
|
||||
|
||||
id_type!(PostID; "A unique post ID");
|
||||
|
||||
/// String type that limits its bytes to the ID string max limit.
|
||||
pub type IDMaxString = MaxLenString<{defaults::POST_ID_MAX_LEN}>;
|
||||
|
||||
/// The timestamp type used in posts
|
||||
pub type PostTimestamp = chrono::DateTime<defaults::Timezone>;
|
||||
|
||||
/// A size limited base64 formatting specifier
|
||||
type PostBodyFormat = formats::BothFormat<formats::MaxLenFormat<{defaults::POST_BODY_MAX_SIZE}>, formats::Base64Format>;
|
||||
|
||||
/// A size limited base64 string
|
||||
pub type PostBodyString = hard_format::FormattedString<PostBodyFormat>;
|
||||
/// A size limited base64 string
|
||||
pub type PostBodyStr = hard_format::FormattedStr<PostBodyFormat>;
|
||||
|
||||
/// Identifiers for a post
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default)]
|
||||
pub struct Ident
|
||||
{
|
||||
name: Option<IDMaxString>,
|
||||
tripcode: Option<Tripcode>,
|
||||
email: Option<IDMaxString>,
|
||||
}
|
||||
|
||||
impl Ident
|
||||
{
|
||||
/// Create a new ident object
|
||||
///
|
||||
/// # Panics
|
||||
/// If `name` or `email` are longer than `defaults::POST_ID_MAX_LEN`.
|
||||
pub fn new(name: Option<&str>, tripcode: Option<Tripcode>, email: Option<&str>) -> Self
|
||||
{
|
||||
Self {
|
||||
name: name.map(|x| IDMaxString::new(x.to_owned()).expect("Name too long")),
|
||||
email: email.map(|x| IDMaxString::new(x.to_owned()).expect("Email too long")),
|
||||
tripcode
|
||||
}
|
||||
}
|
||||
|
||||
/// The name of this user ident
|
||||
pub fn name(&self) -> &str
|
||||
{
|
||||
self.name.as_ref().map(|x| x.as_str()).unwrap_or(defaults::ANON_NAME)
|
||||
}
|
||||
/// The tripcode of this user ident
|
||||
pub fn tripcode(&self) -> Option<&Tripcode>
|
||||
{
|
||||
self.tripcode.as_ref()
|
||||
}
|
||||
/// The email of this user ident
|
||||
pub fn email(&self) -> Option<&str>
|
||||
{
|
||||
self.email.as_ref().map(|x| x.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A single completed post.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct Post
|
||||
{
|
||||
/// Unique ID for each post
|
||||
id: PostID,
|
||||
|
||||
/// Who created the post, (if specified)
|
||||
owner: Option<user::UserID>,
|
||||
|
||||
/// Identifiers for this post
|
||||
ident: Ident,
|
||||
|
||||
/// The client-side generated AES key used for decrypting the body's text.
|
||||
///
|
||||
/// This AES key can be encrypted with any number of RSA public keys on the client side. There must be at least one, or the post body is lost and the post should be removed.
|
||||
body_aes_keys: Vec<PostBodyString>,
|
||||
|
||||
/// The client-side encrypted body string
|
||||
body: PostBodyString,
|
||||
|
||||
/// Signature of the body (optional).
|
||||
signature: Option<Base64FormattedString>,
|
||||
|
||||
/// Hash of the body
|
||||
hash: Sha256Hash,
|
||||
|
||||
/// When the post was created
|
||||
created: PostTimestamp,
|
||||
|
||||
/// When the post was last edited.
|
||||
///
|
||||
/// # Notes
|
||||
/// Each new edit is pushed to the end of the vec, creation does not count as an edit.
|
||||
edited: Vec<PostTimestamp>,
|
||||
|
||||
/// Optional dynamic expiry duration.
|
||||
expires_in: Option<tokio::time::Duration>,
|
||||
}
|
||||
|
||||
// Time based functions
|
||||
impl Post
|
||||
{
|
||||
/// The timestamp when the post should expire.
|
||||
///
|
||||
/// # Notes
|
||||
/// If the conversion overflows, then the expiry time is treated as 0. (i.e. the post expires immediately).
|
||||
pub fn expires_at(&self) -> PostTimestamp
|
||||
{
|
||||
self.created + chrono::Duration::from_std(self.expires_in.unwrap_or(defaults::POST_EXPIRE))
|
||||
.unwrap_or(chrono::Duration::zero())
|
||||
}
|
||||
/// How long until the post has until it reaches its expiry time from its creation time.
|
||||
///
|
||||
/// If this value is *lower* than the value of `time_since_creation`, then the post has expired.
|
||||
pub fn expires_in(&self) -> chrono::Duration
|
||||
{
|
||||
self.expires_at() - self.created
|
||||
}
|
||||
/// Time passed since this post was created as a Chrono `Duration`.
|
||||
pub fn time_since_creation(&self) -> chrono::Duration
|
||||
{
|
||||
defaults::Timezone::now() - self.created
|
||||
}
|
||||
/// The timestamp for when this post was created
|
||||
pub fn created(&self) -> PostTimestamp
|
||||
{
|
||||
self.created
|
||||
}
|
||||
/// A slice of timestamps showing when this post was edited, in order of those edits.
|
||||
pub fn edited(&self) -> &[PostTimestamp]
|
||||
{
|
||||
&self.edited[..]
|
||||
}
|
||||
/// Has this post expired?
|
||||
///
|
||||
/// Expired posts should be removed
|
||||
pub fn expired(&self) -> bool
|
||||
{
|
||||
if let Ok(dur) = &self.time_since_creation().to_std()
|
||||
{
|
||||
dur >= self.expires_in.as_ref().unwrap_or(&defaults::POST_EXPIRE)
|
||||
} else {
|
||||
// Conversion failed. Expire the post
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ident based functions
|
||||
impl Post
|
||||
{
|
||||
/// Get a mnemonic for this post's ID.
|
||||
#[inline] pub fn post_id_mnemonic(&self) -> mnemonic::MnemonicHash
|
||||
{
|
||||
mnemonic::MnemonicHash::from_slice(self.id.id_as_bytes())
|
||||
}
|
||||
/// This post's unique identifier
|
||||
#[inline] pub fn post_id(&self) -> &PostID
|
||||
{
|
||||
&self.id
|
||||
}
|
||||
|
||||
/// `UserID` of the post owner, if there is one.
|
||||
#[inline] pub fn owner_id(&self) -> Option<&user::UserID>
|
||||
{
|
||||
self.owner.as_ref()
|
||||
}
|
||||
/// The user-set name for this post if there is one.
|
||||
#[inline] pub fn own_name(&self) -> Option<&str>
|
||||
{
|
||||
self.ident.name.as_ref().map(|x| x.as_str())
|
||||
}
|
||||
/// The name for this post.
|
||||
///
|
||||
/// If no name is set, returns the default anon name.
|
||||
pub fn name(&self) -> &str
|
||||
{
|
||||
self.own_name().unwrap_or(defaults::ANON_NAME)
|
||||
}
|
||||
|
||||
/// The email set for this post, if there is one.
|
||||
pub fn email(&self) -> Option<&str>
|
||||
{
|
||||
self.ident.email.as_ref().map(|x| x.as_str())
|
||||
}
|
||||
|
||||
/// Get the tripcode of this post, if there is one.
|
||||
pub fn tripcode(&self) -> Option<&Tripcode>
|
||||
{
|
||||
self.ident.tripcode.as_ref()
|
||||
}
|
||||
|
||||
/// The AES encrypted body of this post
|
||||
pub fn body(&self) -> &PostBodyStr
|
||||
{
|
||||
self.body.as_ref()
|
||||
}
|
||||
|
||||
/// An iterator of RSA ciphertexts of the AES key used to encrypt the body text.
|
||||
pub fn body_keys<'a>(&'a self) -> impl Iterator<Item = &'a PostBodyStr> + ExactSizeIterator + 'a
|
||||
{
|
||||
self.body_aes_keys.iter().map(|x| x.as_ref())
|
||||
}
|
||||
|
||||
/// The PEM formatted signature of this post, if there is one.
|
||||
pub fn signature(&self) -> Option<&Base64FormattedStr>
|
||||
{
|
||||
self.signature.as_ref().map(|x| x.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests
|
||||
{
|
||||
#[test]
|
||||
fn post_serialise()
|
||||
{
|
||||
use std::convert::TryInto;
|
||||
let post = super::Post {
|
||||
owner: None,
|
||||
id: super::PostID::id_new(),
|
||||
ident: super::Ident {
|
||||
name: Some("Some name".to_owned().try_into().unwrap()),
|
||||
email: None,
|
||||
tripcode: Some(super::Tripcode::generate("uhh hello").unwrap()),
|
||||
},
|
||||
body: super::PostBodyString::new(crate::hard_format::formats::Base64FormattedString::encode("Hello world").into()).unwrap(),
|
||||
body_aes_keys: vec![super::PostBodyString::new(crate::hard_format::formats::Base64FormattedString::encode("TODO").into()).unwrap()],
|
||||
signature: None,
|
||||
hash: Default::default(),
|
||||
|
||||
created: crate::defaults::Timezone::now(),
|
||||
edited: Default::default(),
|
||||
expires_in: None,
|
||||
};
|
||||
eprintln!("Post as html: {}", html! { body { (post) } }.into_string());
|
||||
println!("Post is: {:?}", post);
|
||||
let post_json = serde_json::to_vec(&post).expect("Serialise");
|
||||
println!("Post json: {}", std::str::from_utf8(&post_json[..]).unwrap());
|
||||
let post2: super::Post = serde_json::from_slice(&post_json[..]).expect("Deserialise");
|
||||
assert_eq!(post, post2);
|
||||
println!("Post was: {:?}", post2);
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
use super::*;
|
||||
use maud::{html, Markup, Render};
|
||||
|
||||
impl Render for Ident
|
||||
{
|
||||
fn render(&self) -> Markup
|
||||
{
|
||||
html! {
|
||||
b.ident {
|
||||
@if let Some(email) = self.email() {
|
||||
a href=(format!("mailto:{}", email)) { (self.name()) }
|
||||
} @else {
|
||||
(self.name())
|
||||
}
|
||||
@if let Some(tripcode) = self.tripcode() {
|
||||
" "
|
||||
code {
|
||||
"!" (tripcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Post
|
||||
{
|
||||
fn render(&self) -> Markup
|
||||
{
|
||||
let id = self.post_id_mnemonic();
|
||||
html! {
|
||||
article#(id) {
|
||||
header {
|
||||
(self.ident)
|
||||
" "
|
||||
time datetime=(self.created()) {
|
||||
(self.created()) //TODO format the DateTime properly
|
||||
}
|
||||
nav {
|
||||
a href=(format!("#{}", id)) { "No." }
|
||||
a href=(format!("#q_{}", id)) { (id) }
|
||||
}
|
||||
}
|
||||
blockquote {
|
||||
(self.body())
|
||||
}
|
||||
//TODO: Signature and hash and other things
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests
|
||||
{
|
||||
#[test]
|
||||
fn ident_htmlrender()
|
||||
{
|
||||
let ident = super::Ident::new(Some("Name"), Some(crate::mnemonic::Tripcode::generate("mwee").unwrap()), Some("user@example.com"));
|
||||
use maud::Render;
|
||||
|
||||
assert_eq!(r#"<b class="ident"><a href="mailto:user@example.com">Name</a> <code>!えナセッゲよラで</code></b>"#, &ident.render().into_string());
|
||||
}
|
||||
}
|
@ -1,147 +1 @@
|
||||
//! Session services
|
||||
use super::*;
|
||||
use tokio::{
|
||||
task::JoinHandle,
|
||||
sync::{
|
||||
mpsc,
|
||||
broadcast,
|
||||
},
|
||||
};
|
||||
use std::{fmt,error};
|
||||
use std::marker::{Send,Sync};
|
||||
|
||||
///// A boxed message that can be downcasted.
|
||||
//pub type BoxedMessage = Box<dyn std::any::Any + Send + Sync + 'static>;
|
||||
|
||||
/// A handle to a service.
|
||||
pub trait Service<T=ExitStatus>
|
||||
where T: Send + 'static
|
||||
{
|
||||
/// The message type to send to the service.
|
||||
type Message: Send + Sync + 'static;
|
||||
/// The response to expect from the service.
|
||||
type Response: Send + Sync + 'static;
|
||||
|
||||
/// Return the wait handle.
|
||||
///
|
||||
/// This method should drop the message pipeline.
|
||||
fn wait_on(self) -> JoinHandle<T>;
|
||||
|
||||
/// An immutable reference to the pipe for sending message. Useful for service handle detaching (i.e. cloning the message input pipe).
|
||||
fn message_in_ref(&self) -> &mpsc::Sender<Self::Message>;
|
||||
|
||||
/// The message pipe for sending messages to the service.
|
||||
fn message_in(&mut self) -> &mut mpsc::Sender<Self::Message>;
|
||||
/// The message pipe for receiving messages from the service.
|
||||
fn message_out(&mut self) -> &mut broadcast::Receiver<Self::Response>;
|
||||
|
||||
/// Is the service alive? A `None` value means 'maybe', and is the default return.
|
||||
///
|
||||
/// # Note
|
||||
/// This should not be considered an infallible indicator of if the service has crashed or not. A better method is attempting to send or receive a message and the sender/receiver returning an error.
|
||||
#[inline] fn is_alive(&self) -> Option<bool>
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Error returned when subscribing to a service
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum SubscribeError
|
||||
{
|
||||
/// The service's receive has already been dropped.
|
||||
SenderDropped,
|
||||
/// The service dropped the response oneshot channel.
|
||||
NoResponse,
|
||||
}
|
||||
|
||||
impl error::Error for SubscribeError{}
|
||||
impl fmt::Display for SubscribeError
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
||||
{
|
||||
match self {
|
||||
Self::SenderDropped => write!(f, "the service has already stopped"),
|
||||
Self::NoResponse => write!(f, "the service declined to, or was unable to respond"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! join_service {
|
||||
($serv:expr) => ($serv.await.map_err(|_| $crate::service::ExitStatus::Abnormal)?)
|
||||
}
|
||||
|
||||
|
||||
/// How a service exited
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum ExitStatus<T=()>
|
||||
{
|
||||
/// A graceful exit with value
|
||||
Graceful(T),
|
||||
/// An abnormal exit (counted as error)
|
||||
///
|
||||
/// # Usage
|
||||
/// Usually for panicked services, otherwise use `Self::Error`.
|
||||
/// The macro `join_service!()` can be used to convert handle join errors into this
|
||||
Abnormal,
|
||||
/// Exit on an error report (counted as error)
|
||||
Error(eyre::Report),
|
||||
}
|
||||
|
||||
impl<T,E: Into<eyre::Report>> From<Result<T, E>> for ExitStatus<T>
|
||||
{
|
||||
fn from(from: Result<T, E>) -> Self
|
||||
{
|
||||
match from
|
||||
{
|
||||
Ok(v) => Self::Graceful(v),
|
||||
Err(e) => Self::Error(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
/// The error `ExitStatus::Abnormal` converts to.
|
||||
pub struct AbnormalExitError;
|
||||
|
||||
impl error::Error for AbnormalExitError{}
|
||||
impl fmt::Display for AbnormalExitError
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
||||
{
|
||||
write!(f, "service terminated in an abnormal way")
|
||||
}
|
||||
}
|
||||
/*
|
||||
impl<U, T: error::Error + Send+Sync+'static> From<T> for ExitStatus<U>
|
||||
{
|
||||
fn from(from: T) -> Self
|
||||
{
|
||||
Self::Error(from.into())
|
||||
}
|
||||
}*/
|
||||
impl<T: Default> Default for ExitStatus<T>
|
||||
{
|
||||
#[inline]
|
||||
fn default() -> Self
|
||||
{
|
||||
Self::Graceful(T::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<ExitStatus<T>> for eyre::Result<T>
|
||||
{
|
||||
fn from(from: ExitStatus<T>) -> Self
|
||||
{
|
||||
match from {
|
||||
ExitStatus::Abnormal => Err(AbnormalExitError).with_note(|| "The background worker likely panicked"),
|
||||
ExitStatus::Graceful(t) => Ok(t),
|
||||
ExitStatus::Error(rep) => Err(rep).wrap_err(AbnormalExitError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//! Services
|
||||
|
@ -1,33 +0,0 @@
|
||||
//! Open post body
|
||||
use super::*;
|
||||
use tokio::{
|
||||
sync::{
|
||||
watch,
|
||||
mpsc,
|
||||
},
|
||||
};
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
/// An open post body
|
||||
#[derive(Debug)]
|
||||
pub struct Karada
|
||||
{
|
||||
scape: Vec<char>,
|
||||
|
||||
body_cache: String,
|
||||
//TODO: Pub/sub? Or should that be in some other place? Probably.
|
||||
}
|
||||
|
||||
impl Karada
|
||||
{
|
||||
fn invalidate_cache(&mut self)
|
||||
{
|
||||
self.body_cache = self.scape.iter().copied().collect();
|
||||
}
|
||||
/// Apply this delta to the body.
|
||||
pub fn apply_delta(&mut self, delta: &delta::Delta)
|
||||
{
|
||||
delta.insert(&mut self.scape);
|
||||
self.invalidate_cache();
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
use tokio::time::{
|
||||
DelayQueue,
|
||||
delay_queue,
|
||||
};
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Cache
|
||||
{
|
||||
//TODO: HashMap and DelayQueue of mapping public keys to user IDs, etc.
|
||||
pkey_maps: HashMap<String, (UserID, delay_queue::Key)>,
|
||||
pkey_rems: DelayQueue<String>,
|
||||
}
|
||||
|
||||
impl Cache
|
||||
{
|
||||
/// Create a new empty cache
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
pkey_maps: HashMap::new(),
|
||||
pkey_rems: DelayQueue::new(),
|
||||
}
|
||||
}
|
||||
|
||||
//how tf do we get this to run concurrently with insertions??? it holds `&mut self` forever!
|
||||
//redesign required. maybe using Arc and RwLock or Mutex and interrupting the purge task when something needs to be inserted. i.e: give this task a stream that it can `select!` along with calling `next()`, if the other future completes first, we return? but wouldn't that lose us an item in `next()`? is there a `select with priority` in `futures`? i think there is. eh...
|
||||
/// Run a purge on this cache.
|
||||
pub async fn purge(&mut self) -> eyre::Result<()>
|
||||
{
|
||||
let mut errors = Vec::default();
|
||||
while let Some(entry) = self.pkey_rems.next().await
|
||||
{
|
||||
match entry {
|
||||
Ok(entry) => {
|
||||
self.pkey_maps.remove(entry.get_ref());
|
||||
},
|
||||
Err(err) => {
|
||||
errors.push(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
if errors.len() > 0 {
|
||||
let mut err = Err(eyre!("One or more removals failed"))
|
||||
.with_note(|| errors.len().to_string().header("Number of failed removals"));
|
||||
for e in errors.into_iter()
|
||||
{
|
||||
err = err.with_error(move || e);
|
||||
}
|
||||
return err;
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
/// Purge all values available to be purged now.
|
||||
pub fn purge_now(&mut self) -> eyre::Result<()>
|
||||
{
|
||||
match self.purge().now_or_never() {
|
||||
Some(x) => x,
|
||||
None => Ok(())
|
||||
}
|
||||
}
|
||||
}
|
@ -1,155 +1,391 @@
|
||||
//! Creating immutable images of state.
|
||||
//! Frozen, serialisable state
|
||||
use super::*;
|
||||
use std::{error,fmt};
|
||||
use cryptohelpers::sha256::{Sha256Hash, self};
|
||||
|
||||
//use futures::prelude::*;
|
||||
use std::io;
|
||||
use tokio::prelude::*;
|
||||
|
||||
/// An image of the entire post container
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub struct Freeze
|
||||
/// An immutable image of `State`.
|
||||
//TODO: Implement this when `State` is solidified and working
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
struct FreezeInner
|
||||
{
|
||||
posts: Vec<post::Post>,
|
||||
users: HashSet<User>,
|
||||
posts: HashSet<(UserID, Post)>,
|
||||
}
|
||||
|
||||
impl From<Freeze> for Imouto
|
||||
const FREEZE_CHK: &[u8; 4] = b"REI\0";
|
||||
|
||||
/// Metadata derived from `FreezeInner`'s CBOR serialisation.
|
||||
///
|
||||
/// This is written and read as-is.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Copy)]
|
||||
struct FreezeMetadata
|
||||
{
|
||||
#[inline] fn from(from: Freeze) -> Self
|
||||
{
|
||||
Self::from_freeze(from)
|
||||
}
|
||||
chk: [u8; 4],
|
||||
version: u32, //Version, //TODO: Convert env!(version) into type
|
||||
body_size: u64,
|
||||
compressed: bool, //TODO: Should body be compressed? If so, with what?
|
||||
body_hash: Sha256Hash,
|
||||
}
|
||||
|
||||
impl TryFrom<Imouto> for Freeze
|
||||
impl FreezeMetadata
|
||||
{
|
||||
type Error = FreezeError;
|
||||
#[inline] pub fn len(&self) -> usize
|
||||
{
|
||||
self.body_size.try_into().expect("Length exceeded limit of `usize`")
|
||||
}
|
||||
#[inline] fn new(from: &[u8]) -> Self
|
||||
{
|
||||
Self {
|
||||
chk: *FREEZE_CHK,
|
||||
version: defaults::VERSION.to_u32(),
|
||||
body_size: from.len().try_into().unwrap(), //this should never fail, right?
|
||||
compressed: false,
|
||||
body_hash: sha256::compute_slice(from),
|
||||
}
|
||||
}
|
||||
/// Write this metadata to an async stream, return the number of bytes written.
|
||||
pub async fn write_to(&self, mut to: impl tokio::io::AsyncWrite + Unpin) -> io::Result<usize>
|
||||
{
|
||||
macro_rules! write_all
|
||||
{
|
||||
($expr:expr) => {
|
||||
{
|
||||
let bytes = $expr;
|
||||
let bytes = &bytes[..];
|
||||
to.write_all(bytes).await?;
|
||||
bytes.len()
|
||||
}
|
||||
}
|
||||
}
|
||||
let done =
|
||||
write_all!(self.chk) +
|
||||
write_all!(u32::to_le_bytes(self.version)) +
|
||||
write_all!(u64::to_le_bytes(self.body_size)) +
|
||||
write_all!(if self.compressed {[1]} else {[0]}) +
|
||||
write_all!(self.body_hash.as_ref());
|
||||
|
||||
#[inline] fn try_from(from: Imouto) -> Result<Self, Self::Error>
|
||||
Ok(done)
|
||||
}
|
||||
/// Read a metadata object from an aynsc stream, without verifying any of its fields
|
||||
async fn read_from_unchecked(mut from: impl tokio::io::AsyncRead + Unpin) -> io::Result<Self>
|
||||
{
|
||||
macro_rules! read_exact
|
||||
{
|
||||
($num:expr) => {
|
||||
{
|
||||
from.try_into_freeze()
|
||||
let mut buf = [0u8; $num];
|
||||
from.read_exact(&mut buf[..]).await?;
|
||||
buf
|
||||
}
|
||||
}
|
||||
};
|
||||
(type $type:ty) => {
|
||||
{
|
||||
read_exact!(std::mem::size_of::<$type>())
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(
|
||||
Self {
|
||||
chk: read_exact!(4),
|
||||
version: u32::from_le_bytes(read_exact!(type u32)),
|
||||
body_size: u64::from_le_bytes(read_exact!(type u64)),
|
||||
compressed: if read_exact!(1)[0] == 0 { false } else { true },
|
||||
body_hash: Sha256Hash::from(read_exact!(type Sha256Hash)),
|
||||
}
|
||||
)
|
||||
}
|
||||
/// Read a metadata object from an async stream, verifying its fields.
|
||||
///
|
||||
/// # Note
|
||||
/// The `body_hash` field must be verified *after* reading the body.
|
||||
pub async fn read_from(from: impl tokio::io::AsyncRead + Unpin) -> eyre::Result<Self>
|
||||
{
|
||||
macro_rules! enforce {
|
||||
($expr:expr; $err:expr) => {
|
||||
{
|
||||
if ! $expr {
|
||||
return Err($err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let this = Self::read_from_unchecked(from).await
|
||||
.wrap_err(eyre!("Failed to read data from stream"))?;
|
||||
|
||||
macro_rules! enforce {
|
||||
($expr:expr, $err:expr) => {
|
||||
{
|
||||
if ! $expr {
|
||||
Err(eyre!($err))
|
||||
.with_section(|| format!("{:?}", this).header("Metadata was"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error returned when a freeze operation fails
|
||||
#[derive(Debug)]
|
||||
pub struct FreezeError{
|
||||
held: Option<Arc<RwLock<post::Post>>>,
|
||||
}
|
||||
enforce!(&this.chk == FREEZE_CHK, "Check value was invalid")
|
||||
.with_section(|| format!("{:?}", FREEZE_CHK).header("Expected"))
|
||||
.with_section(|| format!("{:?}", &this.chk).header("Got"))
|
||||
.with_suggestion(|| "Was this the correct type of file you wanted to load?")?;
|
||||
|
||||
impl FreezeError
|
||||
{
|
||||
/// The post associated with this error, if there is one.
|
||||
pub fn post(&self) -> Option<&Arc<RwLock<post::Post>>>
|
||||
enforce!(this.body_size <= defaults::MAX_IMAGE_READ_SIZE as u64, "Body size exceeded max")
|
||||
.with_section(|| format!("{}", &this.body_size).header("Size read was"))
|
||||
.with_section(|| format!("{}", defaults::MAX_IMAGE_READ_SIZE).header("Max size allowed is"))
|
||||
.with_warning(|| "This may indicate file corruption")?;
|
||||
|
||||
enforce!(this.version <= defaults::VERSION, "Unsupported version")
|
||||
.with_section(|| this.version.to_string().header("Read version was"))
|
||||
.with_section(|| defaults::VERSION.to_string().header("Current version is"))
|
||||
.with_suggestion(|| "This file may have been created with a newer version of the program. Try updating the program.")?;
|
||||
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
/// Verify the hash of this metadata by computing the hash of `from` and checking.
|
||||
///
|
||||
/// # Notes
|
||||
/// It is recommended to to this within a tokio `spawn_blocking` or `block_in_place` closure, as the hashing operation may take a while.
|
||||
pub fn verify_hash_blocking(&self, from: impl AsRef<[u8]>) -> bool
|
||||
{
|
||||
self.held.as_ref()
|
||||
sha256::compute_slice(from) == self.body_hash
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for FreezeError{}
|
||||
impl fmt::Display for FreezeError
|
||||
/// An immutable `State` image
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Freeze
|
||||
{
|
||||
//metadata: FreezeMetadata, //written as-is, calculated from body
|
||||
//body: Vec<u8>, // `FreezeInner` written with CBOR
|
||||
inner: FreezeInner, // Dumped to Vec<u8> as CBOR, then FreezeMetadata is calculated from this binary data. Then metadata is written, then the binary blob is written.
|
||||
}
|
||||
|
||||
impl Freeze
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
||||
/// Generate the output to write.
|
||||
///
|
||||
/// This might take a while, and is done synchronously. Recommended to run on blocking task.
|
||||
fn gen_output(&self) -> serde_cbor::Result<(FreezeMetadata, Vec<u8>)>
|
||||
{
|
||||
write!(f,"Failed to create freeze image")?;
|
||||
let body = serde_cbor::to_vec(&self.inner)?;
|
||||
Ok((FreezeMetadata::new(&body[..]), body))
|
||||
}
|
||||
|
||||
if let Some(aref) = &self.held
|
||||
/// Read `Freeze` from an input stream
|
||||
pub async fn read_async(mut input: impl tokio::io::AsyncRead + Unpin) -> eyre::Result<Self>
|
||||
{
|
||||
let cnt = Arc::strong_count(&aref) - 1;
|
||||
if cnt > 0 {
|
||||
write!(f, "Post reference still held in {} other places.", cnt)
|
||||
let inner: FreezeInner = {
|
||||
let unchecked_meta = FreezeMetadata::read_from(&mut input)
|
||||
.await
|
||||
.wrap_err(eyre!("Failed to read metadata from stream"))?;
|
||||
let mut body: Vec<u8> = vec![0; unchecked_meta.len()];
|
||||
input.read_exact(&mut body[..]).await
|
||||
.wrap_err(eyre!("Failed to read body from stream"))
|
||||
.with_section(|| format!("{:?}", unchecked_meta).header("Metadata was"))?;
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
if !unchecked_meta.verify_hash_blocking(&body) {
|
||||
Err(eyre!("Failed to verify metadata hash"))
|
||||
} else {
|
||||
write!(f, "Post reference was still held in another place at the time, but no longer is.")
|
||||
serde_cbor::from_slice(&body)
|
||||
.wrap_err(eyre!("Failed to deserialise body"))
|
||||
}
|
||||
} else {
|
||||
}).await
|
||||
.wrap_err("Background task panic")?
|
||||
.with_section(|| format!("{:?}", unchecked_meta).header("Metadata was"))?
|
||||
};
|
||||
Ok(Self {
|
||||
inner,
|
||||
})
|
||||
}
|
||||
|
||||
/// Write `Freeze` into this output stream on the current task.
|
||||
///
|
||||
/// This function runs the serialisation and hashing on a the current task, which is synchronous. Recommended to use `into_write_async` instead.
|
||||
pub async fn write_async(&self, mut output: impl tokio::io::AsyncWrite + Unpin) -> eyre::Result<()>
|
||||
{
|
||||
let (meta, body) = self.gen_output()
|
||||
.wrap_err(eyre!("Failed to generate write body"))?;
|
||||
meta.write_to(&mut output).await
|
||||
.wrap_err(eyre!("Failed to write metadata to output stream"))
|
||||
.with_section(|| format!("{:?}", meta).header("Metadata was"))?;
|
||||
output.write_all(&body[..]).await
|
||||
.wrap_err(eyre!("Failed to write whole body to output stream"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Consume this `Freeze` into this output stream.
|
||||
///
|
||||
/// This function runs the serialisation and hashing on a background blocking task instead of on the current one.
|
||||
pub async fn into_write_async(self, mut output: impl tokio::io::AsyncWrite + Unpin) -> eyre::Result<()>
|
||||
{
|
||||
let (meta, body) = tokio::task::spawn_blocking(move || self.gen_output())
|
||||
.await
|
||||
.wrap_err(eyre!("Background task panic"))?
|
||||
.wrap_err(eyre!("Failed to generate write body"))?;
|
||||
meta.write_to(&mut output).await
|
||||
.wrap_err(eyre!("Failed to write metadata to output stream"))
|
||||
.with_section(|| format!("{:?}", meta).header("Metadata was"))?;
|
||||
output.write_all(&body[..]).await
|
||||
.wrap_err(eyre!("Failed to write whole body to output stream"))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Imouto
|
||||
impl State
|
||||
{
|
||||
/// Create a serialisable image of this store by cloning each post into it.
|
||||
/// Create an image from the state.
|
||||
///
|
||||
/// # Locks
|
||||
/// This method holds the read lock of Oneesan, it also holds the read lock of all posts and users.
|
||||
/// This will prevent any writes while the `Freeze` is being created, and will also yield the current task until all write operations on `State` are completed.
|
||||
///
|
||||
/// # Panics
|
||||
/// If the internal state is incorrect.
|
||||
pub async fn freeze(&self) -> Freeze
|
||||
{
|
||||
let read = &self.all;
|
||||
let mut sis = Freeze{
|
||||
posts: Vec::with_capacity(read.len()),
|
||||
let onee = self.0.oneesan.read().await;
|
||||
use std::ops::Deref;
|
||||
|
||||
// this might be kinda expensive. should we offload this?
|
||||
let post_owner_reverse_lookup: HashMap<Index, UserID> = onee.posts_user_map.iter()
|
||||
.map(|(&y,x)| x.iter().map(move |idx| (*idx,y)))
|
||||
.flatten()
|
||||
.collect();
|
||||
let (posts, users) = tokio::join!(
|
||||
stream::iter(onee.posts.iter()).then(|(post_idx, shared)| {
|
||||
let owner_id = *post_owner_reverse_lookup.get(&post_idx).unwrap();
|
||||
async move { (owner_id, Post::clone(shared.read().await.deref())) }
|
||||
}).collect(),
|
||||
stream::iter(onee.users.iter()).then(|(_, shared)| async move { User::clone(shared.read().await.deref()) }).collect()
|
||||
);
|
||||
let inner = FreezeInner {
|
||||
posts, users
|
||||
};
|
||||
for (_, post) in read.iter()
|
||||
{
|
||||
sis.posts.push(post.read().await.clone());
|
||||
Freeze {
|
||||
inner,
|
||||
}
|
||||
|
||||
sis
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume into a serialisable image of this store.
|
||||
impl Freeze
|
||||
{
|
||||
/// Create a working `State` from this image.
|
||||
///
|
||||
/// # Fails
|
||||
/// If references to any posts are still held elsewhere.
|
||||
pub fn try_into_freeze(self) -> Result<Freeze, FreezeError>
|
||||
/// This clones all posts and users in the image. Use `into_state` to move into a state.
|
||||
pub fn unfreeze(&self) -> State
|
||||
{
|
||||
let read = self.all;
|
||||
let mut sis = Freeze{
|
||||
posts: Vec::with_capacity(read.len()),
|
||||
};
|
||||
for post in read.into_iter()
|
||||
let mut users = Arena::with_capacity(self.inner.users.len());
|
||||
let mut posts = Arena::with_capacity(self.inner.posts.len());
|
||||
let mut posts_map = HashMap::with_capacity(self.inner.posts.len());
|
||||
let mut users_map = HashMap::with_capacity(self.inner.users.len());
|
||||
let mut posts_user_map = HashMap::with_capacity(self.inner.users.len());
|
||||
|
||||
for (owner_id, post) in self.inner.posts.iter()
|
||||
{
|
||||
sis.posts.push(match Arc::try_unwrap(post) {
|
||||
Ok(val) => val.into_inner(),
|
||||
Err(arc) => return Err(FreezeError{held: Some(arc)}),
|
||||
// Err(_arc) => panic!("Reference to post is still being used"),//arc.read().await.clone(), // something else holds the reference, we can't consume it.
|
||||
});
|
||||
let idx = posts.insert(Arc::new(RwLock::new(post.clone())));
|
||||
posts_user_map.entry(*owner_id).or_insert_with(|| MaybeVec::new()).push(idx);
|
||||
posts_map.insert(*post.post_id(), idx);
|
||||
}
|
||||
|
||||
Ok(sis)
|
||||
for user in self.inner.users.iter()
|
||||
{
|
||||
let idx = users.insert(Arc::new(RwLock::new(user.clone())));
|
||||
users_map.insert(*user.id(), idx);
|
||||
}
|
||||
|
||||
/// Consume into a serialisable image of this store.
|
||||
///
|
||||
/// # Panics
|
||||
/// If references to any posts are still held elsewhere.
|
||||
pub fn into_freeze(self) -> Freeze
|
||||
{
|
||||
self.try_into_freeze().expect("Failed to consume into freeze")
|
||||
State(Arc::new(Inner {
|
||||
oneesan: RwLock::new(Oneesan {
|
||||
users,
|
||||
posts,
|
||||
posts_map,
|
||||
users_map,
|
||||
posts_user_map
|
||||
}),
|
||||
cache: Cache::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Create a new store from a serialisable image of one by cloning each post in it
|
||||
pub fn unfreeze(freeze: &Freeze) -> Self
|
||||
/// Convert this image into a new `State`.
|
||||
pub fn into_state(self) -> State
|
||||
{
|
||||
let mut posts = Arena::new();
|
||||
let mut user_map = HashMap::new();
|
||||
let mut users = Arena::with_capacity(self.inner.users.len());
|
||||
let mut posts = Arena::with_capacity(self.inner.posts.len());
|
||||
let mut posts_map = HashMap::with_capacity(self.inner.posts.len());
|
||||
let mut users_map = HashMap::with_capacity(self.inner.users.len());
|
||||
let mut posts_user_map = HashMap::with_capacity(self.inner.users.len());
|
||||
|
||||
for post in freeze.posts.iter()
|
||||
for (owner_id, post) in self.inner.posts.into_iter()
|
||||
{
|
||||
let idx = posts.insert(Arc::new(RwLock::new(post.clone())));
|
||||
user_map.entry(post.owner().clone())
|
||||
.or_insert_with(|| MaybeVec::new())
|
||||
.push(idx);
|
||||
let post_id = *post.post_id();
|
||||
let idx = posts.insert(Arc::new(RwLock::new(post)));
|
||||
posts_user_map.entry(owner_id).or_insert_with(|| MaybeVec::new()).push(idx);
|
||||
posts_map.insert(post_id, idx);
|
||||
}
|
||||
|
||||
Self {
|
||||
all: posts,
|
||||
user_map,
|
||||
for user in self.inner.users.into_iter()
|
||||
{
|
||||
let user_id = *user.id();
|
||||
let idx = users.insert(Arc::new(RwLock::new(user)));
|
||||
users_map.insert(user_id, idx);
|
||||
}
|
||||
|
||||
State(Arc::new(Inner {
|
||||
cache: Cache::new(),
|
||||
|
||||
oneesan: RwLock::new(Oneesan {
|
||||
users,
|
||||
posts,
|
||||
posts_map,
|
||||
users_map,
|
||||
posts_user_map
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new store by consuming serialisable image of one by cloning each post in it
|
||||
pub fn from_freeze(freeze: Freeze) -> Self
|
||||
impl From<Freeze> for State
|
||||
{
|
||||
#[inline] fn from(from: Freeze) -> Self
|
||||
{
|
||||
let mut posts = Arena::new();
|
||||
let mut user_map = HashMap::new();
|
||||
from.into_state()
|
||||
}
|
||||
}
|
||||
|
||||
for post in freeze.posts.into_iter()
|
||||
#[cfg(test)]
|
||||
mod tests
|
||||
{
|
||||
#[tokio::test]
|
||||
async fn read_write()
|
||||
{
|
||||
let mapping = user_map.entry(post.owner().clone())
|
||||
.or_insert_with(|| MaybeVec::new());
|
||||
let idx = posts.insert(Arc::new(RwLock::new(post)));
|
||||
mapping.push(idx);
|
||||
}
|
||||
let mut buffer = Vec::new();
|
||||
let state = super::State::new();
|
||||
let freeze = state.freeze().await;
|
||||
|
||||
freeze.write_async(&mut buffer).await.unwrap();
|
||||
let freeze2 = super::Freeze::read_async(&mut &buffer[..]).await.unwrap();
|
||||
assert_eq!(freeze, freeze2);
|
||||
|
||||
let mut buffer2 = Vec::new();
|
||||
freeze2.into_write_async(&mut buffer2).await.unwrap();
|
||||
assert_eq!(buffer, buffer2);
|
||||
|
||||
Self {
|
||||
all: posts,
|
||||
user_map,
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn creation()
|
||||
{
|
||||
let state = super::State::new();
|
||||
let freeze = state.freeze().await;
|
||||
let state2 = freeze.unfreeze();
|
||||
let freeze2 = state2.freeze().await;
|
||||
assert_eq!(freeze, freeze2);
|
||||
let _state3 = freeze2.into_state();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,69 +1,196 @@
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{
|
||||
RwLock,
|
||||
mpsc,
|
||||
};
|
||||
use generational_arena::{
|
||||
Arena, Index as ArenaIndex,
|
||||
Arena,Index,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use futures::prelude::*;
|
||||
|
||||
use user::{User, UserID};
|
||||
use post::{Post, PostID};
|
||||
|
||||
pub mod session;
|
||||
pub mod user;
|
||||
pub mod post;
|
||||
pub mod body;
|
||||
mod cache;
|
||||
pub use cache::*;
|
||||
|
||||
mod service; pub use service::*;
|
||||
mod freeze; pub use freeze::*;
|
||||
mod freeze;
|
||||
pub use freeze::*;
|
||||
|
||||
/// An `Arc<RwLock<T>>` wrapper
|
||||
type SharedMut<T> = Arc<RwLock<T>>;
|
||||
|
||||
/// A shared pointer to a post
|
||||
pub type SharedPost = SharedMut<Post>;
|
||||
/// A shared pointer to a user
|
||||
pub type SharedUser = SharedMut<User>;
|
||||
|
||||
/// Entire post state container
|
||||
#[derive(Debug)]
|
||||
pub struct Imouto
|
||||
struct Oneesan
|
||||
{
|
||||
all: Arena<Arc<RwLock<post::Post>>>,
|
||||
user_map: HashMap<user::UserID, MaybeVec<ArenaIndex>>,
|
||||
}
|
||||
users: Arena<Arc<RwLock<User>>>,
|
||||
posts: Arena<Arc<RwLock<Post>>>,
|
||||
|
||||
impl Imouto
|
||||
{
|
||||
/// Create a new empty container
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
all: Arena::new(),
|
||||
user_map: HashMap::new(),
|
||||
}
|
||||
}
|
||||
/// Maps `UserID`s to indexes in the `users` arena.
|
||||
users_map: HashMap<UserID, Index>,
|
||||
/// Maps `PostID`s to indexies in the `posts` arena.
|
||||
posts_map: HashMap<PostID, Index>,
|
||||
|
||||
/// Maps `UserID`s to the user's owned posts in the `posts` arena.
|
||||
posts_user_map: HashMap<UserID, MaybeVec<Index>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Entire program state
|
||||
struct Oneesan
|
||||
struct Inner
|
||||
{
|
||||
posts: RwLock<Imouto>,
|
||||
/// The posts and user state.
|
||||
oneesan: RwLock<Oneesan>,
|
||||
|
||||
/// A non-persistant cache
|
||||
cache: Cache,
|
||||
}
|
||||
|
||||
/// Shares whole program state
|
||||
/// Contains all posts and users
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State(Arc<Oneesan>);
|
||||
pub struct State(Arc<Inner>);
|
||||
|
||||
impl State
|
||||
{
|
||||
/// Create a new empty state.
|
||||
/// Create a new empty state
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self(Arc::new(Oneesan {
|
||||
posts: RwLock::new(Imouto::new()),
|
||||
}))
|
||||
Self(Arc::new(
|
||||
Inner {
|
||||
oneesan: RwLock::new(Oneesan {
|
||||
users: Arena::new(),
|
||||
posts: Arena::new(),
|
||||
|
||||
users_map: HashMap::new(),
|
||||
posts_map: HashMap::new(),
|
||||
|
||||
posts_user_map: HashMap::new(),
|
||||
}),
|
||||
cache: Cache::new(),
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
/// Insert a new user into state.
|
||||
///
|
||||
/// # Locks
|
||||
/// This function holds the state write lock while performing insertions.
|
||||
pub async fn insert_user(&self, user: User) -> SharedUser
|
||||
{
|
||||
let user_id = *user.id();
|
||||
let nuser = Arc::new(RwLock::new(user));
|
||||
let mut write = self.0.oneesan.write().await;
|
||||
let idx = write.users.insert(Arc::clone(&nuser));
|
||||
write.users_map.insert(user_id, idx);
|
||||
nuser
|
||||
}
|
||||
/// Get a reference to the post state container
|
||||
pub async fn imouto(&self) -> tokio::sync::RwLockReadGuard<'_, Imouto>
|
||||
|
||||
/// Insert a new post into state.
|
||||
///
|
||||
/// # Locks
|
||||
/// This function holds the state write lock while performing insertions.
|
||||
pub async fn insert_post(&self, owner: UserID, post: Post) -> SharedPost
|
||||
{
|
||||
self.0.posts.read().await
|
||||
let post_id =*post.post_id();
|
||||
let npost = Arc::new(RwLock::new(post));
|
||||
let mut write = self.0.oneesan.write().await;
|
||||
let idx = write.posts.insert(Arc::clone(&npost));
|
||||
write.posts_map.insert(post_id, idx);
|
||||
write.posts_user_map.entry(owner).or_insert_with(|| MaybeVec::new()).push(idx);
|
||||
npost
|
||||
}
|
||||
/// Get a mutable reference to the post state container
|
||||
pub async fn imouto_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, Imouto>
|
||||
|
||||
/// Get a shared reference to a user by this ID, if it exists.
|
||||
///
|
||||
/// # Locks
|
||||
/// This functions holds the state read lock while performing lookups.
|
||||
/// # Panics
|
||||
/// If the internal ID mappings are invalid
|
||||
pub async fn get_user_by_id(&self, id: UserID) -> Option<SharedUser>
|
||||
{
|
||||
self.0.posts.write().await
|
||||
let read = self.0.oneesan.read().await;
|
||||
read.users_map.get(&id).map(|&idx| read.users.get(idx).unwrap().clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a shared reference to a post by this ID, if it exists.
|
||||
///
|
||||
/// # Locks
|
||||
/// This functions holds the state read lock while performing lookups.
|
||||
/// # Panics
|
||||
/// If the internal ID mappings are invalid
|
||||
pub async fn get_post_by_id(&self, id: PostID) -> Option<SharedPost>
|
||||
{
|
||||
let read = self.0.oneesan.read().await;
|
||||
read.posts_map.get(&id).map(|&idx| read.posts.get(idx).unwrap().clone())
|
||||
}
|
||||
|
||||
/// Consume into a stream that yields all users lazily.
|
||||
///
|
||||
/// # Locks
|
||||
/// The stream producer holds the Oneesan *read* lock until the stream is consumed or dropped.
|
||||
pub fn all_users_stream(self: Arc<Self>) -> impl Stream<Item=SharedUser>
|
||||
{
|
||||
let (mut tx, rx) = mpsc_channel!(defaults::STATE_STREAM_BUFFER_SIZE);
|
||||
tokio::spawn(async move {
|
||||
let onee = self.0.oneesan.read().await;
|
||||
for (_, user) in onee.users.iter()
|
||||
{
|
||||
if tx.send(user.clone()).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
rx
|
||||
}
|
||||
|
||||
|
||||
/// Consume into a stream that yields all posts created by this user lazily.
|
||||
///
|
||||
/// # Locks
|
||||
/// The stream producer holds the Oneesan *read* lock until the stream is consumed or dropped.
|
||||
///
|
||||
/// # Panics
|
||||
/// The background task will panic and drop the producer if the internal ID mappings are invalid
|
||||
pub fn all_posts_by_user_stream(self: Arc<Self>, user: UserID) -> impl Stream<Item=SharedPost>
|
||||
{
|
||||
let (mut tx, rx) = mpsc_channel!(defaults::STATE_STREAM_BUFFER_SIZE);
|
||||
tokio::spawn(async move {
|
||||
let onee = self.0.oneesan.read().await;
|
||||
if let Some(map) = onee.posts_user_map.get(&user) {
|
||||
for &post in map.iter()
|
||||
{
|
||||
if tx.send(onee.posts.get(post).unwrap().clone()).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
rx
|
||||
}
|
||||
|
||||
/// Consume into a stream that yields all posts lazily.
|
||||
///
|
||||
/// # Locks
|
||||
/// The stream producer holds the Oneesan *read* lock until the stream is consumed or dropped.
|
||||
pub fn all_posts_stream(self: Arc<Self>) -> impl Stream<Item=SharedPost>
|
||||
{
|
||||
let (mut tx, rx) = mpsc_channel!(defaults::STATE_STREAM_BUFFER_SIZE);
|
||||
tokio::spawn(async move {
|
||||
let onee = self.0.oneesan.read().await;
|
||||
for (_, post) in onee.posts.iter()
|
||||
{
|
||||
if tx.send(post.clone()).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
rx
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,37 +0,0 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct OpenPost
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ClosedPost
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PostKind
|
||||
{
|
||||
Open(OpenPost),
|
||||
Closed(ClosedPost),
|
||||
}
|
||||
|
||||
/// A post
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Post
|
||||
{
|
||||
owner: user::UserID,
|
||||
kind: PostKind
|
||||
}
|
||||
|
||||
impl Post
|
||||
{
|
||||
/// The ID of the owning user & session of this post.
|
||||
pub fn owner(&self) -> &user::UserID
|
||||
{
|
||||
&self.owner
|
||||
}
|
||||
}
|
@ -1,141 +0,0 @@
|
||||
//! Controls the broadcasting of events sent from the state service task
|
||||
use super::*;
|
||||
|
||||
/// The kind of event outputted from a state service's broadcast stream
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Copy)]
|
||||
pub enum ServiceEventKind
|
||||
{
|
||||
/// Does nothing.
|
||||
///
|
||||
/// # Associated object
|
||||
/// `()`.
|
||||
Ping,
|
||||
/// Does nothing.
|
||||
///
|
||||
/// # Associated object
|
||||
/// None.
|
||||
KeepAlive,
|
||||
}
|
||||
|
||||
cfg_if!{
|
||||
if #[cfg(debug_assertions)] {
|
||||
/// Type used for directed array.
|
||||
/// Currently testing `smolset` over eagerly allocating.
|
||||
pub(super) type SESet<T> = smolset::SmolSet<[T; 1]>;
|
||||
} else {
|
||||
pub(super) type SESet<T> = std::collections::HashSet<T>;
|
||||
}
|
||||
}
|
||||
|
||||
/// An event outputted from a state service's broadcast stream
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServiceEvent
|
||||
{
|
||||
bc_id: BroadcastID,
|
||||
|
||||
kind: ServiceEventKind,
|
||||
directed: Option<SESet<ServiceSubID>>,
|
||||
obj: Option<ServiceEventObject>,
|
||||
}
|
||||
|
||||
impl ServiceEvent
|
||||
{
|
||||
/// Create a new event to be broadcast
|
||||
fn new<T>(kind: ServiceEventKind, directed: Option<impl IntoIterator<Item=ServiceSubID>>, obj: Option<T>) -> Self
|
||||
where T: Any + Send + Sync + 'static
|
||||
{
|
||||
Self {
|
||||
bc_id: BroadcastID::id_new(),
|
||||
kind,
|
||||
directed: directed.map(|x| x.into_iter().collect()).and_then(|n: SESet<_>| if n.len() < 1 {
|
||||
None
|
||||
} else {
|
||||
Some(n)
|
||||
}),
|
||||
obj: obj.map(|x| ServiceEventObject(Arc::new(x))),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline] pub fn id(&self) -> &BroadcastID
|
||||
{
|
||||
&self.bc_id
|
||||
}
|
||||
|
||||
/// The kind of this event.
|
||||
#[inline] pub fn kind(&self) -> ServiceEventKind
|
||||
{
|
||||
self.kind
|
||||
}
|
||||
|
||||
/// Check if this event is for you
|
||||
pub fn is_directed_for(&self, whom: &ServiceSubID) -> bool
|
||||
{
|
||||
if let Some(yes) = self.directed.as_ref() {
|
||||
yes.contains(whom)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
/// Check if this event is directed to anyone
|
||||
pub fn is_directed(&self) -> bool
|
||||
{
|
||||
self.directed.is_some()
|
||||
}
|
||||
|
||||
/// Check who this event is directed to.
|
||||
///
|
||||
/// If it is not directed, an empty slice will be returned.
|
||||
pub fn directed_to(&self) -> impl Iterator<Item = &'_ ServiceSubID> + '_
|
||||
{
|
||||
match self.directed.as_ref()
|
||||
{
|
||||
Some(yes) => MaybeIter::many(yes.iter()),
|
||||
None => MaybeIter::none(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the object, if there is one.
|
||||
pub fn obj_ref(&self) -> Option<&ServiceEventObject>
|
||||
{
|
||||
self.obj.as_ref()
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the object, if there is one.
|
||||
pub fn obj_mut(&mut self) -> Option<&mut ServiceEventObject>
|
||||
{
|
||||
self.obj.as_mut()
|
||||
}
|
||||
|
||||
/// Try to consume into the inner object. If there is no object, return self.
|
||||
pub fn try_into_object(self) -> Result<ServiceEventObject, Self>
|
||||
{
|
||||
match self.obj
|
||||
{
|
||||
Some(obj) => Ok(obj),
|
||||
None => Err(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl From<ServiceEvent> for Option<ServiceEventObject>
|
||||
{
|
||||
#[inline] fn from(from: ServiceEvent) -> Self
|
||||
{
|
||||
from.obj
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ServiceEvent> for ServiceEventObject
|
||||
{
|
||||
type Error = NoObjectError;
|
||||
|
||||
#[inline] fn try_from(from: ServiceEvent) -> Result<Self, Self::Error>
|
||||
{
|
||||
match from.obj
|
||||
{
|
||||
Some(obj) => Ok(obj),
|
||||
None => Err(NoObjectError),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
//! Global state service
|
||||
use super::*;
|
||||
use tokio::{
|
||||
sync::{
|
||||
watch,
|
||||
mpsc,
|
||||
oneshot,
|
||||
broadcast,
|
||||
},
|
||||
task::JoinHandle,
|
||||
};
|
||||
use crate::service::{
|
||||
ExitStatus,
|
||||
};
|
||||
use std::{error, fmt};
|
||||
use std::sync::Weak;
|
||||
use std::any::Any;
|
||||
use std::collections::{BTreeMap};
|
||||
|
||||
|
||||
id_type!(ServiceSubID; "Optional ID for filtering directed broadcast messages");
|
||||
id_type!(BroadcastID; "Each broadcast message has a unique ID.");
|
||||
|
||||
mod supervisor; pub use supervisor::*;
|
||||
mod resreq; pub use resreq::*;
|
||||
mod obj; pub use obj::*;
|
||||
|
||||
mod events; pub use events::*;
|
@ -1,114 +0,0 @@
|
||||
//! broadcast object definitions
|
||||
use super::*;
|
||||
|
||||
|
||||
/// Object sent through the broadcast channel.
|
||||
///
|
||||
/// These objects can be cloned and downcasted, becaause they are atomically refcounted if that is more desireable.
|
||||
#[derive(Clone)]
|
||||
#[repr(transparent)]
|
||||
pub struct ServiceEventObject(pub(super) Arc<dyn Any + Send + Sync + 'static>);
|
||||
shim_debug!(ServiceEventObject);
|
||||
|
||||
/// A weak reference to a `ServiceEventObject`.
|
||||
#[derive(Clone)]
|
||||
#[repr(transparent)]
|
||||
pub struct ServiceEventObjectRef(pub(super) Weak<dyn Any + Send + Sync + 'static>);
|
||||
shim_debug!(ServiceEventObjectRef);
|
||||
|
||||
impl ServiceEventObjectRef
|
||||
{
|
||||
/// Try to upgrade to a concrete reference, and then clone the inner object.
|
||||
pub fn try_clone(&self) -> Option<ServiceEventObject>
|
||||
{
|
||||
match self.0.upgrade()
|
||||
{
|
||||
Some(arc) => Some(ServiceEventObject(arc).clone()),
|
||||
None => None
|
||||
}
|
||||
}
|
||||
/// Try to upgrade to a concrete reference.
|
||||
pub fn upgrade(self) -> Result<ServiceEventObject, Self>
|
||||
{
|
||||
match self.0.upgrade()
|
||||
{
|
||||
Some(arc) => Ok(ServiceEventObject(arc)),
|
||||
None => Err(self),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the object has not been destroyed yet.
|
||||
pub fn is_alive(&self) -> bool
|
||||
{
|
||||
self.0.strong_count() > 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl ServiceEventObject
|
||||
{
|
||||
pub fn clone_inner(&self) -> Self
|
||||
{
|
||||
Self(Arc::from(self.0.clone_dyn_any_sync()))
|
||||
}
|
||||
/// Get a weak reference counted handle to the object, without cloning the object itself.
|
||||
pub fn clone_weak(&self) -> ServiceEventObjectRef
|
||||
{
|
||||
ServiceEventObjectRef(Arc::downgrade(&self.0))
|
||||
}
|
||||
|
||||
/// Try to downcast the inner object to a concrete type and then clone it.
|
||||
///
|
||||
/// This will fail if:
|
||||
/// * The downcasted type is invalid
|
||||
#[inline] pub fn downcast_clone<T: Any + Clone + Send + Sync + 'static>(&self) -> Option<T>
|
||||
{
|
||||
self.downcast_ref::<T>().map(|x| *x.clone_dyn_any().downcast().unwrap())
|
||||
}
|
||||
/// Try to consume this instance into downcast.
|
||||
///
|
||||
/// This will fail if:
|
||||
/// * The downcasted type is invalid
|
||||
/// * There are other references to this object (created through `clone_ref()`.).
|
||||
pub fn try_into_downcast<T: Any + Send + Sync + 'static>(self) -> Result<T, Self>
|
||||
{
|
||||
match Arc::downcast(self.0)
|
||||
{
|
||||
Ok(v) => match Arc::try_unwrap(v) {
|
||||
Ok(v) => Ok(v),
|
||||
Err(s) => Err(Self(s)),
|
||||
},
|
||||
Err(e) => Err(Self(e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if there are any other references to this object
|
||||
#[inline] pub fn is_unique(&self) -> bool
|
||||
{
|
||||
Arc::strong_count(&self.0) == 1
|
||||
}
|
||||
|
||||
/// Try to downcast the object into a concrete type
|
||||
#[inline] pub fn is<T: Any + Send + Sync + 'static>(&self) -> bool
|
||||
{
|
||||
self.0.as_ref().is::<T>()
|
||||
}
|
||||
/// Try to downcast the object into a concrete type
|
||||
#[inline] pub fn downcast_ref<T: Any + Send + Sync + 'static>(&self) -> Option<&T>
|
||||
{
|
||||
self.0.as_ref().downcast_ref::<T>()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Error returned from trying to extract an object from a `ServiceEvent` which has none.
|
||||
pub struct NoObjectError;
|
||||
|
||||
impl error::Error for NoObjectError{}
|
||||
impl fmt::Display for NoObjectError
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
||||
{
|
||||
write!(f, "there was no object broadcasted along with this ")
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
//! Responses and requests for the state service(s).
|
||||
//!
|
||||
//! These are sent to `Supervisor` which then dispatches them accordingly.
|
||||
use super::*;
|
||||
|
||||
|
||||
/// The kind of request to send to the the service
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum ServiceRequestKind
|
||||
{
|
||||
/// A no-op request.
|
||||
None,
|
||||
|
||||
/// Test request.
|
||||
#[cfg(debug_assertions)] EchoRequest(String),
|
||||
}
|
||||
|
||||
/// The kind of response to expect from a service query, if any.
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum ServiceResponseKind
|
||||
{
|
||||
/// Test response.
|
||||
#[cfg(debug_assertions)] EchoResponse(String),
|
||||
|
||||
/// Empty response
|
||||
None,
|
||||
}
|
||||
|
||||
/// A response from a service to a specific query.
|
||||
///
|
||||
/// It is sent theough the `output` onehot channel in the `ServiceCommand` struct.
|
||||
#[derive(Debug)]
|
||||
pub struct ServiceResponse(ServiceRequestKind);
|
||||
|
||||
impl ServiceResponse
|
||||
{
|
||||
/// An empty (default) response
|
||||
#[inline] pub const fn none() -> Self
|
||||
{
|
||||
Self(ServiceRequestKind::None)
|
||||
}
|
||||
}
|
||||
|
||||
/// A formed service request.
|
||||
#[derive(Debug)]
|
||||
pub struct ServiceRequest
|
||||
{
|
||||
kind: ServiceRequestKind,
|
||||
output: oneshot::Sender<ServiceResponse>, // If there is no response, this sender will just be dropped and the future impl can return `None` instead of `Some(response)`.
|
||||
}
|
||||
|
||||
impl ServiceRequest
|
||||
{
|
||||
/// Create a new request
|
||||
pub(in super) fn new(kind: ServiceRequestKind) -> (Self, oneshot::Receiver<ServiceResponse>)
|
||||
{
|
||||
let (tx, rx) = oneshot::channel();
|
||||
(Self {
|
||||
kind,
|
||||
output: tx
|
||||
}, rx)
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
//! Dispatching to state service task(s) through a supervisor
|
||||
use super::*;
|
||||
use tokio::time;
|
||||
use std::{fmt, error};
|
||||
use futures::prelude::*;
|
||||
|
||||
impl Supervisor
|
||||
{
|
||||
/// Dispatch a request to the supervisor to be passed through to a subtask.
|
||||
///
|
||||
/// # Returns
|
||||
/// Returns a `Future` that can be awaited on to produce the value sent back by the task (if there is one).
|
||||
///
|
||||
/// # Errors
|
||||
/// * The first failure will be caused if sending to the supervisor fails.
|
||||
/// * The 2nd failure will be caused if either the supervisor, or its delegated task panics before being able to respond, or if the task simply does not respond.
|
||||
pub async fn dispatch_req(&mut self, kind: ServiceRequestKind) -> Result<impl Future<Output=Result<ServiceResponse, SupervisorDispatchError>> + 'static, SupervisorDispatchError>
|
||||
{
|
||||
let (req, rx) = ServiceRequest::new(kind);
|
||||
self.pipe.send(req).await.map_err(|_| SupervisorDispatchError::Send)?;
|
||||
Ok(rx.map_err(|_| SupervisorDispatchError::Recv))
|
||||
}
|
||||
|
||||
/// Dispatch a request to the supervisor to be passed through to a subtask and then wait for a response from it.
|
||||
///
|
||||
/// # Returns
|
||||
/// Returns the value sent back by the task, if there is one
|
||||
pub async fn dispatch_and_wait(&mut self, kind: ServiceRequestKind) -> Result<ServiceResponse, SupervisorDispatchError>
|
||||
{
|
||||
Ok(self.dispatch_req(kind)
|
||||
.await?
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Dispatch a request to the supervisor to be passed through to a subtask and then wait for a response from it.
|
||||
/// If the timeout expires before a response from the server is received, then the operation will cancel and the error returned will be `SupervisorDispatchError::Timeout`.
|
||||
///
|
||||
/// # Returns
|
||||
/// Returns the value sent back by the task, if there is one
|
||||
pub async fn dispatch_and_wait_timeout(&mut self, kind: ServiceRequestKind, timeout: time::Duration) -> Result<ServiceResponse, SupervisorDispatchError>
|
||||
{
|
||||
let resp_wait = self.dispatch_req(kind)
|
||||
.await?;
|
||||
tokio::select! {
|
||||
val = resp_wait => {
|
||||
return Ok(val?);
|
||||
}
|
||||
_ = time::delay_for(timeout) => {
|
||||
return Err(SupervisorDispatchError::Timeout("receiving response", Some(timeout)))
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Dispatch a request to the supervisor to be passed through to a subtask and then wait for a response from it.
|
||||
/// If the future `until` completes before a response from the server is received, then the operation will cancel and the error returned will be `SupervisorDispatchError::Timeout`.
|
||||
///
|
||||
/// # Returns
|
||||
/// Returns the value sent back by the task, if there is one
|
||||
pub async fn dispatch_and_wait_until(&mut self, kind: ServiceRequestKind, until: impl Future) -> Result<ServiceResponse, SupervisorDispatchError>
|
||||
{
|
||||
let resp_wait = self.dispatch_req(kind)
|
||||
.await?;
|
||||
tokio::select! {
|
||||
val = resp_wait => {
|
||||
return Ok(val?);
|
||||
}
|
||||
_ = until => {
|
||||
return Err(SupervisorDispatchError::Timeout("receiving response", None))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error when dispatching a request to the supervisor
|
||||
#[derive(Debug)]
|
||||
pub enum SupervisorDispatchError
|
||||
{
|
||||
Send, Recv, Timeout(&'static str, Option<tokio::time::Duration>),
|
||||
}
|
||||
|
||||
impl error::Error for SupervisorDispatchError{}
|
||||
impl fmt::Display for SupervisorDispatchError
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
||||
{
|
||||
match self {
|
||||
Self::Send => write!(f, "dispatching the request failed"),
|
||||
Self::Recv => write!(f, "receiving the response failed"),
|
||||
Self::Timeout(msg, Some(duration)) => write!(f, "timeout on {} was reached ({:?})", msg, duration),
|
||||
Self::Timeout(msg, _) => write!(f, "timeout on {} was reached", msg),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,309 +0,0 @@
|
||||
//! Handles spawning and restarting service task(s)
|
||||
use super::*;
|
||||
use tokio::sync::RwLock;
|
||||
use std::mem::MaybeUninit;
|
||||
use std::ops;
|
||||
use futures::prelude::*;
|
||||
use tokio::time;
|
||||
use std::{fmt, error};
|
||||
|
||||
const SUPERVISOR_BACKLOG: usize = 32;
|
||||
|
||||
mod dispatch; pub use dispatch::*;
|
||||
|
||||
TODO: This all needs redoing when i'm actually lucid. This part seems okay but the rest of `service` needs to go and be replaced by something like this
|
||||
|
||||
/// Signal the shutdown method to the supervisor.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Copy)]
|
||||
pub enum SupervisorControl
|
||||
{
|
||||
/// Normal working
|
||||
Initialise,
|
||||
/// Signal the subtask(s) to shut down, then wait for them and exit, with an optional timeout.
|
||||
///
|
||||
/// # Notes
|
||||
/// If the timeout expires while waiting, then the mode is switched to `Drop`.
|
||||
Signal(Option<time::Duration>),
|
||||
/// Drop all handles and pipes to subtask(s) then immediately exit.
|
||||
Drop,
|
||||
/// Restart any and all subtask(s)
|
||||
Restart,
|
||||
|
||||
/// Set the max task limit. Default is 0.
|
||||
TaskLimit(usize),
|
||||
}
|
||||
|
||||
impl Default for SupervisorControl
|
||||
{
|
||||
#[inline]
|
||||
fn default() -> Self
|
||||
{
|
||||
Self::Initialise
|
||||
}
|
||||
}
|
||||
|
||||
/// Supervisor responsible for spawning the state handler service.
|
||||
#[derive(Debug)]
|
||||
pub(super) struct Supervisor
|
||||
{
|
||||
/// Handle for the supervisor task itself
|
||||
handle: JoinHandle<ExitStatus>,
|
||||
|
||||
/// Watch sender for signalling shutdowns the supervisor task itself
|
||||
shutdown: watch::Sender<SupervisorControl>,
|
||||
|
||||
/// The pipe to send requests to the supervisor's subtasks
|
||||
pipe: mpsc::Sender<ServiceRequest>,
|
||||
|
||||
/// The initial receiver created from `broadcast_root`.
|
||||
broadcast_receiver: broadcast::Receiver<ServiceEvent>,
|
||||
|
||||
/// Data shared between the supervisor's task and its controller instance here.
|
||||
shared: Arc<SupervisorShared>,
|
||||
}
|
||||
|
||||
/// Object shared btweeen the Supervisor control instance and its supervisor task.
|
||||
#[derive(Debug)]
|
||||
struct SupervisorShared
|
||||
{
|
||||
/// this is for filtering specific messages to specific subscribers
|
||||
sub: RwLock<BTreeMap<ServiceEventKind, SESet<ServiceSubID>>>,
|
||||
|
||||
broadcast_root: broadcast::Sender<ServiceEvent>,
|
||||
state: state::State,
|
||||
}
|
||||
|
||||
/// A subscriber to supervisor task(s) event pump
|
||||
#[derive(Debug)]
|
||||
pub struct Subscriber
|
||||
{
|
||||
id: ServiceSubID,
|
||||
|
||||
/// For directed messages
|
||||
spec: mpsc::Receiver<ServiceEvent>,
|
||||
/// For broadcast messages
|
||||
broad: broadcast::Receiver<ServiceEvent>,
|
||||
}
|
||||
|
||||
|
||||
impl Supervisor
|
||||
{
|
||||
/// Attempt to send a control signal to the supervisor itself
|
||||
pub fn signal_control(&self, sig: SupervisorControl) -> Result<(), watch::error::SendError<SupervisorControl>>
|
||||
{
|
||||
self.shutdown.broadcast(sig)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Drop all communications with background worker and wait for it to complete
|
||||
pub async fn join_now(self) -> eyre::Result<()>
|
||||
{
|
||||
let handle = {
|
||||
let Self { handle, ..} = self; // drop everything else
|
||||
handle
|
||||
};
|
||||
handle.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if the background worker has not been dropped.
|
||||
///
|
||||
/// If this returns false it usually indicates a fatal error.
|
||||
pub fn is_alive(&self) -> bool
|
||||
{
|
||||
Arc::strong_count(&self.shared) > 1
|
||||
}
|
||||
|
||||
/// Create a new supervisor for this state.
|
||||
pub fn new(state: state::State) -> Self
|
||||
{
|
||||
let shutdown = watch::channel(Default::default());
|
||||
let pipe = mpsc::channel(SUPERVISOR_BACKLOG);
|
||||
let (broadcast_root, broadcast_receiver) = broadcast::channel(SUPERVISOR_BACKLOG);
|
||||
|
||||
let shared = Arc::new(SupervisorShared{
|
||||
broadcast_root,
|
||||
state,
|
||||
});
|
||||
|
||||
let (shutdown_0, shutdown_1) = shutdown;
|
||||
let (pipe_0, pipe_1) = pipe;
|
||||
|
||||
Self {
|
||||
shutdown: shutdown_0,
|
||||
pipe: pipe_0,
|
||||
broadcast_receiver,
|
||||
shared: Arc::clone(&shared),
|
||||
|
||||
handle: tokio::spawn(async move {
|
||||
let shared = shared;
|
||||
ExitStatus::from(service_fn(SupervisorTaskState {
|
||||
shared,
|
||||
recv: pipe_1,
|
||||
shutdown: shutdown_1,
|
||||
}).await.or_else(|err| err.into_own_result()))
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The state held by the running superviser service
|
||||
#[derive(Debug)]
|
||||
struct SupervisorTaskState
|
||||
{
|
||||
shutdown: watch::Receiver<SupervisorControl>,
|
||||
recv: mpsc::Receiver<ServiceRequest>,
|
||||
shared: Arc<SupervisorShared>,
|
||||
}
|
||||
|
||||
/// Detached supervisor server
|
||||
async fn service_fn(SupervisorTaskState {shared, mut recv, mut shutdown}: SupervisorTaskState) -> Result<(), ServiceTerminationError>
|
||||
{
|
||||
impl Default for TerminationKind
|
||||
{
|
||||
#[inline]
|
||||
fn default() -> Self
|
||||
{
|
||||
Self::Graceful
|
||||
}
|
||||
}
|
||||
|
||||
// The command stream to dispatch to the worker tasks
|
||||
let command_dispatch = async {
|
||||
while let Some(req) = recv.recv().await {
|
||||
todo!("Dispatch to child(s)");
|
||||
}
|
||||
TerminationKind::Graceful
|
||||
};
|
||||
tokio::pin!(command_dispatch);
|
||||
|
||||
// The signal stream to be handled here
|
||||
let signal_stream = async {
|
||||
while let Some(value) = shutdown.recv().await
|
||||
{
|
||||
use SupervisorControl::*;
|
||||
match value {
|
||||
Initialise => info!("Initialised"),
|
||||
Signal(None) => return TerminationKind::SignalHup,
|
||||
Signal(Some(to)) => return TerminationKind::SignalTimeout(to),
|
||||
Drop => return TerminationKind::Immediate,
|
||||
Restart => todo!("not implemented"),
|
||||
TaskLimit(_limit) => todo!("not implemented"),
|
||||
}
|
||||
}
|
||||
TerminationKind::Graceful
|
||||
};
|
||||
tokio::pin!(signal_stream);
|
||||
|
||||
//loop {
|
||||
tokio::select! {
|
||||
sd_kind = &mut signal_stream => {
|
||||
// We received a signal
|
||||
|
||||
Err(ServiceTerminationError::Signal(sd_kind))
|
||||
}
|
||||
disp_end = &mut command_dispatch => {
|
||||
// The command dispatch exited first, the logical error is `Graceful`. But it's not really an error, so...
|
||||
disp_end.into()
|
||||
}
|
||||
}
|
||||
// }
|
||||
|
||||
}
|
||||
/// The mannor in which the supervisor exited.
|
||||
#[derive(Debug)]
|
||||
pub enum TerminationKind
|
||||
{
|
||||
/// The child task(s) were signalled to stop and they were waited on.
|
||||
SignalHup,
|
||||
/// If there was a timeout specified, and that timeout expired, the message will be `SignalTimeout` instead of `SignalHup`.
|
||||
SignalTimeout(time::Duration),
|
||||
/// Immediately drop everything and exit
|
||||
Immediate,
|
||||
/// A non-signalled shutdown. There were no more watchers for the shutdown channel.
|
||||
Graceful,
|
||||
}
|
||||
|
||||
impl TerminationKind
|
||||
{
|
||||
/// Convert `TerminationKind::Graceful` into a non-error
|
||||
fn strip_grace(self) -> Result<(), Self>
|
||||
{
|
||||
match self {
|
||||
Self::Graceful => Ok(()),
|
||||
e => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for TerminationKind{}
|
||||
impl fmt::Display for TerminationKind
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
||||
{
|
||||
match self {
|
||||
Self::SignalHup => write!(f, "children were signalled to shut down and compiled"),
|
||||
Self::SignalTimeout(to) => write!(f, "children were signalled to shut but did not do so within the {:?} timeout", to),
|
||||
Self::Immediate => write!(f, "children were dropped and an immediate exit was made"),
|
||||
Self::Graceful => write!(f, "a graceful shutdown order was issued and compiled with"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The error returned on a failed service termination.
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum ServiceTerminationError
|
||||
{
|
||||
/// Was terminated by a signal.
|
||||
Signal(TerminationKind),
|
||||
/// Was terminated by a panic.
|
||||
Panic,
|
||||
/// There were no more commands being sent through, and the worker gracefully shut down.
|
||||
Interest,
|
||||
}
|
||||
|
||||
impl From<TerminationKind> for Result<(), ServiceTerminationError>
|
||||
{
|
||||
fn from(from: TerminationKind) -> Self
|
||||
{
|
||||
ServiceTerminationError::Signal(from).into_own_result()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl ServiceTerminationError
|
||||
{
|
||||
fn into_own_result(self) -> Result<(), Self>
|
||||
{
|
||||
match self {
|
||||
Self::Signal(term) => term.strip_grace().map_err(Self::Signal),
|
||||
x => Err(x),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for ServiceTerminationError
|
||||
{
|
||||
fn source(&self) -> Option<&(dyn error::Error + 'static)>
|
||||
{
|
||||
Some(match &self {
|
||||
Self::Signal(ts) => ts,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl fmt::Display for ServiceTerminationError
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
||||
{
|
||||
match self {
|
||||
Self::Signal(_) => write!(f, "shut down by signal"),
|
||||
Self::Panic => write!(f, "shut down by panic. this is usually fatal"),
|
||||
Self::Interest => write!(f, "all communications with this service stopped"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,145 +0,0 @@
|
||||
//! Session for each connected user
|
||||
use super::*;
|
||||
use tokio::{
|
||||
sync::{
|
||||
mpsc,
|
||||
broadcast,
|
||||
oneshot,
|
||||
},
|
||||
task::JoinHandle,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use crate::service::{self, SubscribeError};
|
||||
|
||||
id_type!(SessionID; "A unique session ID, not bound to a user.");
|
||||
|
||||
impl SessionID
|
||||
{
|
||||
/// Generate a random session ID.
|
||||
#[inline] fn generate() -> Self
|
||||
{
|
||||
Self::id_new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SessionResponse
|
||||
{
|
||||
Closed(SessionID),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SessionCommand
|
||||
{
|
||||
Shutdown,
|
||||
/// Subscribe to the session's message pump.
|
||||
Subscribe(oneshot::Sender<broadcast::Receiver<SessionResponse>>),
|
||||
|
||||
/// Take this websocket connection.
|
||||
//TODO: websockets
|
||||
Connect(!),
|
||||
}
|
||||
|
||||
/// Metadata for a session, scored across its service and handle(s)
|
||||
#[derive(Debug)]
|
||||
struct SessionMetadata
|
||||
{
|
||||
id: SessionID,
|
||||
user: user::User,
|
||||
|
||||
}
|
||||
|
||||
/// A single connected session.
|
||||
/// Hold the service for this session, its ID, and (TODO) its websocket connection.
|
||||
#[derive(Debug)]
|
||||
pub struct Session
|
||||
{
|
||||
meta: Arc<SessionMetadata>,
|
||||
|
||||
tx: mpsc::Sender<SessionCommand>,
|
||||
rx: broadcast::Receiver<SessionResponse>,
|
||||
handle: JoinHandle<()>,
|
||||
}
|
||||
|
||||
async fn service_task(meta: Arc<SessionMetadata>, state: state::State, response_pair: (broadcast::Sender<SessionResponse>, mpsc::Receiver<SessionCommand>))
|
||||
{
|
||||
let (tx, mut rx) = response_pair;
|
||||
while let Some(command) = rx.recv().await
|
||||
{
|
||||
match command
|
||||
{
|
||||
SessionCommand::Shutdown => break,
|
||||
SessionCommand::Subscribe(out) => ignore!(out.send(tx.subscribe())),
|
||||
|
||||
_ => todo!()
|
||||
}
|
||||
}
|
||||
|
||||
let _ = tx.send(SessionResponse::Closed(meta.id.clone()));
|
||||
}
|
||||
|
||||
impl Session
|
||||
{
|
||||
/// Create a new session object
|
||||
pub fn create(user: user::User, state: state::State) -> Self
|
||||
{
|
||||
let id = SessionID::generate();
|
||||
let meta =Arc::new(SessionMetadata{
|
||||
user,
|
||||
id,
|
||||
});
|
||||
let (handle, tx, rx) = {
|
||||
let (s_tx, s_rx) = broadcast::channel(16);
|
||||
let (r_tx, r_rx) = mpsc::channel(16);
|
||||
(tokio::spawn(service_task(Arc::clone(&meta), state, (s_tx, r_rx))),
|
||||
r_tx, s_rx)
|
||||
};
|
||||
Self {
|
||||
meta,
|
||||
handle,
|
||||
tx, rx,
|
||||
}
|
||||
}
|
||||
/// The randomly generated ID of this session, irrespective of the user of this session.
|
||||
#[inline] pub fn session_id(&self) -> &SessionID
|
||||
{
|
||||
&self.meta.id
|
||||
}
|
||||
/// The unique user ID of this session
|
||||
pub fn user_id(&self) -> user::UserID
|
||||
{
|
||||
self.meta.user.id_for_session(self)
|
||||
}
|
||||
|
||||
/// Ask the service to subscribe to it.
|
||||
pub async fn subscribe(&mut self) -> Result<broadcast::Receiver<SessionResponse>, SubscribeError>
|
||||
{
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.tx.send(SessionCommand::Subscribe(tx)).await.map_err(|_| SubscribeError::SenderDropped)?;
|
||||
|
||||
rx.await.map_err(|_| SubscribeError::NoResponse)
|
||||
}
|
||||
}
|
||||
|
||||
// impl service::Service for Session
|
||||
// {
|
||||
// type Message = SessionCommand;
|
||||
// type Response = SessionResponse;
|
||||
|
||||
// #[inline] fn wait_on(self) -> JoinHandle<()> {
|
||||
// self.handle
|
||||
// }
|
||||
// #[inline] fn message_in_ref(&self) -> &mpsc::Sender<Self::Message> {
|
||||
// &self.tx
|
||||
// }
|
||||
// #[inline] fn message_in(&mut self) -> &mut mpsc::Sender<Self::Message> {
|
||||
// &mut self.tx
|
||||
// }
|
||||
// #[inline] fn message_out(&mut self) -> &mut broadcast::Receiver<Self::Response> {
|
||||
// &mut self.rx
|
||||
// }
|
||||
|
||||
// #[inline] fn is_alive(&self) -> Option<bool> {
|
||||
// Some(Arc::strong_count(&self.meta) > 1)
|
||||
// }
|
||||
// }
|
@ -1,106 +0,0 @@
|
||||
//! Used to determine which post belongs to who.
|
||||
//!
|
||||
//! Mostly for determining if a poster owns a post.
|
||||
use super::*;
|
||||
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
};
|
||||
use cryptohelpers::sha256;
|
||||
|
||||
/// A user's unique ID.
|
||||
///
|
||||
/// This is composed by the user's address and their session ID.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct UserID(SocketAddr, session::SessionID);
|
||||
|
||||
static COUNTER: GlobalCounter = GlobalCounter::new();
|
||||
|
||||
impl UserID
|
||||
{
|
||||
/// Generate a token from this instance.
|
||||
///
|
||||
/// User tokens are deterministically generated and can be deterministically verified.
|
||||
pub fn generate_token(&self) -> u64
|
||||
{
|
||||
let cnt = COUNTER.get();
|
||||
let mut trunc = [0u8; std::mem::size_of::<u64>()];
|
||||
|
||||
let hash = GloballySalted::new(self).compute_sha256_hash();
|
||||
bytes::move_slice(&mut trunc[..], hash.as_ref());
|
||||
|
||||
u64::from_le_bytes(trunc) ^ cnt
|
||||
}
|
||||
|
||||
/// Validate a token for this instance created with `generate_token`.
|
||||
pub fn validate_token(&self, val: u64) -> bool
|
||||
{
|
||||
let mut trunc = [0u8; std::mem::size_of::<u64>()];
|
||||
|
||||
let hash = GloballySalted::new(self).compute_sha256_hash();
|
||||
bytes::move_slice(&mut trunc[..], hash.as_ref());
|
||||
|
||||
COUNTER.valid(u64::from_le_bytes(trunc) ^ val)
|
||||
}
|
||||
}
|
||||
|
||||
/// A user not bound to a session.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
|
||||
pub struct User
|
||||
{
|
||||
addr: SocketAddr,
|
||||
}
|
||||
|
||||
impl User
|
||||
{
|
||||
/// Get the user ID for this session.
|
||||
pub fn id_for_session(&self, session: &session::Session) -> UserID
|
||||
{
|
||||
UserID(self.addr, session.session_id().clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests
|
||||
{
|
||||
use super::*;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time;
|
||||
use std::net::SocketAddrV4;
|
||||
use std::net::Ipv4Addr;
|
||||
#[tokio::test]
|
||||
async fn counter_tokens()
|
||||
{
|
||||
let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 80));
|
||||
let usr = User{addr};
|
||||
let ses = session::Session::create(usr);
|
||||
|
||||
let id = ses.user_id();
|
||||
|
||||
let (mut tx, mut rx) = mpsc::channel(5);
|
||||
let task = tokio::spawn(async move {
|
||||
let id = ses.user_id();
|
||||
|
||||
while let Some(token) = rx.recv().await {
|
||||
if !id.validate_token(token) {
|
||||
panic!("Failed to validate token {:x} for id {:?}", token, id);
|
||||
} else {
|
||||
eprintln!("Token {:x} valid for id {:?}", token, id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for x in 1..=10
|
||||
{
|
||||
if x % 2 == 0 {
|
||||
time::delay_for(time::Duration::from_millis(10 * x)).await;
|
||||
}
|
||||
if tx.send(id.generate_token()).await.is_err() {
|
||||
eprintln!("Failed to send to task");
|
||||
break;
|
||||
}
|
||||
}
|
||||
drop(tx);
|
||||
task.await.expect("Background validate task failed");
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
use super::*;
|
||||
use maud::{
|
||||
Markup,
|
||||
Render,
|
||||
};
|
||||
use maud::DOCTYPE;
|
||||
|
||||
/// Posts view page template
|
||||
mod view
|
||||
{
|
||||
use super::*;
|
||||
|
||||
/// Head for the view page template
|
||||
pub fn head() -> Markup
|
||||
{
|
||||
html! {
|
||||
//TODO
|
||||
}
|
||||
}
|
||||
|
||||
pub fn body(state: &state::State) -> Markup
|
||||
{
|
||||
html! {
|
||||
//TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline] fn page(head: impl Render, body: impl Render) -> Markup
|
||||
{
|
||||
html! {
|
||||
(DOCTYPE)
|
||||
head {
|
||||
(head)
|
||||
}
|
||||
body {
|
||||
main {
|
||||
(body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Post view page
|
||||
pub fn view(state: &state::State) -> Markup
|
||||
{
|
||||
//TODO: Create one-time-use token system for rendering page. Insert into state's one-time-use tokens
|
||||
page(view::head(), view::body(state))
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
//! User related things
|
||||
use super::*;
|
||||
use cryptohelpers::{
|
||||
rsa::{
|
||||
RsaPublicKey,
|
||||
Signature,
|
||||
},
|
||||
sha256::{self, Sha256Hash},
|
||||
};
|
||||
use std::borrow::Borrow;
|
||||
use std::hash::{Hasher, Hash};
|
||||
|
||||
id_type!(UserID; "A unique user iD");
|
||||
|
||||
/// The salt added to to the user ID hash to be signed by the user's private key.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct UserSalt([u8; 16]);
|
||||
|
||||
impl UserSalt
|
||||
{
|
||||
/// Generate a new random salt.
|
||||
pub fn generate() -> Self
|
||||
{
|
||||
let mut ar = [0u8; 16];
|
||||
getrandom::getrandom(&mut ar[..]).expect("rng fatal");
|
||||
Self(ar)
|
||||
}
|
||||
}
|
||||
|
||||
impl UserID
|
||||
{
|
||||
/// SHA256 hash this ID with a salt
|
||||
pub fn hash_with_salt(&self, salt: &UserSalt) -> Sha256Hash
|
||||
{
|
||||
sha256::compute_slice_iter(iter![&self.0.as_bytes()[..], &salt.0[..]])
|
||||
}
|
||||
|
||||
/// Generate a new salt and then return that salt and this ID hashed with that new salt.
|
||||
///
|
||||
/// This salt should be
|
||||
pub fn generate_hash(&self) -> (UserSalt, Sha256Hash)
|
||||
{
|
||||
let salt = UserSalt::generate();
|
||||
let hash = self.hash_with_salt(&salt);
|
||||
(salt, hash)
|
||||
}
|
||||
}
|
||||
|
||||
/// A user identifier.
|
||||
///
|
||||
/// Contains the user's unique ID, their public key(s), and a valid signature of the sha256 hash of the user's ID + a random salt.
|
||||
///
|
||||
/// # Hash
|
||||
/// This type hashes to its unique ID, and also borrows to its unique ID.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct User
|
||||
{
|
||||
/// The user's unique ID.
|
||||
id: UserID,
|
||||
|
||||
/// A optional set of identifiers given by the user. The user must be trusted to set/edit this value.
|
||||
ident: post::Ident,
|
||||
|
||||
/// The public keys associated with this user.
|
||||
///
|
||||
/// # Trust
|
||||
/// Each public key must have a corresponding signature in its complemental entry in `id_sig` to be considered trusted.
|
||||
/// A user with no trusted public keys can be anyone or multiple people. This is not *disallowed* usually but should be discouraged.
|
||||
///
|
||||
/// Users are only considered trusted if they have at least one trusted public key.
|
||||
pubkey: Vec<RsaPublicKey>,
|
||||
|
||||
/// This vector contains the complemental signature (and salt used with `id` to produce the signed hash) to the public keys in `pubkey`. Each element of `pubkey` must have a complemental element in this vector.
|
||||
///
|
||||
/// # Trusted public keys
|
||||
/// `None` values for this are signatures that have not yet been produces for a given salt, and do not count as complete. Public keys in `pubkey` that do not have a corresponding `Some` signature value in this field should not be trusted.
|
||||
id_sig: Vec<(UserSalt, Option<Signature>)>,
|
||||
}
|
||||
|
||||
impl Hash for User {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.id.hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Borrow<UserID> for User
|
||||
{
|
||||
fn borrow(&self) -> &UserID
|
||||
{
|
||||
&self.id
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl User
|
||||
{
|
||||
/// Is this user a trusted user?
|
||||
///
|
||||
/// Does this user have at least one trusted public key (they have produced a valid signature specified in `id_sig`).
|
||||
pub fn is_trusted(&self) -> eyre::Result<bool>
|
||||
{
|
||||
for (i, (key, (salt, sig))) in (0..).zip(self.pubkey.iter().zip(self.id_sig.iter()))
|
||||
{
|
||||
if let Some(sig) = sig {
|
||||
let hash = self.id.hash_with_salt(salt);
|
||||
if sig.verify_slice(&hash, key)
|
||||
.with_section(move || format!("{:?}", key).header("Public key was"))
|
||||
.with_section(move || format!("{:?}", sig).header("Signature was"))
|
||||
.with_section(move || format!("{:?}", salt).header("Salt was"))
|
||||
.with_section(move || format!("{:?}", hash).header("Hash was"))
|
||||
.with_note(|| i.to_string().header("For pubkey"))
|
||||
.with_note(|| format!("{:?} ({:?})", self.id, self.ident).header("For user"))
|
||||
.with_warning(|| "This could indicate key or signature corruption. This key or signature may need to be removed.")
|
||||
.with_suggestion(|| "If the user is unable to produce a verifyable signature for this public key despite haiving access to the private key, the key may be corrupted and may need to be removed and replaced.")
|
||||
.wrap_err(eyre!("Failed to verify embedded signature of salted+hashed ID to complementary public key"))? {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// This user's unique ID
|
||||
pub fn id(&self) -> &UserID
|
||||
{
|
||||
&self.id
|
||||
}
|
||||
}
|
@ -0,0 +1,207 @@
|
||||
use std::fmt;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
/// Represents a semver version number of the order `major.minor.bugfix`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] //TODO: Make these impls instead of derives, because safe packed borrows.
|
||||
#[repr(C, packed)]
|
||||
pub struct Version(u8,u8,u16);
|
||||
|
||||
impl fmt::Display for Version
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
||||
{
|
||||
write!(f, "{}.{}.{}", self.major(), self.minor(), self.bugfix())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for Version
|
||||
{
|
||||
#[inline] fn from(from: u32) -> Self
|
||||
{
|
||||
Self::from_u32(from)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Version> for u32
|
||||
{
|
||||
fn from(from: Version) -> Self
|
||||
{
|
||||
from.to_u32()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Version> for u32
|
||||
{
|
||||
#[inline] fn eq(&self, other: &Version) -> bool
|
||||
{
|
||||
*self == other.to_u32()
|
||||
}
|
||||
}
|
||||
impl PartialEq<u32> for Version
|
||||
{
|
||||
#[inline] fn eq(&self, other: &u32) -> bool
|
||||
{
|
||||
self.to_u32() == *other
|
||||
}
|
||||
}
|
||||
impl PartialOrd<u32> for Version
|
||||
{
|
||||
#[inline] fn partial_cmp(&self, other: &u32) -> Option<Ordering> {
|
||||
self.to_u32().partial_cmp(other)
|
||||
}
|
||||
}
|
||||
impl PartialOrd<Version> for u32
|
||||
{
|
||||
#[inline] fn partial_cmp(&self, other: &Version) -> Option<Ordering> {
|
||||
self.partial_cmp(&other.to_u32())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<()> for Version
|
||||
{
|
||||
#[inline] fn from(from: ()) -> Self
|
||||
{
|
||||
Self(0,0,0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl From<(usize,)> for Version
|
||||
{
|
||||
#[inline] fn from(from: (usize,)) -> Self
|
||||
{
|
||||
Self::new(from.0,0,0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl From<(usize, usize)> for Version
|
||||
{
|
||||
#[inline] fn from((ma, mi): (usize, usize)) -> Self
|
||||
{
|
||||
Self::new(ma, mi, 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl From<(usize, usize, usize)> for Version
|
||||
{
|
||||
#[inline] fn from((ma,mi,bu): (usize, usize, usize)) -> Self
|
||||
{
|
||||
Self::new(ma,mi,bu)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(u8, u8, u16)> for Version
|
||||
{
|
||||
#[inline] fn from((ma, mi, bu): (u8, u8, u16)) -> Self
|
||||
{
|
||||
Self(ma,mi,bu)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Version> for (u8, u8, u16)
|
||||
{
|
||||
#[inline] fn from(from: Version) -> Self
|
||||
{
|
||||
(from.0, from.1, from.2)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Version> for (usize, usize, usize)
|
||||
{
|
||||
#[inline] fn from(from: Version) -> Self
|
||||
{
|
||||
(from.major(), from.minor(), from.bugfix())
|
||||
}
|
||||
}
|
||||
|
||||
impl Version
|
||||
{
|
||||
/// The major component of this `Version`.
|
||||
#[inline] pub const fn major(self) -> usize
|
||||
{
|
||||
self.0 as usize
|
||||
}
|
||||
/// The minor component of this `Version`.
|
||||
#[inline] pub const fn minor(self) -> usize
|
||||
{
|
||||
self.1 as usize
|
||||
}
|
||||
/// The bugfix component of this `Version`.
|
||||
#[inline] pub const fn bugfix(self) -> usize
|
||||
{
|
||||
self.2 as usize
|
||||
}
|
||||
|
||||
/// Convert to a 32 bit integer representation
|
||||
#[inline] pub const fn to_u32(self) -> u32
|
||||
{
|
||||
let mb = self.2.to_be_bytes();
|
||||
u32::from_be_bytes([
|
||||
self.0,
|
||||
self.1,
|
||||
mb[0],
|
||||
mb[1],
|
||||
])
|
||||
}
|
||||
/// Convert to a 32 bit integer representation
|
||||
#[inline] pub const fn from_u32(from: u32) -> Self
|
||||
{
|
||||
let bytes = from.to_be_bytes();
|
||||
Self(
|
||||
bytes[0],
|
||||
bytes[1],
|
||||
u16::from_be_bytes([bytes[2], bytes[3]]),
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a new version object
|
||||
#[inline] pub const fn new_exact(major: u8, minor: u8, bugfix: u16) -> Self
|
||||
{
|
||||
Self(major,minor,bugfix)
|
||||
}
|
||||
|
||||
/// Create a new version object
|
||||
///
|
||||
/// # Panics
|
||||
/// If any of the components do not fit within their bounds.
|
||||
#[inline] pub fn new(major: usize, minor: usize, bugfix: usize) -> Self
|
||||
{
|
||||
use std::convert::TryInto;
|
||||
Self::new_exact(major.try_into().expect("Major exceeded limit of u8"),
|
||||
minor.try_into().expect("Minor exceeded limit of u8"),
|
||||
bugfix.try_into().expect("Bugfix exceeded limit of u16"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! version {
|
||||
($maj:expr, $min:expr, $bfx:expr) => ($crate::version::Version::new_exact($maj as u8, $min as u8, $bfx as u16));
|
||||
($maj:expr, $min:expr) => ($crate::version::Version::new_exact($maj as u8, $min as u8, 0));
|
||||
($maj:expr) => ($crate::version::Version::new_exact($maj as u8, 0, 0));
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests
|
||||
{
|
||||
use super::*;
|
||||
#[test]
|
||||
fn ordinal()
|
||||
{
|
||||
assert!( version!(1) > version!(0, 9, 1));
|
||||
assert!( version!(2) > version!(1, 9, 300));
|
||||
assert!( (version!(1)..version!(1, 9)).contains(&version!(1, 8)));
|
||||
assert!( !(version!(1)..version!(2)).contains(&version!(2, 10, 432)));
|
||||
|
||||
println!("{}: {}", version!(1), version!(1).to_u32());
|
||||
println!("{}: {}", version!(0,9,1), version!(0,9,1).to_u32());
|
||||
|
||||
assert!( version!(1).to_u32() > version!(0, 9, 1).to_u32());
|
||||
assert!( version!(2).to_u32() > version!(1, 9, 300).to_u32());
|
||||
assert!( (version!(1).to_u32()..version!(1, 9).to_u32()).contains(&version!(1, 8).to_u32()));
|
||||
assert!( !(version!(1).to_u32()..version!(2).to_u32()).contains(&version!(2, 10, 432).to_u32()));
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
use super::*;
|
||||
use std::any::Any;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CommandKind
|
||||
{
|
||||
/// Shutdown gracefully.
|
||||
///
|
||||
/// # Response
|
||||
/// None.
|
||||
GracefulShutdown,
|
||||
}
|
||||
|
||||
/// A response from the interrupt channel.
|
||||
/// Some command kinds may warrant a response.
|
||||
pub type CommandResponse = Box<dyn Any + Send + 'static>;
|
||||
|
||||
/// A command to interrupt the web background task.
|
||||
#[derive(Debug)]
|
||||
pub(super) struct Command
|
||||
{
|
||||
pub(super) kind: CommandKind,
|
||||
pub(super) response: oneshot::Sender<CommandResponse>, // If the interrupt stream produces no response for this query, the sender will just be dropped and the receiver will `Err`.
|
||||
}
|
||||
|
||||
/// A channel to communicate with background task
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InterruptChannel(pub(super) mpsc::Sender<Command>);
|
@ -0,0 +1,86 @@
|
||||
use super::*;
|
||||
|
||||
use state::State;
|
||||
use futures::prelude::*;
|
||||
use tokio::{
|
||||
sync::{
|
||||
mpsc,
|
||||
oneshot,
|
||||
},
|
||||
};
|
||||
|
||||
mod command;
|
||||
pub use command::*;
|
||||
|
||||
mod route;
|
||||
|
||||
/// Serve this state with this interrupt signal
|
||||
pub fn serve(state: State) -> (InterruptChannel, impl Future<Output = eyre::Result<()>> + 'static)
|
||||
{
|
||||
// interrupt handler
|
||||
let (int_tx, mut int_rx) = mpsc::channel::<Command>(16);
|
||||
let (grace_tx, grace_rx) = oneshot::channel::<()>();
|
||||
let (s_int_tx, s_int_rx) = oneshot::channel::<()>();
|
||||
|
||||
// When this future completes, the server will initiate a graceful shutdown.
|
||||
let graceful_shutdown = async move {
|
||||
tokio::select!{
|
||||
//_ = tokio::signal::ctrl_c() =>{} //The caller should handle this and then send `InterruptChannel` a `GracefulShutdown` event.
|
||||
_ = grace_rx => {}
|
||||
}
|
||||
};
|
||||
|
||||
let h_int = tokio::spawn(async move {
|
||||
let work = async {
|
||||
// Handle commands from interrupt channel.
|
||||
while let Some(com) = int_rx.recv().await
|
||||
{
|
||||
let resp = com.response; //sender for response
|
||||
match com.kind {
|
||||
CommandKind::GracefulShutdown => {
|
||||
report!(grace_tx.send(()));
|
||||
return;
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
let int = async {
|
||||
let _ = tokio::join![
|
||||
//tokio::signal::ctrl_c(),
|
||||
s_int_rx,
|
||||
];
|
||||
};
|
||||
tokio::select!{
|
||||
_ = int => {info!("h_int shutdown due to interrupt");},
|
||||
_ = work => {info!("h_int shutdown due to exhausted work stream or shutdown signal");},
|
||||
}
|
||||
});
|
||||
let command_channel = InterruptChannel(int_tx);
|
||||
|
||||
// setup server
|
||||
let server = {
|
||||
// TODO: warp routing paths
|
||||
let routes = route::setup(state.clone());
|
||||
clone!(command_channel);
|
||||
async move {
|
||||
mv![command_channel, // If we need to send commands to our own stream
|
||||
state, // The program state
|
||||
graceful_shutdown, // The graceful shutdown Future for warp.
|
||||
routes,
|
||||
];
|
||||
|
||||
// TODO: warp::try_serve... `routes`.
|
||||
}
|
||||
};
|
||||
(command_channel,
|
||||
async move {
|
||||
info!("Waiting on server future");
|
||||
tokio::join![
|
||||
server,
|
||||
h_int,
|
||||
].1?;
|
||||
|
||||
report!(s_int_tx.send(()));
|
||||
Ok(())
|
||||
})
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
use super::*;
|
||||
use warp::Filter;
|
||||
use std::convert::Infallible;
|
||||
|
||||
use hard_format::{
|
||||
FormattedString,
|
||||
formats,
|
||||
};
|
||||
|
||||
pub fn setup(state: State) -> impl warp::Filter //TODO: What output should this have?
|
||||
{
|
||||
let root = warp::path("yuurei"); //TODO: configurable
|
||||
let state = warp::any().map(move || state.clone());
|
||||
|
||||
let post_api = warp::post()
|
||||
.and(warp::path("create"))
|
||||
.and(state.clone())
|
||||
//TODO: Filter to extract `User`. How? Dunno. Maybe cookies + IP or in the body itself.
|
||||
.and(warp::body::content_length_limit(defaults::MAX_CONTENT_LENGTH)) //TODO: configurable
|
||||
.and(warp::body::json())
|
||||
.and_then(|state: State, body: post::Post| { //TODO: post read is not this type, but another more restricted one
|
||||
async move {
|
||||
Ok::<_, Infallible>("test")
|
||||
}
|
||||
});
|
||||
let get_api = warp::get()
|
||||
.and(warp::path("get"))
|
||||
.and({
|
||||
let get_post_by_id = warp::any()
|
||||
.and(warp::path("post"))
|
||||
.and(state.clone())
|
||||
.and(warp::path::param().map(|opt: formats::HexFormattedString| opt)) //TODO: Convert to `PostID`.
|
||||
.and_then(|state: State, id: formats::HexFormattedString| {
|
||||
|
||||
async move {
|
||||
Ok::<_, Infallible>("test")
|
||||
}
|
||||
});
|
||||
let get_posts_by_user_id = warp::any()
|
||||
.and(warp::path("user"))
|
||||
.and(state.clone())
|
||||
.and(warp::path::param().map(|opt: formats::HexFormattedString| opt)) //TODO: Convert to `UserID`.
|
||||
.and_then(|state: State, id: formats::HexFormattedString| {
|
||||
|
||||
async move {
|
||||
Ok::<_, Infallible>("test")
|
||||
}
|
||||
});
|
||||
|
||||
get_post_by_id
|
||||
.or(get_posts_by_user_id)
|
||||
});
|
||||
let render_api = warp::get()
|
||||
.and(state.clone())
|
||||
.and_then(|state: State| {
|
||||
async move {
|
||||
Ok::<_, std::convert::Infallible>(template::view(&state).into_string())
|
||||
}
|
||||
});
|
||||
root.and(post_api
|
||||
.or(get_api)
|
||||
.or(render_api))
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script src="js/crypto-js.js"></script>
|
||||
<script src="js/jsencrypt.min.js"></script>
|
||||
<script src="js/NodeRSA.js"></script>
|
||||
<script type="text/javascript">
|
||||
function toHex(buffer) {
|
||||
return Array.prototype.map.call(buffer, x => ('00' + x.toString(16)).slice(-2)).join('');
|
||||
}
|
||||
function secure_rng(sz) {
|
||||
let array = new Uint8Array(sz);
|
||||
window.crypto.getRandomValues(array);
|
||||
return array;
|
||||
}
|
||||
function aes_genkey(password) {
|
||||
password = password || toHex(secure_rng());
|
||||
console.log(`AES PBKDF2 Password: ${password}`);
|
||||
//TODO: Generate custom random Key and IV of the correct size instead of this
|
||||
const KEY_BITS = 256;
|
||||
const salt = CryptoJS.lib.WordArray.random(KEY_BITS /8);
|
||||
return CryptoJS.PBKDF2(password, salt, { keySize: KEY_BITS / 32 });
|
||||
}
|
||||
function test(priv, pub) {
|
||||
//const priv = priv || document.getElementById("privkey").textContent;
|
||||
//const pub = pub || document.getElementById("pubkey").textContent;
|
||||
console.log(`Priv: ${priv}`);
|
||||
console.log(`Pub: ${pub}`);
|
||||
|
||||
const encrypt = new JSEncrypt();
|
||||
encrypt.setPublicKey(pub);
|
||||
const ciphertext = encrypt.encrypt("test input");
|
||||
|
||||
console.log(`Ciphertext: ${ciphertext}`);
|
||||
|
||||
const decrypt = new JSEncrypt();
|
||||
decrypt.setPrivateKey(priv);
|
||||
const plaintext = decrypt.decrypt(ciphertext);
|
||||
|
||||
console.log(`Plaintext: ${plaintext}`);
|
||||
|
||||
const sign = new JSEncrypt();
|
||||
sign.setPrivateKey(priv);
|
||||
const signature = sign.sign("test input", CryptoJS.SHA256, "sha256");
|
||||
|
||||
console.log(`Signature: ${signature}`);
|
||||
|
||||
const verify = new JSEncrypt();
|
||||
verify.setPublicKey(pub);
|
||||
const verified = verify.verify("test input", signature, CryptoJS.SHA256);
|
||||
|
||||
console.log(`Verified: ${verified}`);
|
||||
|
||||
|
||||
const key = aes_genkey().toString();
|
||||
console.log(`AES key: ${key}`);
|
||||
const aes_ciphertext = CryptoJS.AES.encrypt("test input", key).toString();
|
||||
|
||||
console.log(`AES ciphertext: ${aes_ciphertext}`);
|
||||
|
||||
const bytes = CryptoJS.AES.decrypt(aes_ciphertext, key);
|
||||
const aes_plaintext = bytes.toString(CryptoJS.enc.Utf8);
|
||||
|
||||
console.log(`AES plaintext: ${aes_plaintext}`);
|
||||
}
|
||||
window.onload = (async() => {
|
||||
const NodeRSA = require("node-rsa");
|
||||
const key = new NodeRSA({b: 1024});
|
||||
//key.generateKeyPair(); //unneeded I think
|
||||
const pub = key.exportKey("public");
|
||||
const priv = key.exportKey("private");
|
||||
//console.log(`Pub: ${pub}, priv: ${priv}`);
|
||||
document.getElementById("privkey").textContent = priv;
|
||||
document.getElementById("pubkey").textContent = pub;
|
||||
|
||||
test(priv, pub);
|
||||
});
|
||||
</script>
|
||||
<textarea id="privkey" rows="15" cols="65">(unbound)</textarea>
|
||||
<textarea id="pubkey" rows="15" cols="65">(unbound)</textarea>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1 @@
|
||||
../../vendor/NodeRSA.js
|
@ -0,0 +1 @@
|
||||
../../bower_components/crypto-js/crypto-js.js
|
@ -0,0 +1 @@
|
||||
../../node_modules/jsencrypt/bin/jsencrypt.min.js
|
Loading…
Reference in new issue