diff --git a/Cargo.lock b/Cargo.lock index b400517..51464ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,6 +64,12 @@ dependencies = [ "safemem", ] +[[package]] +name = "build_const" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39" + [[package]] name = "byte-tools" version = "0.3.1" @@ -82,6 +88,12 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" +[[package]] +name = "cc" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" + [[package]] name = "cfg-if" version = "0.1.10" @@ -109,6 +121,45 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" +[[package]] +name = "crc" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" +dependencies = [ + "build_const", +] + +[[package]] +name = "crypto-mac" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58bcd97a54c7ca5ce2f6eb16f6bede5b0ab5f0055fedc17d2f0b4466e21671ca" +dependencies = [ + "generic-array 0.14.4", + "subtle", +] + +[[package]] +name = "cryptohelpers" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "705d37f9525e6e677af724ec414c7cb712e8576dbdd11bafbcfbcea2f468bcd9" +dependencies = [ + "crc", + "futures", + "getrandom 0.1.16", + "hex-literal", + "hmac", + "libc", + "openssl", + "pbkdf2", + "serde", + "serde_derive", + "sha2", + "tokio", +] + [[package]] name = "difference" version = "2.0.0" @@ -151,6 +202,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.0.0" @@ -278,6 +344,15 @@ dependencies = [ "slab", ] +[[package]] +name = "generational-arena" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d3b771574f62d0548cee0ad9057857e9fc25d7a3335f140c84f6acd0bf601" +dependencies = [ + "cfg-if 0.1.10", +] + [[package]] name = "generic-array" version = "0.12.3" @@ -308,6 +383,17 @@ dependencies = [ "wasi 0.9.0+wasi-snapshot-preview1", ] +[[package]] +name = "getrandom" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4060f4657be78b8e766215b02b18a2e862d83745545de804638e2b545e81aee6" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", +] + [[package]] name = "h2" version = "0.2.7" @@ -368,6 +454,22 @@ dependencies = [ "libc", ] +[[package]] +name = "hex-literal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5af1f635ef1bc545d78392b136bfe1c9809e029023c84a3638a864a10b8819c8" + +[[package]] +name = "hmac" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deae6d9dbb35ec2c502d62b8f7b1c000a0822c3b0794ba36b3149c0a1c840dff" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + [[package]] name = "http" version = "0.2.2" @@ -650,6 +752,48 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl" +version = "0.10.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038d43985d1ddca7a9900630d8cd031b56e4794eecc2e9ea39dd17aa04399a70" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "foreign-types", + "lazy_static", + "libc", + "openssl-sys", +] + +[[package]] +name = "openssl-sys" +version = "0.9.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "921fc71883267538946025deffb622905ecad223c28efbfdef9bb59a0175f3e6" +dependencies = [ + "autocfg 1.0.1", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "pbkdf2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170d73bf11f39b4ce1809aabc95bf5c33564cdc16fc3200ddda17a5f6e5e48b" +dependencies = [ + "base64", + "crypto-mac", + "hmac", + "rand 0.7.3", + "rand_core 0.5.1", + "sha2", + "subtle", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -714,6 +858,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + [[package]] name = "ppv-lite86" version = "0.2.10" @@ -781,7 +931,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ - "getrandom", + "getrandom 0.1.16", "libc", "rand_chacha 0.2.2", "rand_core 0.5.1", @@ -829,7 +979,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" dependencies = [ - "getrandom", + "getrandom 0.1.16", ] [[package]] @@ -1013,6 +1163,19 @@ dependencies = [ "opaque-debug 0.3.0", ] +[[package]] +name = "sha2" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7aab86fe2149bad8c507606bdb3f4ef5e7b2380eb92350f56122cca72a42a8" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 1.0.0", + "cpuid-bool", + "digest 0.9.0", + "opaque-debug 0.3.0", +] + [[package]] name = "signal-hook-registry" version = "1.3.0" @@ -1048,6 +1211,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "subtle" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" + [[package]] name = "syn" version = "1.0.56" @@ -1295,6 +1464,22 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7" +[[package]] +name = "uuid" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" +dependencies = [ + "rand 0.7.3", + "serde", +] + +[[package]] +name = "vcpkg" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb" + [[package]] name = "version_check" version = "0.9.2" @@ -1399,11 +1584,17 @@ dependencies = [ name = "yuurei" version = "0.1.0" dependencies = [ + "cryptohelpers", "difference", "futures", + "generational-arena", + "getrandom 0.2.1", + "lazy_static", "once_cell", "serde", + "sha2", "smallvec", "tokio", + "uuid", "warp", ] diff --git a/Cargo.toml b/Cargo.toml index 1d63cc0..67af577 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,10 +11,16 @@ default = ["nightly"] nightly = ["smallvec/const_generics"] [dependencies] +cryptohelpers = {version = "1.7.1", features=["full"]} difference = "2.0.0" futures = "0.3.8" +generational-arena = "0.2.8" +getrandom = "0.2.1" +lazy_static = "1.4.0" once_cell = "1.5.2" serde = {version = "1.0.118", features=["derive"]} +sha2 = "0.9.2" smallvec = {version = "1.6.0", features= ["union", "serde", "write"]} tokio = {version = "0.2", features=["full"] } +uuid = {version = "0.8.1", features=["v4","serde"]} warp = "0.2.5" diff --git a/src/bytes.rs b/src/bytes.rs new file mode 100644 index 0000000..82a18ae --- /dev/null +++ b/src/bytes.rs @@ -0,0 +1,29 @@ +use std::ptr; + +/// Copy slice of bytes only +/// +/// # Notes +/// `dst` and `src` must not overlap. See [move_slice]. +pub fn copy_slice(dst: &mut [u8], src: &[u8]) -> usize +{ + let sz = std::cmp::min(dst.len(),src.len()); + unsafe { + //libc::memcpy(&mut dst[0] as *mut u8 as *mut c_void, &src[0] as *const u8 as *const c_void, sz); + ptr::copy_nonoverlapping(&src[0] as *const u8, &mut dst[0] as *mut u8, sz); + } + sz +} + +/// Move slice of bytes only +/// +/// # Notes +/// `dst` and `src` can overlap. +pub fn move_slice(dst: &mut [u8], src: &[u8]) -> usize +{ + let sz = std::cmp::min(dst.len(),src.len()); + unsafe { + //libc::memmove(&mut dst[0] as *mut u8 as *mut c_void, &src[0] as *const u8 as *const c_void, sz); + ptr::copy(&src[0] as *const u8, &mut dst[0] as *mut u8, sz); + } + sz +} diff --git a/src/ext.rs b/src/ext.rs index 568e327..226449b 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -85,7 +85,7 @@ impl BackInserter for Vec } impl BackInserter for SmallVec - where T: smallvec::Array +where T: smallvec::Array { #[inline] fn push_back(&mut self, value: T) { @@ -348,3 +348,144 @@ mod tests } } } + +#[macro_export] macro_rules! id_type { + ($name:ident $(; $doc:literal)?) => { + $(#[doc(comment=$doc)])? + #[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash, ::serde::Serialize, ::serde::Deserialize)] + pub struct $name(uuid::Uuid); + + impl $name + { + /// Create a random new unique ID + #[inline] fn id_new() -> Self + { + Self(::uuid::Uuid::new_v4()) + } + /// Create from a UUID + #[inline] fn id_from(from: ::uuid::Uuid) -> Self + { + Self(from) + } + } + + }; +} + +mod global_counter +{ + use std::sync::atomic::{ + Ordering, AtomicU64, + }; + + #[derive(Debug)] + pub struct GlobalCounter(AtomicU64); + + impl GlobalCounter + { + /// Get and increment the counter + pub fn get(&self) -> u64 + { + self.0.fetch_add(1, Ordering::SeqCst) + } + /// Check if this `u64` is valid to have come from this counter. + pub fn valid(&self, val: u64) -> bool + { + val <= self.0.load(Ordering::Acquire) + } + /// Create a new global counter. + pub const fn new() -> Self + { + Self(AtomicU64::new(0)) + } + } +} +pub use global_counter::GlobalCounter; + +const GLOBAL_SALT_SIZE: usize = 16; +lazy_static! { + pub static ref GLOBAL_SALT: &'static [u8] = { + let mut this = Box::new([0u8; GLOBAL_SALT_SIZE]); + getrandom::getrandom(&mut this[..]).expect("Failed to populate global salt"); + &Box::leak(this)[..] + }; +} + +/// A wrapper for hashing with a specific salt. +#[derive(Debug, Hash)] +pub struct Salted<'a, T: std::hash::Hash>(&'a T, &'a [u8]); + +impl<'a, T> Salted<'a, T> +where T: std::hash::Hash +{ + /// Create a new wrapper. + pub fn new(val: &'a T, salt: &'a [u8]) -> Self + { + Self(val, &salt) + } +} +/// A wrapper for hashing with the global salt. +#[derive(Debug, Hash)] +pub struct GloballySalted<'a, T: std::hash::Hash>(&'a T, &'static [u8]); + +impl<'a, T> GloballySalted<'a, T> +where T: std::hash::Hash +{ + /// Create a new wrapper. + pub fn new(val: &'a T) -> Self + { + Self(val, &GLOBAL_SALT[..]) + } +} + +mod sha256_hasher { + use std::mem::size_of; + use sha2::{ + Digest, Sha256, + }; + use std::hash::{ + Hasher, Hash, + }; + use cryptohelpers::sha256::Sha256Hash; + struct Sha256Hasher(Sha256); + impl Sha256Hasher + { + pub fn new() -> Self + { + Self(Sha256::new()) + } + } + impl Hasher for Sha256Hasher + { + fn write(&mut self, bytes: &[u8]) + { + self.0.update(bytes); + } + fn finish(&self) -> u64 + { + let ar = self.0.clone().finalize(); + let mut rest = [0u8; size_of::()]; + crate::bytes::move_slice(&mut rest[..], &ar[..]); + u64::from_le_bytes(rest) + } + } + + pub trait Sha256HashExt + { + fn compute_sha256_hash(&self) -> Sha256Hash; + } + + impl Sha256HashExt for T + where T: Hash + { + fn compute_sha256_hash(&self) -> Sha256Hash { + let mut hasher = Sha256Hasher::new(); + self.hash(&mut hasher); + hasher.0.into() + } + } +} +pub use sha256_hasher::Sha256HashExt; + +/// Value may hold one in place or allocate on the heap to hold many. +pub type MaybeVec = smallvec::SmallVec<[T; 1]>; diff --git a/src/main.rs b/src/main.rs index 567ae0f..8cc63a2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,9 +4,13 @@ #[cfg(all(feature="nightly", test))] extern crate test; #[macro_use] extern crate serde; +#[macro_use] extern crate lazy_static; +use std::convert::{TryFrom, TryInto}; #[macro_use] mod ext; use ext::*; +mod bytes; + mod delta; mod state; diff --git a/src/state/freeze.rs b/src/state/freeze.rs new file mode 100644 index 0000000..d398ac5 --- /dev/null +++ b/src/state/freeze.rs @@ -0,0 +1,153 @@ +//! Creating immutable images of state. +use super::*; +use std::{error,fmt}; + + +/// An image of the entire post container +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct Imouto +{ + posts: Vec, +} + +impl From for Oneesan +{ + #[inline] fn from(from: Imouto) -> Self + { + Self::from_freeze(from) + } +} + +impl TryFrom for Imouto +{ + type Error = FreezeError; + + #[inline] fn try_from(from: Oneesan) -> Result + { + from.try_into_freeze() + } +} + + +/// Error returned when a freeze operation fails +#[derive(Debug)] +pub struct FreezeError{ + held: Option>>, +} + +impl FreezeError +{ + /// The post associated with this error, if there is one. + pub fn post(&self) -> Option<&Arc>> + { + self.held.as_ref() + } +} + +impl error::Error for FreezeError{} +impl fmt::Display for FreezeError +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result + { + write!(f,"Failed to create freeze image")?; + + if let Some(aref) = &self.held + { + let cnt = Arc::strong_count(&aref) - 1; + if cnt > 0 { + write!(f, "Post reference still held in {} other places.", cnt) + } else { + write!(f, "Post reference was still held in another place at the time, but no longer is.") + } + } else { + Ok(()) + } + } +} + +impl Oneesan +{ + /// Create a serialisable image of this store by cloning each post into it. + pub async fn freeze(&self) -> Imouto + { + let read = self.posts.read().await; + let mut sis = Imouto{ + posts: Vec::with_capacity(read.0.len()), + }; + for (_, post) in read.0.iter() + { + sis.posts.push(post.read().await.clone()); + } + + sis + } + + /// Consume into a serialisable image of this store. + /// + /// # Fails + /// If references to any posts are still held elsewhere. + pub fn try_into_freeze(self) -> Result + { + let read = self.posts.into_inner(); + let mut sis = Imouto{ + posts: Vec::with_capacity(read.0.len()), + }; + for post in read.0.into_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. + }); + } + + Ok(sis) + } + + /// Consume into a serialisable image of this store. + /// + /// # Panics + /// If references to any posts are still held elsewhere. + pub fn into_freeze(self) -> Imouto + { + self.try_into_freeze().expect("Failed to consume into freeze") + } + + /// Create a new store from a serialisable image of one by cloning each post in it + pub fn unfreeze(freeze: &Imouto) -> Self + { + let mut posts = Arena::new(); + let mut user_map = HashMap::new(); + + for post in freeze.posts.iter() + { + let idx = posts.insert(Arc::new(RwLock::new(post.clone()))); + user_map.entry(post.owner().clone()) + .or_insert_with(|| MaybeVec::new()) + .push(idx); + } + + Self { + posts: RwLock::new((posts, user_map)) + } + } + + /// Create a new store by consuming serialisable image of one by cloning each post in it + pub fn from_freeze(freeze: Imouto) -> Self + { + let mut posts = Arena::new(); + let mut user_map = HashMap::new(); + + for post in freeze.posts.into_iter() + { + 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); + } + + Self { + posts: RwLock::new((posts, user_map)) + } + } +} diff --git a/src/state/mod.rs b/src/state/mod.rs index 6e94012..2984ed3 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -1,5 +1,22 @@ use super::*; +use std::collections::HashMap; +use generational_arena::{ + Arena, Index as ArenaIndex, +}; +use std::sync::Arc; +use tokio::sync::RwLock; +pub mod session; pub mod user; pub mod post; pub mod body; + +mod freeze; pub use freeze::*; + +/// Entire post container +pub struct Oneesan +{ + posts: RwLock<(Arena>> // All posts + , HashMap> // Post lookup by user ID + )>, +} diff --git a/src/state/post.rs b/src/state/post.rs index 70122a0..074c5e1 100644 --- a/src/state/post.rs +++ b/src/state/post.rs @@ -1,18 +1,18 @@ use super::*; -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct OpenPost { } -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ClosedPost { } -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum PostKind { Open(OpenPost), @@ -20,9 +20,18 @@ pub enum PostKind } /// A post -#[derive(Debug)] +#[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 + } +} diff --git a/src/state/session.rs b/src/state/session.rs new file mode 100644 index 0000000..748caa3 --- /dev/null +++ b/src/state/session.rs @@ -0,0 +1,25 @@ +//! Session for each connected user +use super::*; + +id_type!(SessionID; "A unique session ID, not bound to a user."); + +#[derive(Debug)] +pub struct Session +{ + id: SessionID, + user: user::User, +} + +impl Session +{ + /// The randomly generated ID of this session, irrespective of the user of this session. + #[inline] pub fn session_id(&self) -> &SessionID + { + &self.id + } + /// The unique user ID of this session + pub fn user_id(&self) -> user::UserID + { + self.user.id_for_session(self) + } +} diff --git a/src/state/user.rs b/src/state/user.rs index eb20ee8..1bc990c 100644 --- a/src/state/user.rs +++ b/src/state/user.rs @@ -3,5 +3,59 @@ //! Mostly for determining if a poster owns a post. use super::*; -#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] -pub struct UserID(()); //TODO: User ID. Maybe use IP + session ID hash? +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::()]; + + 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::()]; + + 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, 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()) + } +}