M Cargo.lock => Cargo.lock +56 -25
@@ 10,24 10,24 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "cc"
-version = "1.0.70"
+version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
[[package]]
name = "ed25519"
-version = "1.2.0"
+version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4620d40f6d2601794401d6dd95a5cf69b6c157852539470eeda433a99b3c0efc"
+checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369"
dependencies = [
"signature",
]
[[package]]
name = "libc"
-version = "0.2.101"
+version = "0.2.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21"
+checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c"
[[package]]
name = "libsodium-sys"
@@ 48,28 48,29 @@ dependencies = [
"base64",
"serde",
"sodiumoxide",
+ "sqlite",
]
[[package]]
name = "pkg-config"
-version = "0.3.19"
+version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
+checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
[[package]]
name = "proc-macro2"
-version = "1.0.29"
+version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
+checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
dependencies = [
- "unicode-xid",
+ "unicode-ident",
]
[[package]]
name = "quote"
-version = "1.0.9"
+version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
dependencies = [
"proc-macro2",
]
@@ 85,18 86,18 @@ dependencies = [
[[package]]
name = "serde"
-version = "1.0.130"
+version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
+checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.130"
+version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
+checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
dependencies = [
"proc-macro2",
"quote",
@@ 105,9 106,9 @@ dependencies = [
[[package]]
name = "signature"
-version = "1.3.1"
+version = "1.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c19772be3c4dd2ceaacf03cb41d5885f2a02c4d8804884918e3a258480803335"
+checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
[[package]]
name = "sodiumoxide"
@@ 122,21 123,51 @@ dependencies = [
]
[[package]]
+name = "sqlite"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2df8edd55685048550daaaf2be9024182f3523086cc86f7d50c136e55173e8c"
+dependencies = [
+ "libc",
+ "sqlite3-sys",
+]
+
+[[package]]
+name = "sqlite3-src"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1815a7a02c996eb8e5c64f61fcb6fd9b12e593ce265c512c5853b2513635691"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
+[[package]]
+name = "sqlite3-sys"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d47c99824fc55360ba00caf28de0b8a0458369b832e016a64c13af0ad9fbb9ee"
+dependencies = [
+ "libc",
+ "sqlite3-src",
+]
+
+[[package]]
name = "syn"
-version = "1.0.76"
+version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84"
+checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1"
dependencies = [
"proc-macro2",
"quote",
- "unicode-xid",
+ "unicode-ident",
]
[[package]]
-name = "unicode-xid"
-version = "0.2.2"
+name = "unicode-ident"
+version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
[[package]]
name = "walkdir"
M Cargo.toml => Cargo.toml +1 -0
@@ 8,3 8,4 @@ edition = "2021"
sodiumoxide = "0.2.7"
serde = { version = "1", features = ["derive"] }
base64 = "0.13.0"
+sqlite = "0.27.0"
M README.md => README.md +3 -1
@@ 1,1 1,3 @@
-This is a library for [the Sufec messaging protocol](https://yujiri.xyz/sufec), minimizing the cost of implementing a client.
+Library for [the Sufec messaging protocol](https://yujiri.xyz/sufec).
+
+Currently this library includes some structs that aren't related to the protocol but that most clients would want, like `Room` and `Store`. In the future, this library cold be separted into a core "libsufec" and a "sufec-client-utils" or something.
M src/lib.rs => src/lib.rs +9 -7
@@ 5,16 5,18 @@ mod message;
mod crypto;
mod error;
mod server;
+mod store;
+mod room;
-pub use account::{Account, DeviceId};
-pub use addr::SufecAddr;
-pub use message::{Message, MessageContent};
-pub use error::ServerError;
-pub use server::{PORT, MAX_FILE_SIZE, connect, send, login, ListeningConn};
+pub use account::*;
+pub use addr::*;
+pub use message::*;
+pub use error::*;
+pub use server::*;
+pub use room::*;
+pub use store::*;
use prelude::*;
-pub const MAX_HASHES_PER_MESSAGE: usize = u8::MAX as usize;
-
#[derive(Serialize, Deserialize, Clone)]
pub struct Contact {
pub name: String,
M src/prelude.rs => src/prelude.rs +2 -0
@@ 6,3 6,5 @@ pub use sodiumoxide::crypto::hash::sha512::{hash, Digest, DIGESTBYTES};
pub use std::io::{self, Write, Read};
pub use std::net::TcpStream;
pub use std::fmt::{self, Display, Debug};
+pub use std::collections::HashMap;
+pub use std::path::Path;
A src/room.rs => src/room.rs +12 -0
@@ 0,0 1,12 @@
+use crate::prelude::*;
+use crate::addr::*;
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct Room {
+ pub id: RoomId,
+ pub name: String,
+ pub members: Vec<SufecAddr>,
+ pub unseen: usize,
+}
+
+pub type RoomId = i64;
M src/server.rs => src/server.rs +1 -1
@@ 101,7 101,7 @@ impl ListeningConn {
}
}
-pub struct EncryptedStream {
+struct EncryptedStream {
pub stream: TcpStream,
pub key: PrecomputedKey,
pub nonce: Nonce,
A src/store.rs => src/store.rs +162 -0
@@ 0,0 1,162 @@
+use crate::prelude::*;
+use crate::addr::*;
+use crate::message::*;
+use crate::room::*;
+
+/// An SQLite database storing messages and hashes, and an in-memory cache of loaded messages.
+/// Panics if it fails to access the database.
+pub struct Store {
+ conn: sqlite::Connection,
+ cache: HashMap<RoomId, Vec<HistoryEntry>>,
+}
+impl Store {
+ /// Creates the database file, including all parent directories, if it doesn't exist.
+ /// Panics if that fails.
+ pub fn open(path: &Path) -> Self {
+ std::fs::create_dir_all(&path.parent().unwrap()).unwrap();
+ let exists = Path::try_exists(path).unwrap();
+ let conn = sqlite::open(path).expect("can't create database");
+ if !exists { conn.execute(CREATE_TABLES).unwrap() }
+ Store{conn, cache: HashMap::new()}
+ }
+ pub fn add_message(&mut self, room_id: RoomId, m: &HistoryEntry) {
+ if let Some(cache) = self.cache.get_mut(&room_id) {
+ cache.push(m.clone());
+ }
+ self.conn.prepare(ADD_MESSAGE).unwrap().into_cursor().bind(&[
+ sqlite::Value::Integer(room_id),
+ sqlite::Value::Binary(m.sender.to_bytes()),
+ sqlite::Value::Integer(m.timestamp as i64),
+ sqlite::Value::Binary(m.content.to_bytes()),
+ sqlite::Value::Integer(m.hash_sent as i64),
+ sqlite::Value::Integer(m.all_verified as i64),
+ ]).unwrap().try_next().unwrap();
+ }
+ pub fn get(&mut self, room_id: RoomId) -> &[HistoryEntry] {
+ if self.cache.get(&room_id).is_none() {
+ self.cache.insert(room_id, self.load_messages(room_id));
+ }
+ self.cache.get(&room_id).unwrap()
+ }
+ pub fn has_message(&self, room_id: RoomId, timestamp: u64, sender: &SufecAddr) -> bool {
+ self.conn.prepare("SELECT room_id FROM messages WHERE room_id = ? AND timestamp = ? AND sender = ?").unwrap()
+ .into_cursor().bind(&[
+ sqlite::Value::Integer(room_id),
+ sqlite::Value::Integer(timestamp as i64),
+ sqlite::Value::Binary(sender.to_bytes()),
+ ]).unwrap().try_next().unwrap().is_some()
+ }
+ fn load_messages(&self, room_id: RoomId) -> Vec<HistoryEntry> {
+ let mut results = vec![];
+ let query = format!("{} {}", SELECT_MESSAGES, "WHERE room_id = ? ORDER BY timestamp ASC");
+ let mut cursor = self.conn.prepare(&query).unwrap().into_cursor().bind(&[
+ sqlite::Value::Integer(room_id),
+ ]).unwrap();
+ while let Some(row) = cursor.try_next().unwrap() {
+ results.push(self.scan_message(row));
+ }
+ results
+ }
+ fn scan_message(&self, row: &[sqlite::Value]) -> HistoryEntry {
+ HistoryEntry{
+ sender: SufecAddr::from_bytes(row[0].as_binary().unwrap()).unwrap(),
+ timestamp: row[1].as_integer().unwrap() as u64,
+ content: MessageContent::from_bytes(row[2].as_binary().unwrap()).unwrap(),
+ hash_sent: row[3].as_integer().unwrap() == 1,
+ all_verified: row[4].as_integer().unwrap() == 1,
+ }
+ }
+ pub fn check_hashes(&self, room: &Room, entry: &HistoryEntry) -> bool {
+ let mut need: Vec<SufecAddr> = room.members.iter().cloned().filter(|m| *m != entry.sender).collect();
+ let query = "SELECT sender, hash FROM hashes WHERE room_id = ? AND timestamp = ?";
+ let mut cursor = self.conn.prepare(&query).unwrap().into_cursor().bind(&[
+ sqlite::Value::Integer(room.id),
+ sqlite::Value::Integer(entry.timestamp as i64),
+ ]).unwrap();
+ while let Some(row) = cursor.try_next().unwrap() {
+ let hasher = SufecAddr::from_bytes(row[0].as_binary().unwrap()).unwrap();
+ let hash = Digest::from_slice(row[1].as_binary().unwrap()).unwrap();
+ if hash != entry.hash() { return false }
+ need.retain(|a| *a != hasher);
+ }
+ need.is_empty()
+ }
+ pub fn add_hash(&mut self, room_id: RoomId, sender: &SufecAddr, timestamp: u64, hash: Digest) {
+ self.conn.prepare(ADD_HASH).unwrap().into_cursor().bind(&[
+ sqlite::Value::Integer(room_id),
+ sqlite::Value::Integer(timestamp as i64),
+ sqlite::Value::Binary(sender.to_bytes()),
+ sqlite::Value::Binary(hash.as_ref().to_vec()),
+ ]).unwrap().try_next().unwrap();
+ }
+ // Note the messages are marked as hash_sent immediately, so if sending these hashes fails,
+ // the client will falsely think these messages have been sent.
+ pub fn prepare_outgoing_hashes(&self, room_id: RoomId) -> Vec<(u64, Digest)> {
+ let mut messages = vec![];
+ let query = format!("{} {}", SELECT_MESSAGES,
+ "WHERE room_id = ? AND NOT hash_sent ORDER BY timestamp ASC LIMIT ?");
+ let mut cursor = self.conn.prepare(&query).unwrap().into_cursor().bind(&[
+ sqlite::Value::Integer(room_id),
+ sqlite::Value::Integer(MAX_HASHES_PER_MESSAGE as i64),
+ ]).unwrap();
+ while let Some(row) = cursor.try_next().unwrap() {
+ let msg = self.scan_message(row);
+ self.conn.prepare("UPDATE messages SET hash_sent = 1 WHERE room_id = ? AND timestamp = ?").unwrap()
+ .into_cursor().bind(&[
+ sqlite::Value::Integer(room_id),
+ sqlite::Value::Integer(msg.timestamp as i64),
+ ]).unwrap().try_next().unwrap();
+ messages.push(msg);
+ }
+ messages.iter().map(|m| (m.timestamp, m.hash())).collect()
+ }
+ pub fn mark_verified(&mut self, room_id: RoomId, timestamp: u64) {
+ if let Some(cache) = self.cache.get_mut(&room_id) {
+ let msg = cache.iter_mut().find(|m| m.timestamp == timestamp).unwrap();
+ msg.all_verified = true;
+ }
+ self.conn.prepare("UPDATE messages SET all_verified = 1 WHERE room_id = ? AND timestamp = ?").unwrap()
+ .into_cursor().bind(&[
+ sqlite::Value::Integer(room_id),
+ sqlite::Value::Integer(timestamp as i64),
+ ]).unwrap().try_next().unwrap();
+ }
+}
+
+const CREATE_TABLES: &str = "
+CREATE TABLE messages (
+ room_id integer not null,
+ sender blob not null,
+ timestamp integer not null,
+ content blob not null,
+ hash_sent integer not null,
+ all_verified integer not null
+);
+CREATE TABLE hashes (
+ room_id integer not null,
+ timestamp integer not null,
+ sender blob not null,
+ hash blob not null
+)";
+
+const ADD_MESSAGE: &str = "INSERT INTO messages VALUES (?, ?, ?, ?, ?, ?)";
+
+const SELECT_MESSAGES: &str = "SELECT sender, timestamp, content, hash_sent, all_verified FROM messages";
+
+const ADD_HASH: &str = "INSERT INTO hashes VALUES (?, ?, ?, ?)";
+
+const MAX_HASHES_PER_MESSAGE: usize = u8::MAX as usize;
+
+#[derive(Clone, Debug)]
+pub struct HistoryEntry {
+ pub sender: SufecAddr,
+ pub timestamp: u64,
+ pub content: MessageContent,
+ pub all_verified: bool,
+ pub hash_sent: bool,
+}
+impl HistoryEntry {
+ pub fn hash(&self) -> Digest {
+ hash(&[self.sender.to_bytes(), self.content.to_bytes()].concat())
+ }
+}