mirror of
https://github.com/Blah-IM/blahrs.git
synced 2025-05-19 00:31:09 +00:00
feat(types,webapi): impl id_key/act_key for all APIs and update docs
This commit is contained in:
parent
fb76756482
commit
cb72d049e0
10 changed files with 426 additions and 330 deletions
|
@ -16,7 +16,7 @@ serde = { version = "1", features = ["derive"] }
|
||||||
serde_jcs = "0.1"
|
serde_jcs = "0.1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde_with = "3.9.0"
|
serde_with = "3.9.0"
|
||||||
url = "2"
|
url = { version = "2", features = ["serde"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
expect-test = "1.5.0"
|
expect-test = "1.5.0"
|
||||||
|
|
|
@ -23,7 +23,7 @@ pub const X_BLAH_DIFFICULTY: &str = "x-blah-difficulty";
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct UserIdentityDesc {
|
pub struct UserIdentityDesc {
|
||||||
/// User primary identity key, only for signing action keys.
|
/// User primary identity key, only for signing action keys.
|
||||||
pub id_key: UserKey,
|
pub id_key: PubKey,
|
||||||
/// User action subkeys, signed by the identity key.
|
/// User action subkeys, signed by the identity key.
|
||||||
pub act_keys: Vec<Signed<UserActKeyDesc>>,
|
pub act_keys: Vec<Signed<UserActKeyDesc>>,
|
||||||
/// User profile, signed by any valid action key.
|
/// User profile, signed by any valid action key.
|
||||||
|
@ -38,7 +38,7 @@ impl UserIdentityDesc {
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(tag = "typ", rename = "user_act_key")]
|
#[serde(tag = "typ", rename = "user_act_key")]
|
||||||
pub struct UserActKeyDesc {
|
pub struct UserActKeyDesc {
|
||||||
pub act_key: UserKey,
|
pub act_key: PubKey,
|
||||||
pub expire_time: u64,
|
pub expire_time: u64,
|
||||||
pub comment: String,
|
pub comment: String,
|
||||||
}
|
}
|
||||||
|
@ -83,10 +83,16 @@ impl<T> WithMsgId<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(transparent)]
|
pub struct UserKey {
|
||||||
pub struct UserKey(#[serde(with = "hex::serde")] pub [u8; PUBLIC_KEY_LENGTH]);
|
pub id_key: PubKey,
|
||||||
|
pub act_key: PubKey,
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for UserKey {
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct PubKey(#[serde(with = "hex::serde")] pub [u8; PUBLIC_KEY_LENGTH]);
|
||||||
|
|
||||||
|
impl fmt::Display for PubKey {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
let mut buf = [0u8; PUBLIC_KEY_LENGTH * 2];
|
let mut buf = [0u8; PUBLIC_KEY_LENGTH * 2];
|
||||||
hex::encode_to_slice(self.0, &mut buf).expect("buf size is correct");
|
hex::encode_to_slice(self.0, &mut buf).expect("buf size is correct");
|
||||||
|
@ -108,6 +114,7 @@ pub struct Signee<T> {
|
||||||
pub nonce: u32,
|
pub nonce: u32,
|
||||||
pub payload: T,
|
pub payload: T,
|
||||||
pub timestamp: u64,
|
pub timestamp: u64,
|
||||||
|
#[serde(flatten)]
|
||||||
pub user: UserKey,
|
pub user: UserKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,7 +133,8 @@ impl<T: Serialize> Signed<T> {
|
||||||
|
|
||||||
/// Sign the payload with the given `key`.
|
/// Sign the payload with the given `key`.
|
||||||
pub fn sign(
|
pub fn sign(
|
||||||
key: &SigningKey,
|
id_key: &PubKey,
|
||||||
|
act_key: &SigningKey,
|
||||||
timestamp: u64,
|
timestamp: u64,
|
||||||
rng: &mut (impl RngCore + ?Sized),
|
rng: &mut (impl RngCore + ?Sized),
|
||||||
payload: T,
|
payload: T,
|
||||||
|
@ -135,10 +143,13 @@ impl<T: Serialize> Signed<T> {
|
||||||
nonce: rng.next_u32(),
|
nonce: rng.next_u32(),
|
||||||
payload,
|
payload,
|
||||||
timestamp,
|
timestamp,
|
||||||
user: UserKey(key.verifying_key().to_bytes()),
|
user: UserKey {
|
||||||
|
act_key: PubKey(act_key.verifying_key().to_bytes()),
|
||||||
|
id_key: id_key.clone(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let canonical_signee = serde_jcs::to_vec(&signee).expect("serialization cannot fail");
|
let canonical_signee = serde_jcs::to_vec(&signee).expect("serialization cannot fail");
|
||||||
let sig = key.try_sign(&canonical_signee)?.to_bytes();
|
let sig = act_key.try_sign(&canonical_signee)?.to_bytes();
|
||||||
Ok(Self { sig, signee })
|
Ok(Self { sig, signee })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,7 +157,7 @@ impl<T: Serialize> Signed<T> {
|
||||||
///
|
///
|
||||||
/// Note that this does not check validity of timestamp and other data.
|
/// Note that this does not check validity of timestamp and other data.
|
||||||
pub fn verify(&self) -> Result<(), SignatureError> {
|
pub fn verify(&self) -> Result<(), SignatureError> {
|
||||||
VerifyingKey::from_bytes(&self.signee.user.0)?
|
VerifyingKey::from_bytes(&self.signee.user.act_key.0)?
|
||||||
.verify_strict(&self.canonical_signee(), &Signature::from_bytes(&self.sig))?;
|
.verify_strict(&self.canonical_signee(), &Signature::from_bytes(&self.sig))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -158,7 +169,7 @@ impl<T: Serialize> Signed<T> {
|
||||||
pub struct UserRegisterPayload {
|
pub struct UserRegisterPayload {
|
||||||
pub server_url: Url,
|
pub server_url: Url,
|
||||||
pub id_url: Url,
|
pub id_url: Url,
|
||||||
pub id_key: UserKey,
|
pub id_key: PubKey,
|
||||||
pub challenge_nonce: u32,
|
pub challenge_nonce: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -387,7 +398,7 @@ pub struct RoomMetadata {
|
||||||
pub member_permission: Option<MemberPermission>,
|
pub member_permission: Option<MemberPermission>,
|
||||||
/// The peer user, if this is a peer chat room.
|
/// The peer user, if this is a peer chat room.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub peer_user: Option<UserKey>,
|
pub peer_user: Option<PubKey>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
@ -409,7 +420,7 @@ pub struct CreateGroup {
|
||||||
/// Peer-to-peer chat room with exactly two symmetric users.
|
/// Peer-to-peer chat room with exactly two symmetric users.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct CreatePeerChat {
|
pub struct CreatePeerChat {
|
||||||
pub peer: UserKey,
|
pub peer: PubKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A collection of room members, with these invariants:
|
/// A collection of room members, with these invariants:
|
||||||
|
@ -443,7 +454,7 @@ impl TryFrom<Vec<RoomMember>> for RoomMemberList {
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct RoomMember {
|
pub struct RoomMember {
|
||||||
pub permission: MemberPermission,
|
pub permission: MemberPermission,
|
||||||
pub user: UserKey,
|
pub user: PubKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Proof of room membership for read-access.
|
/// Proof of room membership for read-access.
|
||||||
|
@ -466,10 +477,10 @@ pub struct RoomAdminPayload {
|
||||||
pub enum RoomAdminOp {
|
pub enum RoomAdminOp {
|
||||||
AddMember {
|
AddMember {
|
||||||
permission: MemberPermission,
|
permission: MemberPermission,
|
||||||
user: UserKey,
|
user: PubKey,
|
||||||
},
|
},
|
||||||
RemoveMember {
|
RemoveMember {
|
||||||
user: UserKey,
|
user: PubKey,
|
||||||
},
|
},
|
||||||
// TODO: RU
|
// TODO: RU
|
||||||
}
|
}
|
||||||
|
@ -533,19 +544,19 @@ mod sql_impl {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToSql for UserKey {
|
impl ToSql for PubKey {
|
||||||
fn to_sql(&self) -> Result<ToSqlOutput<'_>> {
|
fn to_sql(&self) -> Result<ToSqlOutput<'_>> {
|
||||||
// TODO: Extensive key format?
|
// TODO: Extensive key format?
|
||||||
self.0.to_sql()
|
self.0.to_sql()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromSql for UserKey {
|
impl FromSql for PubKey {
|
||||||
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||||
let rawkey = <[u8; PUBLIC_KEY_LENGTH]>::column_result(value)?;
|
let rawkey = <[u8; PUBLIC_KEY_LENGTH]>::column_result(value)?;
|
||||||
let key = VerifyingKey::from_bytes(&rawkey)
|
let key = VerifyingKey::from_bytes(&rawkey)
|
||||||
.map_err(|err| FromSqlError::Other(format!("invalid pubkey: {err}").into()))?;
|
.map_err(|err| FromSqlError::Other(format!("invalid pubkey: {err}").into()))?;
|
||||||
Ok(UserKey(key.to_bytes()))
|
Ok(PubKey(key.to_bytes()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -596,10 +607,12 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn canonical_msg() {
|
fn canonical_msg() {
|
||||||
let mut fake_rng = rand::rngs::mock::StepRng::new(0x42, 1);
|
let mut fake_rng = rand::rngs::mock::StepRng::new(0x42, 1);
|
||||||
let signing_key = SigningKey::from_bytes(&[0x42; 32]);
|
let id_key = SigningKey::from_bytes(&[0x42; 32]);
|
||||||
|
let act_key = SigningKey::from_bytes(&[0x43; 32]);
|
||||||
let timestamp = 0xDEAD_BEEF;
|
let timestamp = 0xDEAD_BEEF;
|
||||||
let msg = Signed::sign(
|
let msg = Signed::sign(
|
||||||
&signing_key,
|
&PubKey(id_key.verifying_key().to_bytes()),
|
||||||
|
&act_key,
|
||||||
timestamp,
|
timestamp,
|
||||||
&mut fake_rng,
|
&mut fake_rng,
|
||||||
ChatPayload {
|
ChatPayload {
|
||||||
|
@ -611,7 +624,7 @@ mod tests {
|
||||||
|
|
||||||
let json = serde_jcs::to_string(&msg).unwrap();
|
let json = serde_jcs::to_string(&msg).unwrap();
|
||||||
let expect = expect![[
|
let expect = expect![[
|
||||||
r#"{"sig":"18ee190722bebfd438c82f34890540d91578b4ba9f6c0c6011cc4fd751a321e32e9442d00dad1920799c54db011694c72a9ba993b408922e9997119209aa5e09","signee":{"nonce":66,"payload":{"rich_text":["hello"],"room":"42","typ":"chat"},"timestamp":3735928559,"user":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12"}}"#
|
r#"{"sig":"74ca2895ac94e741e086bae28ce8c282bf375e3e59a3408f562420d72e98d799f7e627879aa883fa0804a0799eb9b90398150b0150c2e3550635ff28b9991502","signee":{"act_key":"22fc297792f0b6ffc0bfcfdb7edb0c0aa14e025a365ec0e342e86e3829cb74b6","id_key":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","nonce":66,"payload":{"rich_text":["hello"],"room":"42","typ":"chat"},"timestamp":3735928559}}"#
|
||||||
]];
|
]];
|
||||||
expect.assert_eq(&json);
|
expect.assert_eq(&json);
|
||||||
|
|
||||||
|
@ -657,7 +670,7 @@ mod tests {
|
||||||
room: Id(42),
|
room: Id(42),
|
||||||
op: RoomAdminOp::AddMember {
|
op: RoomAdminOp::AddMember {
|
||||||
permission: MemberPermission::POST_CHAT,
|
permission: MemberPermission::POST_CHAT,
|
||||||
user: UserKey([0x42; PUBLIC_KEY_LENGTH]),
|
user: PubKey([0x42; PUBLIC_KEY_LENGTH]),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let raw = serde_jcs::to_string(&data).unwrap();
|
let raw = serde_jcs::to_string(&data).unwrap();
|
||||||
|
|
|
@ -4,8 +4,8 @@ use std::{fs, io};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use blah_types::{
|
use blah_types::{
|
||||||
bitflags, get_timestamp, ChatPayload, CreateGroup, CreateRoomPayload, Id, RichText, RoomAttrs,
|
bitflags, get_timestamp, ChatPayload, CreateGroup, CreateRoomPayload, Id, PubKey, RichText,
|
||||||
ServerPermission, Signed, UserKey,
|
RoomAttrs, ServerPermission, Signed,
|
||||||
};
|
};
|
||||||
use ed25519_dalek::pkcs8::spki::der::pem::LineEnding;
|
use ed25519_dalek::pkcs8::spki::der::pem::LineEnding;
|
||||||
use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey};
|
use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey};
|
||||||
|
@ -126,9 +126,9 @@ fn userkey_parser(s: &str) -> clap::error::Result<VerifyingKey> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
async fn fetch_key(&self) -> Result<UserKey> {
|
async fn fetch_key(&self) -> Result<PubKey> {
|
||||||
let rawkey = if let Some(key) = &self.key {
|
let rawkey = if let Some(key) = &self.key {
|
||||||
return Ok(UserKey(key.to_bytes()));
|
return Ok(PubKey(key.to_bytes()));
|
||||||
} else if let Some(path) = &self.public_key_file {
|
} else if let Some(path) = &self.public_key_file {
|
||||||
fs::read_to_string(path).context("failed to read key file")?
|
fs::read_to_string(path).context("failed to read key file")?
|
||||||
} else if let Some(url) = &self.url {
|
} else if let Some(url) = &self.url {
|
||||||
|
@ -140,7 +140,7 @@ impl User {
|
||||||
let key = VerifyingKey::from_public_key_pem(&rawkey)
|
let key = VerifyingKey::from_public_key_pem(&rawkey)
|
||||||
.context("invalid key")?
|
.context("invalid key")?
|
||||||
.to_bytes();
|
.to_bytes();
|
||||||
Ok(UserKey(key))
|
Ok(PubKey(key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,7 +221,14 @@ async fn main_api(api_url: Url, command: ApiCommand) -> Result<()> {
|
||||||
attrs: attrs.unwrap_or_default(),
|
attrs: attrs.unwrap_or_default(),
|
||||||
title,
|
title,
|
||||||
});
|
});
|
||||||
let payload = Signed::sign(&key, get_timestamp(), &mut OsRng, payload)?;
|
// FIXME: Same key.
|
||||||
|
let payload = Signed::sign(
|
||||||
|
&PubKey(key.to_bytes()),
|
||||||
|
&key,
|
||||||
|
get_timestamp(),
|
||||||
|
&mut OsRng,
|
||||||
|
payload,
|
||||||
|
)?;
|
||||||
|
|
||||||
let ret = client
|
let ret = client
|
||||||
.post(api_url.join("/room/create")?)
|
.post(api_url.join("/room/create")?)
|
||||||
|
@ -243,7 +250,14 @@ async fn main_api(api_url: Url, command: ApiCommand) -> Result<()> {
|
||||||
room: Id(room),
|
room: Id(room),
|
||||||
rich_text: RichText::from(text),
|
rich_text: RichText::from(text),
|
||||||
};
|
};
|
||||||
let payload = Signed::sign(&key, get_timestamp(), &mut OsRng, payload)?;
|
// FIXME: Same key.
|
||||||
|
let payload = Signed::sign(
|
||||||
|
&PubKey(key.to_bytes()),
|
||||||
|
&key,
|
||||||
|
get_timestamp(),
|
||||||
|
&mut OsRng,
|
||||||
|
payload,
|
||||||
|
)?;
|
||||||
|
|
||||||
let ret = client
|
let ret = client
|
||||||
.post(api_url.join(&format!("/room/{room}/msg"))?)
|
.post(api_url.join(&format!("/room/{room}/msg"))?)
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
-- implemented and layout can change at any time.
|
-- implemented and layout can change at any time.
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `user` (
|
CREATE TABLE IF NOT EXISTS `user` (
|
||||||
`uid` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
`uid` INTEGER NOT NULL PRIMARY KEY,
|
||||||
`userkey` BLOB NOT NULL UNIQUE,
|
`id_key` BLOB NOT NULL UNIQUE,
|
||||||
`permission` INTEGER NOT NULL DEFAULT 0,
|
`permission` INTEGER NOT NULL DEFAULT 0,
|
||||||
`last_fetch_time` INTEGER NOT NULL,
|
`last_fetch_time` INTEGER NOT NULL,
|
||||||
`id_desc` TEXT NOT NULL
|
`id_desc` TEXT NOT NULL
|
||||||
|
@ -15,7 +15,13 @@ CREATE TABLE IF NOT EXISTS `user_act_key` (
|
||||||
`expire_time` INTEGER NOT NULL,
|
`expire_time` INTEGER NOT NULL,
|
||||||
|
|
||||||
PRIMARY KEY (`uid`, `act_key`)
|
PRIMARY KEY (`uid`, `act_key`)
|
||||||
) STRICT;
|
) STRICT, WITHOUT ROWID;
|
||||||
|
|
||||||
|
CREATE VIEW IF NOT EXISTS `valid_user_act_key` AS
|
||||||
|
SELECT `act_key`, `user`.*
|
||||||
|
FROM `user_act_key`
|
||||||
|
JOIN `user` USING (`uid`)
|
||||||
|
WHERE unixepoch() < `expire_time`;
|
||||||
|
|
||||||
-- The highest bit of `rid` will be set for peer chat room.
|
-- The highest bit of `rid` will be set for peer chat room.
|
||||||
-- So simply comparing it against 0 can filter them out.
|
-- So simply comparing it against 0 can filter them out.
|
||||||
|
@ -53,6 +59,8 @@ CREATE TABLE IF NOT EXISTS `msg` (
|
||||||
`cid` INTEGER NOT NULL PRIMARY KEY,
|
`cid` INTEGER NOT NULL PRIMARY KEY,
|
||||||
`rid` INTEGER NOT NULL REFERENCES `room` ON DELETE CASCADE,
|
`rid` INTEGER NOT NULL REFERENCES `room` ON DELETE CASCADE,
|
||||||
`uid` INTEGER NOT NULL REFERENCES `user` ON DELETE RESTRICT,
|
`uid` INTEGER NOT NULL REFERENCES `user` ON DELETE RESTRICT,
|
||||||
|
-- Optionally references `user_act_key`(`act_key`)
|
||||||
|
`act_key` BLOB NOT NULL,
|
||||||
`timestamp` INTEGER NOT NULL,
|
`timestamp` INTEGER NOT NULL,
|
||||||
`nonce` INTEGER NOT NULL,
|
`nonce` INTEGER NOT NULL,
|
||||||
`sig` BLOB NOT NULL,
|
`sig` BLOB NOT NULL,
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
|
use std::borrow::Borrow;
|
||||||
use std::ops::DerefMut;
|
use std::ops::DerefMut;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::{ensure, Context, Result};
|
use anyhow::{ensure, Context, Result};
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use blah_types::{ServerPermission, UserKey};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rusqlite::{params, Connection, OpenFlags};
|
use rusqlite::{params, Connection, OpenFlags, OptionalExtension};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_inline_default::serde_inline_default;
|
use serde_inline_default::serde_inline_default;
|
||||||
|
|
||||||
|
use crate::ApiError;
|
||||||
|
|
||||||
const DEFAULT_DATABASE_PATH: &str = "/var/lib/blahd/db.sqlite";
|
const DEFAULT_DATABASE_PATH: &str = "/var/lib/blahd/db.sqlite";
|
||||||
|
|
||||||
static INIT_SQL: &str = include_str!("../schema.sql");
|
static INIT_SQL: &str = include_str!("../schema.sql");
|
||||||
|
@ -97,6 +102,31 @@ impl Database {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait ConnectionExt: Borrow<Connection> {
|
||||||
|
fn get_user(&self, user: &UserKey) -> Result<(i64, ServerPermission), ApiError> {
|
||||||
|
self.borrow()
|
||||||
|
.query_row(
|
||||||
|
r"
|
||||||
|
SELECT `uid`, `permission`
|
||||||
|
FROM `valid_user_act_key`
|
||||||
|
WHERE (`id_key`, `act_key`) = (?, ?)
|
||||||
|
",
|
||||||
|
params![user.id_key, user.act_key],
|
||||||
|
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||||
|
)
|
||||||
|
.optional()?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
error_response!(
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
"not_found",
|
||||||
|
"the user does not exist",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectionExt for Connection {}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn init_sql_valid() {
|
fn init_sql_valid() {
|
||||||
let conn = Connection::open_in_memory().unwrap();
|
let conn = Connection::open_in_memory().unwrap();
|
||||||
|
|
|
@ -7,20 +7,20 @@ use std::sync::Arc;
|
||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{bail, Context as _, Result};
|
use anyhow::{anyhow, bail, Context as _, Result};
|
||||||
use axum::extract::ws::{Message, WebSocket};
|
use axum::extract::ws::{Message, WebSocket};
|
||||||
use blah_types::{AuthPayload, Signed, SignedChatMsg};
|
use blah_types::{AuthPayload, Signed, SignedChatMsg};
|
||||||
use futures_util::future::Either;
|
use futures_util::future::Either;
|
||||||
use futures_util::stream::SplitSink;
|
use futures_util::stream::SplitSink;
|
||||||
use futures_util::{stream_select, SinkExt as _, Stream, StreamExt};
|
use futures_util::{stream_select, SinkExt as _, Stream, StreamExt};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rusqlite::{params, OptionalExtension};
|
|
||||||
use serde::{de, Deserialize, Serialize};
|
use serde::{de, Deserialize, Serialize};
|
||||||
use serde_inline_default::serde_inline_default;
|
use serde_inline_default::serde_inline_default;
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
|
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
|
||||||
use tokio_stream::wrappers::BroadcastStream;
|
use tokio_stream::wrappers::BroadcastStream;
|
||||||
|
|
||||||
|
use crate::database::ConnectionExt;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
@ -143,19 +143,13 @@ pub async fn handle_ws(st: Arc<AppState>, ws: &mut WebSocket) -> Result<Infallib
|
||||||
let auth = serde_json::from_str::<Signed<AuthPayload>>(&payload)?;
|
let auth = serde_json::from_str::<Signed<AuthPayload>>(&payload)?;
|
||||||
st.verify_signed_data(&auth)?;
|
st.verify_signed_data(&auth)?;
|
||||||
|
|
||||||
st.db
|
let (uid, _) = st
|
||||||
|
.db
|
||||||
.get()
|
.get()
|
||||||
.query_row(
|
.get_user(&auth.signee.user)
|
||||||
r"
|
.map_err(|err| anyhow!("{}", err.message))?;
|
||||||
SELECT `uid`
|
// FIXME: Consistency of id's sign.
|
||||||
FROM `user`
|
uid as u64
|
||||||
WHERE `userkey` = ?
|
|
||||||
",
|
|
||||||
params![auth.signee.user],
|
|
||||||
|row| row.get::<_, u64>(0),
|
|
||||||
)
|
|
||||||
.optional()?
|
|
||||||
.context("invalid user")?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::debug!(%uid, "user connected");
|
tracing::debug!(%uid, "user connected");
|
||||||
|
|
191
blahd/src/lib.rs
191
blahd/src/lib.rs
|
@ -15,6 +15,7 @@ use blah_types::{
|
||||||
RoomAdminPayload, RoomAttrs, RoomMetadata, ServerPermission, Signed, SignedChatMsg, Signee,
|
RoomAdminPayload, RoomAttrs, RoomMetadata, ServerPermission, Signed, SignedChatMsg, Signee,
|
||||||
UserKey, UserRegisterPayload, WithMsgId,
|
UserKey, UserRegisterPayload, WithMsgId,
|
||||||
};
|
};
|
||||||
|
use database::ConnectionExt;
|
||||||
use ed25519_dalek::SIGNATURE_LENGTH;
|
use ed25519_dalek::SIGNATURE_LENGTH;
|
||||||
use id::IdExt;
|
use id::IdExt;
|
||||||
use middleware::{Auth, MaybeAuth, ResultExt as _, SignedJson};
|
use middleware::{Auth, MaybeAuth, ResultExt as _, SignedJson};
|
||||||
|
@ -188,10 +189,10 @@ async fn user_get(
|
||||||
.query_row(
|
.query_row(
|
||||||
"
|
"
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM `user`
|
FROM `valid_user_act_key`
|
||||||
WHERE `userkey` = ?
|
WHERE (`id_key`, `act_key`) = (?, ?)
|
||||||
",
|
",
|
||||||
params![user],
|
params![user.id_key, user.act_key],
|
||||||
|_| Ok(()),
|
|_| Ok(()),
|
||||||
)
|
)
|
||||||
.optional()?,
|
.optional()?,
|
||||||
|
@ -271,7 +272,10 @@ async fn room_list(
|
||||||
signee: Signee {
|
signee: Signee {
|
||||||
nonce: row.get("nonce")?,
|
nonce: row.get("nonce")?,
|
||||||
timestamp: row.get("timestamp")?,
|
timestamp: row.get("timestamp")?,
|
||||||
user: row.get("userkey")?,
|
user: UserKey {
|
||||||
|
act_key: row.get("act_key")?,
|
||||||
|
id_key: row.get("id_key")?,
|
||||||
|
},
|
||||||
payload: ChatPayload {
|
payload: ChatPayload {
|
||||||
rich_text: row.get("rich_text")?,
|
rich_text: row.get("rich_text")?,
|
||||||
room: rid,
|
room: rid,
|
||||||
|
@ -290,7 +294,7 @@ async fn room_list(
|
||||||
.filter(|cid| cid.0 != 0),
|
.filter(|cid| cid.0 != 0),
|
||||||
unseen_cnt: row.get("unseen_cnt").ok(),
|
unseen_cnt: row.get("unseen_cnt").ok(),
|
||||||
member_permission: row.get("member_perm").ok(),
|
member_permission: row.get("member_perm").ok(),
|
||||||
peer_user: row.get("peer_userkey").ok(),
|
peer_user: row.get("peer_id_key").ok(),
|
||||||
})
|
})
|
||||||
})?
|
})?
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
@ -303,7 +307,8 @@ async fn room_list(
|
||||||
ListRoomFilter::Public => query(
|
ListRoomFilter::Public => query(
|
||||||
r"
|
r"
|
||||||
SELECT `rid`, `title`, `attrs`, 0 AS `last_seen_cid`,
|
SELECT `rid`, `title`, `attrs`, 0 AS `last_seen_cid`,
|
||||||
`cid`, `last_author`.`userkey`, `timestamp`, `nonce`, `sig`, `rich_text`
|
`cid`, `timestamp`, `nonce`, `sig`, `rich_text`,
|
||||||
|
`last_author`.`id_key`, `msg`.`act_key`
|
||||||
FROM `room`
|
FROM `room`
|
||||||
LEFT JOIN `msg` USING (`rid`)
|
LEFT JOIN `msg` USING (`rid`)
|
||||||
LEFT JOIN `user` AS `last_author` USING (`uid`)
|
LEFT JOIN `user` AS `last_author` USING (`uid`)
|
||||||
|
@ -325,16 +330,17 @@ async fn room_list(
|
||||||
r"
|
r"
|
||||||
SELECT
|
SELECT
|
||||||
`rid`, `title`, `attrs`, `last_seen_cid`, `room_member`.`permission` AS `member_perm`,
|
`rid`, `title`, `attrs`, `last_seen_cid`, `room_member`.`permission` AS `member_perm`,
|
||||||
`cid`, `last_author`.`userkey`, `timestamp`, `nonce`, `sig`, `rich_text`,
|
`cid`, `timestamp`, `nonce`, `sig`, `rich_text`,
|
||||||
`peer_user`.`userkey` AS `peer_userkey`
|
`last_author`.`id_key`, `msg`.`act_key`,
|
||||||
FROM `user`
|
`peer_user`.`id_key` AS `peer_id_key`
|
||||||
|
FROM `valid_user_act_key` AS `me`
|
||||||
JOIN `room_member` USING (`uid`)
|
JOIN `room_member` USING (`uid`)
|
||||||
JOIN `room` USING (`rid`)
|
JOIN `room` USING (`rid`)
|
||||||
LEFT JOIN `msg` USING (`rid`)
|
LEFT JOIN `msg` USING (`rid`)
|
||||||
LEFT JOIN `user` AS `last_author` ON (`last_author`.`uid` = `msg`.`uid`)
|
LEFT JOIN `user` AS `last_author` ON (`last_author`.`uid` = `msg`.`uid`)
|
||||||
LEFT JOIN `user` AS `peer_user` ON
|
LEFT JOIN `user` AS `peer_user` ON
|
||||||
(`peer_user`.`uid` = `room`.`peer1` + `room`.`peer2` - `user`.`uid`)
|
(`peer_user`.`uid` = `room`.`peer1` + `room`.`peer2` - `me`.`uid`)
|
||||||
WHERE `user`.`userkey` = :userkey AND
|
WHERE (`me`.`id_key`, `me`.`act_key`) = (:id_key, :act_key) AND
|
||||||
`rid` > :start_rid
|
`rid` > :start_rid
|
||||||
GROUP BY `rid` HAVING `cid` IS MAX(`cid`)
|
GROUP BY `rid` HAVING `cid` IS MAX(`cid`)
|
||||||
ORDER BY `rid` ASC
|
ORDER BY `rid` ASC
|
||||||
|
@ -343,7 +349,8 @@ async fn room_list(
|
||||||
named_params! {
|
named_params! {
|
||||||
":start_rid": start_rid,
|
":start_rid": start_rid,
|
||||||
":page_len": page_len,
|
":page_len": page_len,
|
||||||
":userkey": user,
|
":id_key": user.id_key,
|
||||||
|
":act_key": user.act_key,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -353,20 +360,21 @@ async fn room_list(
|
||||||
r"
|
r"
|
||||||
SELECT
|
SELECT
|
||||||
`rid`, `title`, `attrs`, `last_seen_cid`, `room_member`.`permission` AS `member_perm`,
|
`rid`, `title`, `attrs`, `last_seen_cid`, `room_member`.`permission` AS `member_perm`,
|
||||||
`cid`, `last_author`.`userkey`, `timestamp`, `nonce`, `sig`, `rich_text`,
|
`cid`, `timestamp`, `nonce`, `sig`, `rich_text`,
|
||||||
`peer_user`.`userkey` AS `peer_userkey`,
|
`last_author`.`id_key`, `msg`.`act_key`,
|
||||||
|
`peer_user`.`id_key` AS `peer_id_key`,
|
||||||
(SELECT COUNT(*)
|
(SELECT COUNT(*)
|
||||||
FROM `msg` AS `unseen_msg`
|
FROM `msg` AS `unseen_msg`
|
||||||
WHERE `unseen_msg`.`rid` = `room`.`rid` AND
|
WHERE `unseen_msg`.`rid` = `room`.`rid` AND
|
||||||
`last_seen_cid` < `unseen_msg`.`cid`) AS `unseen_cnt`
|
`last_seen_cid` < `unseen_msg`.`cid`) AS `unseen_cnt`
|
||||||
FROM `user`
|
FROM `valid_user_act_key` AS `me`
|
||||||
JOIN `room_member` USING (`uid`)
|
JOIN `room_member` USING (`uid`)
|
||||||
JOIN `room` USING (`rid`)
|
JOIN `room` USING (`rid`)
|
||||||
LEFT JOIN `msg` USING (`rid`)
|
LEFT JOIN `msg` USING (`rid`)
|
||||||
LEFT JOIN `user` AS `last_author` ON (`last_author`.`uid` = `msg`.`uid`)
|
LEFT JOIN `user` AS `last_author` ON (`last_author`.`uid` = `msg`.`uid`)
|
||||||
LEFT JOIN `user` AS `peer_user` ON
|
LEFT JOIN `user` AS `peer_user` ON
|
||||||
(`peer_user`.`uid` = `room`.`peer1` + `room`.`peer2` - `user`.`uid`)
|
(`peer_user`.`uid` = `room`.`peer1` + `room`.`peer2` - `me`.`uid`)
|
||||||
WHERE `user`.`userkey` = :userkey AND
|
WHERE (`me`.`id_key`, `me`.`act_key`) = (:id_key, :act_key) AND
|
||||||
`rid` > :start_rid AND
|
`rid` > :start_rid AND
|
||||||
`cid` > `last_seen_cid`
|
`cid` > `last_seen_cid`
|
||||||
GROUP BY `rid` HAVING `cid` IS MAX(`cid`)
|
GROUP BY `rid` HAVING `cid` IS MAX(`cid`)
|
||||||
|
@ -376,7 +384,8 @@ async fn room_list(
|
||||||
named_params! {
|
named_params! {
|
||||||
":start_rid": start_rid,
|
":start_rid": start_rid,
|
||||||
":page_len": page_len,
|
":page_len": page_len,
|
||||||
":userkey": user,
|
":id_key": user.id_key,
|
||||||
|
":act_key": user.act_key,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -389,14 +398,16 @@ async fn room_create(
|
||||||
SignedJson(params): SignedJson<CreateRoomPayload>,
|
SignedJson(params): SignedJson<CreateRoomPayload>,
|
||||||
) -> Result<Json<Id>, ApiError> {
|
) -> Result<Json<Id>, ApiError> {
|
||||||
match params.signee.payload {
|
match params.signee.payload {
|
||||||
CreateRoomPayload::Group(op) => room_create_group(&st, params.signee.user, op).await,
|
CreateRoomPayload::Group(op) => room_create_group(&st, ¶ms.signee.user, op).await,
|
||||||
CreateRoomPayload::PeerChat(op) => room_create_peer_chat(&st, params.signee.user, op).await,
|
CreateRoomPayload::PeerChat(op) => {
|
||||||
|
room_create_peer_chat(&st, ¶ms.signee.user, op).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn room_create_group(
|
async fn room_create_group(
|
||||||
st: &AppState,
|
st: &AppState,
|
||||||
user: UserKey,
|
user: &UserKey,
|
||||||
op: CreateGroup,
|
op: CreateGroup,
|
||||||
) -> Result<Json<Id>, ApiError> {
|
) -> Result<Json<Id>, ApiError> {
|
||||||
if !RoomAttrs::GROUP_ATTRS.contains(op.attrs) {
|
if !RoomAttrs::GROUP_ATTRS.contains(op.attrs) {
|
||||||
|
@ -412,10 +423,10 @@ async fn room_create_group(
|
||||||
.query_row(
|
.query_row(
|
||||||
r"
|
r"
|
||||||
SELECT `uid`, `permission`
|
SELECT `uid`, `permission`
|
||||||
FROM `user`
|
FROM `valid_user_act_key`
|
||||||
WHERE `userkey` = ?
|
WHERE (`id_key`, `act_key`) = (?, ?)
|
||||||
",
|
",
|
||||||
params![user],
|
params![user.id_key, user.act_key],
|
||||||
|row| {
|
|row| {
|
||||||
Ok((
|
Ok((
|
||||||
row.get::<_, i64>("uid")?,
|
row.get::<_, i64>("uid")?,
|
||||||
|
@ -462,11 +473,11 @@ async fn room_create_group(
|
||||||
|
|
||||||
async fn room_create_peer_chat(
|
async fn room_create_peer_chat(
|
||||||
st: &AppState,
|
st: &AppState,
|
||||||
src_user: UserKey,
|
src_user: &UserKey,
|
||||||
op: CreatePeerChat,
|
op: CreatePeerChat,
|
||||||
) -> Result<Json<Id>, ApiError> {
|
) -> Result<Json<Id>, ApiError> {
|
||||||
let tgt_user = op.peer;
|
let tgt_user_id_key = op.peer;
|
||||||
if tgt_user == src_user {
|
if tgt_user_id_key == src_user.id_key {
|
||||||
return Err(error_response!(
|
return Err(error_response!(
|
||||||
StatusCode::NOT_IMPLEMENTED,
|
StatusCode::NOT_IMPLEMENTED,
|
||||||
"not_implemented",
|
"not_implemented",
|
||||||
|
@ -478,38 +489,19 @@ async fn room_create_peer_chat(
|
||||||
|
|
||||||
let mut conn = st.db.get();
|
let mut conn = st.db.get();
|
||||||
let txn = conn.transaction()?;
|
let txn = conn.transaction()?;
|
||||||
let src_uid = txn
|
let (src_uid, _) = txn.get_user(src_user)?;
|
||||||
|
let (tgt_uid, _) = txn
|
||||||
.query_row(
|
.query_row(
|
||||||
r"
|
r"
|
||||||
SELECT `uid` FROM `user`
|
SELECT `uid`, `permission`
|
||||||
WHERE `userkey` = ?
|
|
||||||
",
|
|
||||||
params![src_user],
|
|
||||||
|row| row.get::<_, i64>(0),
|
|
||||||
)
|
|
||||||
.optional()?
|
|
||||||
.ok_or_else(|| {
|
|
||||||
error_response!(
|
|
||||||
StatusCode::NOT_FOUND,
|
|
||||||
"not_found",
|
|
||||||
"the user does not exist",
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let tgt_uid = txn
|
|
||||||
.query_row(
|
|
||||||
r"
|
|
||||||
SELECT `uid`
|
|
||||||
FROM `user`
|
FROM `user`
|
||||||
WHERE `userkey` = :userkey AND
|
WHERE `id_key` = ?
|
||||||
`permission` & :perm = :perm
|
|
||||||
",
|
",
|
||||||
named_params! {
|
params![tgt_user_id_key],
|
||||||
":userkey": tgt_user,
|
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, ServerPermission>(1)?)),
|
||||||
":perm": ServerPermission::ACCEPT_PEER_CHAT,
|
|
||||||
},
|
|
||||||
|row| row.get::<_, i64>(0),
|
|
||||||
)
|
)
|
||||||
.optional()?
|
.optional()?
|
||||||
|
.filter(|(_, perm)| perm.contains(ServerPermission::ACCEPT_PEER_CHAT))
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
error_response!(
|
error_response!(
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
|
@ -518,11 +510,8 @@ async fn room_create_peer_chat(
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let (peer1, peer2) = if src_uid <= tgt_uid {
|
let mut peers = [src_uid, tgt_uid];
|
||||||
(src_uid, tgt_uid)
|
peers.sort();
|
||||||
} else {
|
|
||||||
(tgt_uid, src_uid)
|
|
||||||
};
|
|
||||||
let rid = Id::gen_peer_chat_rid();
|
let rid = Id::gen_peer_chat_rid();
|
||||||
let updated = txn.execute(
|
let updated = txn.execute(
|
||||||
r"
|
r"
|
||||||
|
@ -533,8 +522,8 @@ async fn room_create_peer_chat(
|
||||||
named_params! {
|
named_params! {
|
||||||
":rid": rid,
|
":rid": rid,
|
||||||
":attrs": RoomAttrs::PEER_CHAT,
|
":attrs": RoomAttrs::PEER_CHAT,
|
||||||
":peer1": peer1,
|
":peer1": peers[0],
|
||||||
":peer2": peer2,
|
":peer2": peers[1],
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
if updated == 0 {
|
if updated == 0 {
|
||||||
|
@ -553,7 +542,7 @@ async fn room_create_peer_chat(
|
||||||
",
|
",
|
||||||
)?;
|
)?;
|
||||||
// TODO: Limit permission of the src user?
|
// TODO: Limit permission of the src user?
|
||||||
for uid in [peer1, peer2] {
|
for uid in peers {
|
||||||
stmt.execute(named_params! {
|
stmt.execute(named_params! {
|
||||||
":rid": rid,
|
":rid": rid,
|
||||||
":uid": uid,
|
":uid": uid,
|
||||||
|
@ -656,7 +645,8 @@ async fn room_get_feed(
|
||||||
.map(|WithMsgId { cid, msg }| {
|
.map(|WithMsgId { cid, msg }| {
|
||||||
let time = SystemTime::UNIX_EPOCH + Duration::from_secs(msg.signee.timestamp);
|
let time = SystemTime::UNIX_EPOCH + Duration::from_secs(msg.signee.timestamp);
|
||||||
let author = FeedAuthor {
|
let author = FeedAuthor {
|
||||||
name: msg.signee.user.to_string(),
|
// TODO: Retrieve id_url as name.
|
||||||
|
name: msg.signee.user.id_key.to_string(),
|
||||||
};
|
};
|
||||||
FeedItem {
|
FeedItem {
|
||||||
id: cid.to_string(),
|
id: cid.to_string(),
|
||||||
|
@ -747,6 +737,11 @@ fn get_room_if_readable<T>(
|
||||||
user: Option<&UserKey>,
|
user: Option<&UserKey>,
|
||||||
f: impl FnOnce(&Row<'_>) -> rusqlite::Result<T>,
|
f: impl FnOnce(&Row<'_>) -> rusqlite::Result<T>,
|
||||||
) -> Result<T, ApiError> {
|
) -> Result<T, ApiError> {
|
||||||
|
let (id_key, act_key) = match user {
|
||||||
|
Some(keys) => (Some(&keys.id_key), Some(&keys.act_key)),
|
||||||
|
None => (None, None),
|
||||||
|
};
|
||||||
|
|
||||||
conn.query_row(
|
conn.query_row(
|
||||||
r"
|
r"
|
||||||
SELECT `title`, `attrs`
|
SELECT `title`, `attrs`
|
||||||
|
@ -755,14 +750,15 @@ fn get_room_if_readable<T>(
|
||||||
((`attrs` & :perm) = :perm OR
|
((`attrs` & :perm) = :perm OR
|
||||||
EXISTS(SELECT 1
|
EXISTS(SELECT 1
|
||||||
FROM `room_member`
|
FROM `room_member`
|
||||||
JOIN `user` USING (`uid`)
|
JOIN `valid_user_act_key` USING (`uid`)
|
||||||
WHERE `room_member`.`rid` = `room`.`rid` AND
|
WHERE `room_member`.`rid` = `room`.`rid` AND
|
||||||
`userkey` = :userkey))
|
(`id_key`, `act_key`) = (:id_key, :act_key)))
|
||||||
",
|
",
|
||||||
named_params! {
|
named_params! {
|
||||||
":rid": rid,
|
":rid": rid,
|
||||||
":perm": RoomAttrs::PUBLIC_READABLE,
|
":perm": RoomAttrs::PUBLIC_READABLE,
|
||||||
":userkey": user,
|
":id_key": id_key,
|
||||||
|
":act_key": act_key,
|
||||||
},
|
},
|
||||||
f,
|
f,
|
||||||
)
|
)
|
||||||
|
@ -787,7 +783,7 @@ fn query_room_msgs(
|
||||||
let page_len = pagination.effective_page_len(st);
|
let page_len = pagination.effective_page_len(st);
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
r"
|
r"
|
||||||
SELECT `cid`, `timestamp`, `nonce`, `sig`, `userkey`, `sig`, `rich_text`
|
SELECT `cid`, `timestamp`, `nonce`, `sig`, `id_key`, `act_key`, `sig`, `rich_text`
|
||||||
FROM `msg`
|
FROM `msg`
|
||||||
JOIN `user` USING (`uid`)
|
JOIN `user` USING (`uid`)
|
||||||
WHERE `rid` = :rid AND
|
WHERE `rid` = :rid AND
|
||||||
|
@ -813,7 +809,10 @@ fn query_room_msgs(
|
||||||
signee: Signee {
|
signee: Signee {
|
||||||
nonce: row.get("nonce")?,
|
nonce: row.get("nonce")?,
|
||||||
timestamp: row.get("timestamp")?,
|
timestamp: row.get("timestamp")?,
|
||||||
user: row.get("userkey")?,
|
user: UserKey {
|
||||||
|
id_key: row.get("id_key")?,
|
||||||
|
act_key: row.get("act_key")?,
|
||||||
|
},
|
||||||
payload: ChatPayload {
|
payload: ChatPayload {
|
||||||
room: rid,
|
room: rid,
|
||||||
rich_text: row.get("rich_text")?,
|
rich_text: row.get("rich_text")?,
|
||||||
|
@ -850,13 +849,14 @@ async fn room_msg_post(
|
||||||
r"
|
r"
|
||||||
SELECT `uid`, `room_member`.`permission`
|
SELECT `uid`, `room_member`.`permission`
|
||||||
FROM `room_member`
|
FROM `room_member`
|
||||||
JOIN `user` USING (`uid`)
|
JOIN `valid_user_act_key` USING (`uid`)
|
||||||
WHERE `rid` = :rid AND
|
WHERE `rid` = :rid AND
|
||||||
`userkey` = :userkey
|
(`id_key`, `act_key`) = (:id_key, :act_key)
|
||||||
",
|
",
|
||||||
named_params! {
|
named_params! {
|
||||||
":rid": rid,
|
":rid": rid,
|
||||||
":userkey": &chat.signee.user,
|
":id_key": &chat.signee.user.id_key,
|
||||||
|
":act_key": &chat.signee.user.act_key,
|
||||||
},
|
},
|
||||||
|row| {
|
|row| {
|
||||||
Ok((
|
Ok((
|
||||||
|
@ -885,13 +885,14 @@ async fn room_msg_post(
|
||||||
let cid = Id::gen();
|
let cid = Id::gen();
|
||||||
conn.execute(
|
conn.execute(
|
||||||
r"
|
r"
|
||||||
INSERT INTO `msg` (`cid`, `rid`, `uid`, `timestamp`, `nonce`, `sig`, `rich_text`)
|
INSERT INTO `msg` (`cid`, `rid`, `uid`, `act_key`, `timestamp`, `nonce`, `sig`, `rich_text`)
|
||||||
VALUES (:cid, :rid, :uid, :timestamp, :nonce, :sig, :rich_text)
|
VALUES (:cid, :rid, :uid, :act_key, :timestamp, :nonce, :sig, :rich_text)
|
||||||
",
|
",
|
||||||
named_params! {
|
named_params! {
|
||||||
":cid": cid,
|
":cid": cid,
|
||||||
":rid": rid,
|
":rid": rid,
|
||||||
":uid": uid,
|
":uid": uid,
|
||||||
|
":act_key": chat.signee.user.act_key,
|
||||||
":timestamp": chat.signee.timestamp,
|
":timestamp": chat.signee.timestamp,
|
||||||
":nonce": chat.signee.nonce,
|
":nonce": chat.signee.nonce,
|
||||||
":rich_text": &chat.signee.payload.rich_text,
|
":rich_text": &chat.signee.payload.rich_text,
|
||||||
|
@ -954,7 +955,7 @@ async fn room_admin(
|
||||||
|
|
||||||
match op.signee.payload.op {
|
match op.signee.payload.op {
|
||||||
RoomAdminOp::AddMember { user, permission } => {
|
RoomAdminOp::AddMember { user, permission } => {
|
||||||
if user != op.signee.user {
|
if user != op.signee.user.id_key {
|
||||||
return Err(error_response!(
|
return Err(error_response!(
|
||||||
StatusCode::NOT_IMPLEMENTED,
|
StatusCode::NOT_IMPLEMENTED,
|
||||||
"not_implemented",
|
"not_implemented",
|
||||||
|
@ -968,17 +969,17 @@ async fn room_admin(
|
||||||
"invalid permission",
|
"invalid permission",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
room_join(&st, rid, user, permission).await?;
|
room_join(&st, rid, &op.signee.user, permission).await?;
|
||||||
}
|
}
|
||||||
RoomAdminOp::RemoveMember { user } => {
|
RoomAdminOp::RemoveMember { user } => {
|
||||||
if user != op.signee.user {
|
if user != op.signee.user.id_key {
|
||||||
return Err(error_response!(
|
return Err(error_response!(
|
||||||
StatusCode::NOT_IMPLEMENTED,
|
StatusCode::NOT_IMPLEMENTED,
|
||||||
"not_implemented",
|
"not_implemented",
|
||||||
"only self-removing is implemented yet",
|
"only self-removing is implemented yet",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
room_leave(&st, rid, user).await?;
|
room_leave(&st, rid, &op.signee.user).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -988,29 +989,12 @@ async fn room_admin(
|
||||||
async fn room_join(
|
async fn room_join(
|
||||||
st: &AppState,
|
st: &AppState,
|
||||||
rid: Id,
|
rid: Id,
|
||||||
user: UserKey,
|
user: &UserKey,
|
||||||
permission: MemberPermission,
|
permission: MemberPermission,
|
||||||
) -> Result<(), ApiError> {
|
) -> Result<(), ApiError> {
|
||||||
let mut conn = st.db.get();
|
let mut conn = st.db.get();
|
||||||
let txn = conn.transaction()?;
|
let txn = conn.transaction()?;
|
||||||
let uid = txn
|
let (uid, _) = txn.get_user(user)?;
|
||||||
.query_row(
|
|
||||||
r"
|
|
||||||
SELECT `uid`
|
|
||||||
FROM `user`
|
|
||||||
WHERE `userkey` = ?
|
|
||||||
",
|
|
||||||
params![user],
|
|
||||||
|row| row.get::<_, i32>(0),
|
|
||||||
)
|
|
||||||
.optional()?
|
|
||||||
.ok_or_else(|| {
|
|
||||||
error_response!(
|
|
||||||
StatusCode::NOT_FOUND,
|
|
||||||
"not_found",
|
|
||||||
"the user does not exist",
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
txn.query_row(
|
txn.query_row(
|
||||||
r"
|
r"
|
||||||
SELECT `attrs`
|
SELECT `attrs`
|
||||||
|
@ -1053,7 +1037,7 @@ async fn room_join(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn room_leave(st: &AppState, rid: Id, user: UserKey) -> Result<(), ApiError> {
|
async fn room_leave(st: &AppState, rid: Id, user: &UserKey) -> Result<(), ApiError> {
|
||||||
let mut conn = st.db.get();
|
let mut conn = st.db.get();
|
||||||
let txn = conn.transaction()?;
|
let txn = conn.transaction()?;
|
||||||
|
|
||||||
|
@ -1062,13 +1046,13 @@ async fn room_leave(st: &AppState, rid: Id, user: UserKey) -> Result<(), ApiErro
|
||||||
r"
|
r"
|
||||||
SELECT `uid`
|
SELECT `uid`
|
||||||
FROM `room_member`
|
FROM `room_member`
|
||||||
JOIN `user` USING (`uid`)
|
JOIN `valid_user_act_key` USING (`uid`)
|
||||||
WHERE `rid` = :rid AND
|
WHERE (`rid`, `id_key`, `act_key`) = (:rid, :id_key, :act_key)
|
||||||
`userkey` = :userkey
|
|
||||||
",
|
",
|
||||||
named_params! {
|
named_params! {
|
||||||
":rid": rid,
|
":rid": rid,
|
||||||
":userkey": user,
|
":id_key": user.id_key,
|
||||||
|
":act_key": user.act_key,
|
||||||
},
|
},
|
||||||
|row| row.get::<_, u64>("uid"),
|
|row| row.get::<_, u64>("uid"),
|
||||||
)
|
)
|
||||||
|
@ -1108,12 +1092,15 @@ async fn room_msg_mark_seen(
|
||||||
SET `last_seen_cid` = MAX(`last_seen_cid`, :cid)
|
SET `last_seen_cid` = MAX(`last_seen_cid`, :cid)
|
||||||
WHERE
|
WHERE
|
||||||
`rid` = :rid AND
|
`rid` = :rid AND
|
||||||
`uid` = (SELECT `uid` FROM `user` WHERE `userkey` = :userkey)
|
`uid` = (SELECT `uid`
|
||||||
|
FROM `valid_user_act_key`
|
||||||
|
WHERE (`id_key`, `act_key`) = (:id_key, :act_key))
|
||||||
",
|
",
|
||||||
named_params! {
|
named_params! {
|
||||||
":cid": cid,
|
":cid": cid,
|
||||||
":rid": rid,
|
":rid": rid,
|
||||||
":userkey": user,
|
":id_key": user.id_key,
|
||||||
|
":act_key": user.act_key,
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ use std::time::{Duration, Instant};
|
||||||
use anyhow::{anyhow, ensure, Context};
|
use anyhow::{anyhow, ensure, Context};
|
||||||
use axum::http::{HeaderMap, HeaderName, StatusCode};
|
use axum::http::{HeaderMap, HeaderName, StatusCode};
|
||||||
use blah_types::{
|
use blah_types::{
|
||||||
get_timestamp, Signed, UserIdentityDesc, UserKey, UserRegisterPayload, X_BLAH_DIFFICULTY,
|
get_timestamp, PubKey, Signed, UserIdentityDesc, UserRegisterPayload, X_BLAH_DIFFICULTY,
|
||||||
X_BLAH_NONCE,
|
X_BLAH_NONCE,
|
||||||
};
|
};
|
||||||
use http_body_util::BodyExt;
|
use http_body_util::BodyExt;
|
||||||
|
@ -270,9 +270,9 @@ pub async fn user_register(
|
||||||
let uid = txn
|
let uid = txn
|
||||||
.query_row(
|
.query_row(
|
||||||
r"
|
r"
|
||||||
INSERT INTO `user` (`userkey`, `last_fetch_time`, `id_desc`)
|
INSERT INTO `user` (`id_key`, `last_fetch_time`, `id_desc`)
|
||||||
VALUES (:id_key, :last_fetch_time, :id_desc)
|
VALUES (:id_key, :last_fetch_time, :id_desc)
|
||||||
ON CONFLICT (`userkey`) DO UPDATE SET
|
ON CONFLICT (`id_key`) DO UPDATE SET
|
||||||
`last_fetch_time` = :last_fetch_time,
|
`last_fetch_time` = :last_fetch_time,
|
||||||
`id_desc` = :id_desc
|
`id_desc` = :id_desc
|
||||||
WHERE `last_fetch_time` < :last_fetch_time
|
WHERE `last_fetch_time` < :last_fetch_time
|
||||||
|
@ -323,20 +323,31 @@ pub async fn user_register(
|
||||||
|
|
||||||
fn validate_id_desc(
|
fn validate_id_desc(
|
||||||
id_url: &Url,
|
id_url: &Url,
|
||||||
id_key: &UserKey,
|
id_key: &PubKey,
|
||||||
id_desc: &UserIdentityDesc,
|
id_desc: &UserIdentityDesc,
|
||||||
now: u64,
|
now: u64,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
ensure!(*id_key == id_desc.id_key, "id_key mismatch");
|
ensure!(*id_key == id_desc.id_key, "id_key mismatch");
|
||||||
|
|
||||||
let profile_signing_key = &id_desc.profile.signee.user;
|
ensure!(
|
||||||
|
*id_key == id_desc.profile.signee.user.id_key,
|
||||||
|
"profile id_key mismatch",
|
||||||
|
);
|
||||||
|
let profile_signing_key = &id_desc.profile.signee.user.act_key;
|
||||||
let mut profile_signed = false;
|
let mut profile_signed = false;
|
||||||
|
|
||||||
for (i, act_key) in id_desc.act_keys.iter().enumerate() {
|
for (i, signed_kdesc) in id_desc.act_keys.iter().enumerate() {
|
||||||
let kdesc = &act_key.signee.payload;
|
let kdesc = &signed_kdesc.signee.payload;
|
||||||
(|| {
|
(|| {
|
||||||
ensure!(act_key.signee.user == *id_key, "not signed by id_key");
|
// act_key itself is signed by id_key, so both are id_key here.
|
||||||
act_key.verify().context("signature verification failed")?;
|
ensure!(
|
||||||
|
signed_kdesc.signee.user.id_key == *id_key
|
||||||
|
&& signed_kdesc.signee.user.act_key == *id_key,
|
||||||
|
"not signed by id_key",
|
||||||
|
);
|
||||||
|
signed_kdesc
|
||||||
|
.verify()
|
||||||
|
.context("signature verification failed")?;
|
||||||
if now < kdesc.expire_time && *profile_signing_key == kdesc.act_key {
|
if now < kdesc.expire_time && *profile_signing_key == kdesc.act_key {
|
||||||
profile_signed = true;
|
profile_signed = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ use anyhow::Result;
|
||||||
use axum::http::HeaderMap;
|
use axum::http::HeaderMap;
|
||||||
use blah_types::{
|
use blah_types::{
|
||||||
get_timestamp, AuthPayload, ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, Id,
|
get_timestamp, AuthPayload, ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, Id,
|
||||||
MemberPermission, RichText, RoomAdminOp, RoomAdminPayload, RoomAttrs, RoomMetadata,
|
MemberPermission, PubKey, RichText, RoomAdminOp, RoomAdminPayload, RoomAttrs, RoomMetadata,
|
||||||
ServerPermission, Signed, SignedChatMsg, UserActKeyDesc, UserIdentityDesc, UserKey,
|
ServerPermission, Signed, SignedChatMsg, UserActKeyDesc, UserIdentityDesc, UserKey,
|
||||||
UserProfile, UserRegisterPayload, WithMsgId, X_BLAH_DIFFICULTY, X_BLAH_NONCE,
|
UserProfile, UserRegisterPayload, WithMsgId, X_BLAH_DIFFICULTY, X_BLAH_NONCE,
|
||||||
};
|
};
|
||||||
|
@ -52,14 +52,31 @@ unsafe_allow_id_url_custom_port = true
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
static ALICE_PRIV: LazyLock<SigningKey> = LazyLock::new(|| SigningKey::from_bytes(&[b'A'; 32]));
|
struct User {
|
||||||
static ALICE: LazyLock<UserKey> = LazyLock::new(|| UserKey(ALICE_PRIV.verifying_key().to_bytes()));
|
pubkeys: UserKey,
|
||||||
static BOB_PRIV: LazyLock<SigningKey> = LazyLock::new(|| SigningKey::from_bytes(&[b'B'; 32]));
|
id_priv: SigningKey,
|
||||||
static BOB: LazyLock<UserKey> = LazyLock::new(|| UserKey(BOB_PRIV.verifying_key().to_bytes()));
|
act_priv: SigningKey,
|
||||||
static CAROL_PRIV: LazyLock<SigningKey> = LazyLock::new(|| SigningKey::from_bytes(&[b'C'; 32]));
|
}
|
||||||
static CAROL: LazyLock<UserKey> = LazyLock::new(|| UserKey(CAROL_PRIV.verifying_key().to_bytes()));
|
|
||||||
|
|
||||||
static CAROL_ACT_PRIV: LazyLock<SigningKey> = LazyLock::new(|| SigningKey::from_bytes(&[b'c'; 32]));
|
impl User {
|
||||||
|
fn new(b: u8) -> Self {
|
||||||
|
assert!(b.is_ascii_uppercase());
|
||||||
|
let id_priv = SigningKey::from_bytes(&[b; 32]);
|
||||||
|
let act_priv = SigningKey::from_bytes(&[b.to_ascii_lowercase(); 32]);
|
||||||
|
Self {
|
||||||
|
pubkeys: UserKey {
|
||||||
|
id_key: PubKey(id_priv.verifying_key().to_bytes()),
|
||||||
|
act_key: PubKey(act_priv.verifying_key().to_bytes()),
|
||||||
|
},
|
||||||
|
id_priv,
|
||||||
|
act_priv,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ALICE: LazyLock<User> = LazyLock::new(|| User::new(b'A'));
|
||||||
|
static BOB: LazyLock<User> = LazyLock::new(|| User::new(b'B'));
|
||||||
|
static CAROL: LazyLock<User> = LazyLock::new(|| User::new(b'C'));
|
||||||
|
|
||||||
#[fixture]
|
#[fixture]
|
||||||
fn rng() -> impl RngCore {
|
fn rng() -> impl RngCore {
|
||||||
|
@ -165,15 +182,25 @@ impl Server {
|
||||||
.map_ok(|resp| resp.unwrap())
|
.map_ok(|resp| resp.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sign<T: Serialize>(&self, user: &User, msg: T) -> Signed<T> {
|
||||||
|
Signed::sign(
|
||||||
|
&user.pubkeys.id_key,
|
||||||
|
&user.act_priv,
|
||||||
|
get_timestamp(),
|
||||||
|
&mut *self.rng.borrow_mut(),
|
||||||
|
msg,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
fn create_room(
|
fn create_room(
|
||||||
&self,
|
&self,
|
||||||
key: &SigningKey,
|
user: &User,
|
||||||
attrs: RoomAttrs,
|
attrs: RoomAttrs,
|
||||||
title: &str,
|
title: &str,
|
||||||
) -> impl Future<Output = Result<Id>> + use<'_> {
|
) -> impl Future<Output = Result<Id>> + use<'_> {
|
||||||
let req = sign(
|
let req = self.sign(
|
||||||
key,
|
user,
|
||||||
&mut *self.rng.borrow_mut(),
|
|
||||||
CreateRoomPayload::Group(CreateGroup {
|
CreateRoomPayload::Group(CreateGroup {
|
||||||
attrs,
|
attrs,
|
||||||
title: title.to_string(),
|
title: title.to_string(),
|
||||||
|
@ -190,17 +217,16 @@ impl Server {
|
||||||
fn join_room(
|
fn join_room(
|
||||||
&self,
|
&self,
|
||||||
rid: Id,
|
rid: Id,
|
||||||
key: &SigningKey,
|
user: &User,
|
||||||
permission: MemberPermission,
|
permission: MemberPermission,
|
||||||
) -> impl Future<Output = Result<()>> + use<'_> {
|
) -> impl Future<Output = Result<()>> + use<'_> {
|
||||||
let req = sign(
|
let req = self.sign(
|
||||||
key,
|
user,
|
||||||
&mut *self.rng.borrow_mut(),
|
|
||||||
RoomAdminPayload {
|
RoomAdminPayload {
|
||||||
room: rid,
|
room: rid,
|
||||||
op: RoomAdminOp::AddMember {
|
op: RoomAdminOp::AddMember {
|
||||||
permission,
|
permission,
|
||||||
user: UserKey(key.verifying_key().to_bytes()),
|
user: user.pubkeys.id_key.clone(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -208,14 +234,13 @@ impl Server {
|
||||||
.map_ok(|None| {})
|
.map_ok(|None| {})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn leave_room(&self, rid: Id, key: &SigningKey) -> impl Future<Output = Result<()>> + use<'_> {
|
fn leave_room(&self, rid: Id, user: &User) -> impl Future<Output = Result<()>> + use<'_> {
|
||||||
let req = sign(
|
let req = self.sign(
|
||||||
key,
|
user,
|
||||||
&mut *self.rng.borrow_mut(),
|
|
||||||
RoomAdminPayload {
|
RoomAdminPayload {
|
||||||
room: rid,
|
room: rid,
|
||||||
op: RoomAdminOp::RemoveMember {
|
op: RoomAdminOp::RemoveMember {
|
||||||
user: UserKey(key.verifying_key().to_bytes()),
|
user: user.pubkeys.id_key.clone(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -226,12 +251,11 @@ impl Server {
|
||||||
fn post_chat(
|
fn post_chat(
|
||||||
&self,
|
&self,
|
||||||
rid: Id,
|
rid: Id,
|
||||||
key: &SigningKey,
|
user: &User,
|
||||||
text: &str,
|
text: &str,
|
||||||
) -> impl Future<Output = Result<WithMsgId<SignedChatMsg>>> + use<'_> {
|
) -> impl Future<Output = Result<WithMsgId<SignedChatMsg>>> + use<'_> {
|
||||||
let msg = sign(
|
let msg = self.sign(
|
||||||
key,
|
user,
|
||||||
&mut *self.rng.borrow_mut(),
|
|
||||||
ChatPayload {
|
ChatPayload {
|
||||||
room: rid,
|
room: rid,
|
||||||
rich_text: text.into(),
|
rich_text: text.into(),
|
||||||
|
@ -262,7 +286,7 @@ fn server() -> Server {
|
||||||
let mut add_user = conn
|
let mut add_user = conn
|
||||||
.prepare(
|
.prepare(
|
||||||
r"
|
r"
|
||||||
INSERT INTO `user` (`userkey`, `permission`, `last_fetch_time`, `id_desc`)
|
INSERT INTO `user` (`id_key`, `permission`, `last_fetch_time`, `id_desc`)
|
||||||
VALUES (?, ?, 0, '{}')
|
VALUES (?, ?, 0, '{}')
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
|
@ -279,9 +303,13 @@ fn server() -> Server {
|
||||||
(&*ALICE, ServerPermission::ALL),
|
(&*ALICE, ServerPermission::ALL),
|
||||||
(&BOB, ServerPermission::empty()),
|
(&BOB, ServerPermission::empty()),
|
||||||
] {
|
] {
|
||||||
add_user.execute(params![user, perm]).unwrap();
|
add_user
|
||||||
|
.execute(params![user.pubkeys.id_key, perm])
|
||||||
|
.unwrap();
|
||||||
let uid = conn.last_insert_rowid();
|
let uid = conn.last_insert_rowid();
|
||||||
add_act_key.execute(params![uid, user, i64::MAX]).unwrap();
|
add_act_key
|
||||||
|
.execute(params![uid, user.pubkeys.act_key, i64::MAX])
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let db = Database::from_raw(conn).unwrap();
|
let db = Database::from_raw(conn).unwrap();
|
||||||
|
@ -314,12 +342,16 @@ async fn smoke(server: Server) {
|
||||||
assert_eq!(got, exp);
|
assert_eq!(got, exp);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sign<T: Serialize>(key: &SigningKey, rng: &mut dyn RngCore, payload: T) -> Signed<T> {
|
fn auth(user: &User, rng: &mut impl RngCore) -> String {
|
||||||
Signed::sign(key, get_timestamp(), rng, payload).unwrap()
|
let msg = Signed::sign(
|
||||||
}
|
&user.pubkeys.id_key,
|
||||||
|
&user.act_priv,
|
||||||
fn auth(key: &SigningKey, rng: &mut impl RngCore) -> String {
|
get_timestamp(),
|
||||||
serde_json::to_string(&sign(key, rng, AuthPayload {})).unwrap()
|
rng,
|
||||||
|
AuthPayload {},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
serde_json::to_string(&msg).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
|
@ -345,26 +377,26 @@ async fn room_create_get(server: Server, ref mut rng: impl RngCore, #[case] publ
|
||||||
|
|
||||||
// Alice has permission.
|
// Alice has permission.
|
||||||
let rid = server
|
let rid = server
|
||||||
.create_room(&ALICE_PRIV, room_meta.attrs, title)
|
.create_room(&ALICE, room_meta.attrs, title)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
room_meta.rid = rid;
|
room_meta.rid = rid;
|
||||||
|
|
||||||
// Bob has no permission.
|
// Bob has no permission.
|
||||||
server
|
server
|
||||||
.create_room(&BOB_PRIV, room_meta.attrs, title)
|
.create_room(&BOB, room_meta.attrs, title)
|
||||||
.await
|
.await
|
||||||
.expect_api_err(StatusCode::FORBIDDEN, "permission_denied");
|
.expect_api_err(StatusCode::FORBIDDEN, "permission_denied");
|
||||||
|
|
||||||
// Alice can always access it.
|
// Alice can always access it.
|
||||||
let got_meta = server
|
let got_meta = server
|
||||||
.get::<RoomMetadata>(&format!("/room/{rid}"), Some(&auth(&ALICE_PRIV, rng)))
|
.get::<RoomMetadata>(&format!("/room/{rid}"), Some(&auth(&ALICE, rng)))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(got_meta, room_meta);
|
assert_eq!(got_meta, room_meta);
|
||||||
|
|
||||||
// Bob or public can access it when it is public.
|
// Bob or public can access it when it is public.
|
||||||
for auth in [None, Some(auth(&BOB_PRIV, rng))] {
|
for auth in [None, Some(auth(&BOB, rng))] {
|
||||||
let resp = server
|
let resp = server
|
||||||
.get::<RoomMetadata>(&format!("/room/{rid}"), auth.as_deref())
|
.get::<RoomMetadata>(&format!("/room/{rid}"), auth.as_deref())
|
||||||
.await;
|
.await;
|
||||||
|
@ -401,13 +433,13 @@ async fn room_create_get(server: Server, ref mut rng: impl RngCore, #[case] publ
|
||||||
.await
|
.await
|
||||||
.expect_api_err(StatusCode::UNAUTHORIZED, "unauthorized");
|
.expect_api_err(StatusCode::UNAUTHORIZED, "unauthorized");
|
||||||
let got_joined = server
|
let got_joined = server
|
||||||
.get::<RoomList>("/room?filter=joined", Some(&auth(&ALICE_PRIV, rng)))
|
.get::<RoomList>("/room?filter=joined", Some(&auth(&ALICE, rng)))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(got_joined, expect_list(true, Some(MemberPermission::ALL)));
|
assert_eq!(got_joined, expect_list(true, Some(MemberPermission::ALL)));
|
||||||
|
|
||||||
let got_joined = server
|
let got_joined = server
|
||||||
.get::<RoomList>("/room?filter=joined", Some(&auth(&BOB_PRIV, rng)))
|
.get::<RoomList>("/room?filter=joined", Some(&auth(&BOB, rng)))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(got_joined, expect_list(false, None));
|
assert_eq!(got_joined, expect_list(false, None));
|
||||||
|
@ -417,41 +449,40 @@ async fn room_create_get(server: Server, ref mut rng: impl RngCore, #[case] publ
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn room_join_leave(server: Server, ref mut rng: impl RngCore) {
|
async fn room_join_leave(server: Server, ref mut rng: impl RngCore) {
|
||||||
let rid_pub = server
|
let rid_pub = server
|
||||||
.create_room(&ALICE_PRIV, RoomAttrs::PUBLIC_JOINABLE, "public room")
|
.create_room(&ALICE, RoomAttrs::PUBLIC_JOINABLE, "public room")
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let rid_priv = server
|
let rid_priv = server
|
||||||
.create_room(&ALICE_PRIV, RoomAttrs::empty(), "private room")
|
.create_room(&ALICE, RoomAttrs::empty(), "private room")
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let join =
|
let join = |rid, user| server.join_room(rid, user, MemberPermission::MAX_SELF_ADD);
|
||||||
|rid: Id, key: &SigningKey| server.join_room(rid, key, MemberPermission::MAX_SELF_ADD);
|
|
||||||
|
|
||||||
// Ok.
|
// Ok.
|
||||||
join(rid_pub, &BOB_PRIV).await.unwrap();
|
join(rid_pub, &BOB).await.unwrap();
|
||||||
// Already joined.
|
// Already joined.
|
||||||
join(rid_pub, &BOB_PRIV)
|
join(rid_pub, &BOB)
|
||||||
.await
|
.await
|
||||||
.expect_api_err(StatusCode::CONFLICT, "exists");
|
.expect_api_err(StatusCode::CONFLICT, "exists");
|
||||||
// Not permitted.
|
// Not permitted.
|
||||||
join(rid_priv, &BOB_PRIV)
|
join(rid_priv, &BOB)
|
||||||
.await
|
.await
|
||||||
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
||||||
// Not exists.
|
// Not exists.
|
||||||
join(Id::INVALID, &BOB_PRIV)
|
join(Id::INVALID, &BOB)
|
||||||
.await
|
.await
|
||||||
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
||||||
// Overly high permission.
|
// Overly high permission.
|
||||||
server
|
server
|
||||||
.join_room(rid_priv, &BOB_PRIV, MemberPermission::ALL)
|
.join_room(rid_priv, &BOB, MemberPermission::ALL)
|
||||||
.await
|
.await
|
||||||
.expect_api_err(StatusCode::BAD_REQUEST, "deserialization");
|
.expect_api_err(StatusCode::BAD_REQUEST, "deserialization");
|
||||||
|
|
||||||
// Bob is joined now.
|
// Bob is joined now.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
server
|
server
|
||||||
.get::<RoomList>("/room?filter=joined", Some(&auth(&BOB_PRIV, rng)))
|
.get::<RoomList>("/room?filter=joined", Some(&auth(&BOB, rng)))
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.rooms
|
.rooms
|
||||||
|
@ -459,20 +490,20 @@ async fn room_join_leave(server: Server, ref mut rng: impl RngCore) {
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
|
|
||||||
let leave = |rid: Id, key: &SigningKey| server.leave_room(rid, key);
|
let leave = |rid, user| server.leave_room(rid, user);
|
||||||
|
|
||||||
// Ok.
|
// Ok.
|
||||||
leave(rid_pub, &BOB_PRIV).await.unwrap();
|
leave(rid_pub, &BOB).await.unwrap();
|
||||||
// Already left.
|
// Already left.
|
||||||
leave(rid_pub, &BOB_PRIV)
|
leave(rid_pub, &BOB)
|
||||||
.await
|
.await
|
||||||
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
||||||
// Unpermitted and not inside.
|
// Unpermitted and not inside.
|
||||||
leave(rid_priv, &BOB_PRIV)
|
leave(rid_priv, &BOB)
|
||||||
.await
|
.await
|
||||||
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
||||||
// Invalid room.
|
// Invalid room.
|
||||||
leave(Id::INVALID, &BOB_PRIV)
|
leave(Id::INVALID, &BOB)
|
||||||
.await
|
.await
|
||||||
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
||||||
}
|
}
|
||||||
|
@ -482,21 +513,20 @@ async fn room_join_leave(server: Server, ref mut rng: impl RngCore) {
|
||||||
async fn room_chat_post_read(server: Server, ref mut rng: impl RngCore) {
|
async fn room_chat_post_read(server: Server, ref mut rng: impl RngCore) {
|
||||||
let rid_pub = server
|
let rid_pub = server
|
||||||
.create_room(
|
.create_room(
|
||||||
&ALICE_PRIV,
|
&ALICE,
|
||||||
RoomAttrs::PUBLIC_READABLE | RoomAttrs::PUBLIC_JOINABLE,
|
RoomAttrs::PUBLIC_READABLE | RoomAttrs::PUBLIC_JOINABLE,
|
||||||
"public room",
|
"public room",
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let rid_priv = server
|
let rid_priv = server
|
||||||
.create_room(&ALICE_PRIV, RoomAttrs::empty(), "private room")
|
.create_room(&ALICE, RoomAttrs::empty(), "private room")
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut chat = |rid: Id, key: &SigningKey, msg: &str| {
|
let chat = |rid: Id, user: &User, msg: &str| {
|
||||||
sign(
|
server.sign(
|
||||||
key,
|
user,
|
||||||
rng,
|
|
||||||
ChatPayload {
|
ChatPayload {
|
||||||
room: rid,
|
room: rid,
|
||||||
rich_text: RichText::from(msg),
|
rich_text: RichText::from(msg),
|
||||||
|
@ -510,8 +540,8 @@ async fn room_chat_post_read(server: Server, ref mut rng: impl RngCore) {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ok.
|
// Ok.
|
||||||
let chat1 = chat(rid_pub, &ALICE_PRIV, "one");
|
let chat1 = chat(rid_pub, &ALICE, "one");
|
||||||
let chat2 = chat(rid_pub, &ALICE_PRIV, "two");
|
let chat2 = chat(rid_pub, &ALICE, "two");
|
||||||
let cid1 = post(rid_pub, chat1.clone()).await.unwrap();
|
let cid1 = post(rid_pub, chat1.clone()).await.unwrap();
|
||||||
let cid2 = post(rid_pub, chat2.clone()).await.unwrap();
|
let cid2 = post(rid_pub, chat2.clone()).await.unwrap();
|
||||||
|
|
||||||
|
@ -521,26 +551,26 @@ async fn room_chat_post_read(server: Server, ref mut rng: impl RngCore) {
|
||||||
.expect_api_err(StatusCode::BAD_REQUEST, "duplicated_nonce");
|
.expect_api_err(StatusCode::BAD_REQUEST, "duplicated_nonce");
|
||||||
|
|
||||||
// Wrong room.
|
// Wrong room.
|
||||||
post(rid_pub, chat(rid_priv, &ALICE_PRIV, "wrong room"))
|
post(rid_pub, chat(rid_priv, &ALICE, "wrong room"))
|
||||||
.await
|
.await
|
||||||
.expect_api_err(StatusCode::BAD_REQUEST, "invalid_request");
|
.expect_api_err(StatusCode::BAD_REQUEST, "invalid_request");
|
||||||
|
|
||||||
// Not a member.
|
// Not a member.
|
||||||
post(rid_pub, chat(rid_pub, &BOB_PRIV, "not a member"))
|
post(rid_pub, chat(rid_pub, &BOB, "not a member"))
|
||||||
.await
|
.await
|
||||||
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
||||||
|
|
||||||
// Is a member but without permission.
|
// Is a member but without permission.
|
||||||
server
|
server
|
||||||
.join_room(rid_pub, &BOB_PRIV, MemberPermission::empty())
|
.join_room(rid_pub, &BOB, MemberPermission::empty())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
post(rid_pub, chat(rid_pub, &BOB_PRIV, "no permission"))
|
post(rid_pub, chat(rid_pub, &BOB, "no permission"))
|
||||||
.await
|
.await
|
||||||
.expect_api_err(StatusCode::FORBIDDEN, "permission_denied");
|
.expect_api_err(StatusCode::FORBIDDEN, "permission_denied");
|
||||||
|
|
||||||
// Room not exists.
|
// Room not exists.
|
||||||
post(Id::INVALID, chat(Id::INVALID, &ALICE_PRIV, "not permitted"))
|
post(Id::INVALID, chat(Id::INVALID, &ALICE, "not permitted"))
|
||||||
.await
|
.await
|
||||||
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
||||||
|
|
||||||
|
@ -605,19 +635,13 @@ async fn room_chat_post_read(server: Server, ref mut rng: impl RngCore) {
|
||||||
|
|
||||||
// Not a member.
|
// Not a member.
|
||||||
server
|
server
|
||||||
.get::<RoomMsgs>(
|
.get::<RoomMsgs>(&format!("/room/{rid_priv}/msg"), Some(&auth(&BOB, rng)))
|
||||||
&format!("/room/{rid_priv}/msg"),
|
|
||||||
Some(&auth(&BOB_PRIV, rng)),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
||||||
|
|
||||||
// Ok.
|
// Ok.
|
||||||
let msgs = server
|
let msgs = server
|
||||||
.get::<RoomMsgs>(
|
.get::<RoomMsgs>(&format!("/room/{rid_priv}/msg"), Some(&auth(&ALICE, rng)))
|
||||||
&format!("/room/{rid_priv}/msg"),
|
|
||||||
Some(&auth(&ALICE_PRIV, rng)),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(msgs, RoomMsgs::default());
|
assert_eq!(msgs, RoomMsgs::default());
|
||||||
|
@ -629,18 +653,18 @@ async fn last_seen(server: Server, ref mut rng: impl RngCore) {
|
||||||
let title = "public room";
|
let title = "public room";
|
||||||
let attrs = RoomAttrs::PUBLIC_READABLE | RoomAttrs::PUBLIC_JOINABLE;
|
let attrs = RoomAttrs::PUBLIC_READABLE | RoomAttrs::PUBLIC_JOINABLE;
|
||||||
let member_perm = MemberPermission::ALL;
|
let member_perm = MemberPermission::ALL;
|
||||||
let rid = server.create_room(&ALICE_PRIV, attrs, title).await.unwrap();
|
let rid = server.create_room(&ALICE, attrs, title).await.unwrap();
|
||||||
server
|
server
|
||||||
.join_room(rid, &BOB_PRIV, MemberPermission::MAX_SELF_ADD)
|
.join_room(rid, &BOB, MemberPermission::MAX_SELF_ADD)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let alice_chat1 = server.post_chat(rid, &ALICE_PRIV, "alice1").await.unwrap();
|
let alice_chat1 = server.post_chat(rid, &ALICE, "alice1").await.unwrap();
|
||||||
let alice_chat2 = server.post_chat(rid, &ALICE_PRIV, "alice2").await.unwrap();
|
let alice_chat2 = server.post_chat(rid, &ALICE, "alice2").await.unwrap();
|
||||||
|
|
||||||
// 2 new msgs.
|
// 2 new msgs.
|
||||||
let rooms = server
|
let rooms = server
|
||||||
.get::<RoomList>("/room?filter=unseen", Some(&auth(&ALICE_PRIV, rng)))
|
.get::<RoomList>("/room?filter=unseen", Some(&auth(&ALICE, rng)))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -660,21 +684,21 @@ async fn last_seen(server: Server, ref mut rng: impl RngCore) {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
let seen = |key: &SigningKey, cid: Id| {
|
let seen = |user: &User, cid: Id| {
|
||||||
server.request::<NoContent, NoContent>(
|
server.request::<NoContent, NoContent>(
|
||||||
Method::POST,
|
Method::POST,
|
||||||
&format!("/room/{rid}/msg/{cid}/seen"),
|
&format!("/room/{rid}/msg/{cid}/seen"),
|
||||||
Some(&auth(key, &mut *server.rng.borrow_mut())),
|
Some(&auth(user, &mut *server.rng.borrow_mut())),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mark the first one seen.
|
// Mark the first one seen.
|
||||||
seen(&ALICE_PRIV, alice_chat1.cid).await.unwrap();
|
seen(&ALICE, alice_chat1.cid).await.unwrap();
|
||||||
|
|
||||||
// 1 new msg.
|
// 1 new msg.
|
||||||
let rooms = server
|
let rooms = server
|
||||||
.get::<RoomList>("/room?filter=unseen", Some(&auth(&ALICE_PRIV, rng)))
|
.get::<RoomList>("/room?filter=unseen", Some(&auth(&ALICE, rng)))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -695,17 +719,17 @@ async fn last_seen(server: Server, ref mut rng: impl RngCore) {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mark the second one seen. Now there is no new messages.
|
// Mark the second one seen. Now there is no new messages.
|
||||||
seen(&ALICE_PRIV, alice_chat2.cid).await.unwrap();
|
seen(&ALICE, alice_chat2.cid).await.unwrap();
|
||||||
let rooms = server
|
let rooms = server
|
||||||
.get::<RoomList>("/room?filter=unseen", Some(&auth(&ALICE_PRIV, rng)))
|
.get::<RoomList>("/room?filter=unseen", Some(&auth(&ALICE, rng)))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(rooms, RoomList::default());
|
assert_eq!(rooms, RoomList::default());
|
||||||
|
|
||||||
// Marking a seen message seen is a no-op.
|
// Marking a seen message seen is a no-op.
|
||||||
seen(&ALICE_PRIV, alice_chat2.cid).await.unwrap();
|
seen(&ALICE, alice_chat2.cid).await.unwrap();
|
||||||
let rooms = server
|
let rooms = server
|
||||||
.get::<RoomList>("/room?filter=unseen", Some(&auth(&ALICE_PRIV, rng)))
|
.get::<RoomList>("/room?filter=unseen", Some(&auth(&ALICE, rng)))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(rooms, RoomList::default());
|
assert_eq!(rooms, RoomList::default());
|
||||||
|
@ -714,11 +738,12 @@ async fn last_seen(server: Server, ref mut rng: impl RngCore) {
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn peer_chat(server: Server, ref mut rng: impl RngCore) {
|
async fn peer_chat(server: Server, ref mut rng: impl RngCore) {
|
||||||
let mut create_chat = |src: &SigningKey, tgt: &UserKey| {
|
let create_chat = |src: &User, tgt: &User| {
|
||||||
let req = sign(
|
let req = server.sign(
|
||||||
src,
|
src,
|
||||||
rng,
|
CreateRoomPayload::PeerChat(CreatePeerChat {
|
||||||
CreateRoomPayload::PeerChat(CreatePeerChat { peer: tgt.clone() }),
|
peer: tgt.pubkeys.id_key.clone(),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
server
|
server
|
||||||
.request::<_, Id>(Method::POST, "/room/create", None, Some(req))
|
.request::<_, Id>(Method::POST, "/room/create", None, Some(req))
|
||||||
|
@ -726,15 +751,15 @@ async fn peer_chat(server: Server, ref mut rng: impl RngCore) {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Bob disallows peer chat.
|
// Bob disallows peer chat.
|
||||||
create_chat(&ALICE_PRIV, &BOB)
|
create_chat(&ALICE, &BOB)
|
||||||
.await
|
.await
|
||||||
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
||||||
|
|
||||||
// Alice accepts bob.
|
// Alice accepts bob.
|
||||||
let rid = create_chat(&BOB_PRIV, &ALICE).await.unwrap();
|
let rid = create_chat(&BOB, &ALICE).await.unwrap();
|
||||||
|
|
||||||
// Room already exists.
|
// Room already exists.
|
||||||
create_chat(&BOB_PRIV, &ALICE)
|
create_chat(&BOB, &ALICE)
|
||||||
.await
|
.await
|
||||||
.expect_api_err(StatusCode::CONFLICT, "exists");
|
.expect_api_err(StatusCode::CONFLICT, "exists");
|
||||||
|
|
||||||
|
@ -750,7 +775,7 @@ async fn peer_chat(server: Server, ref mut rng: impl RngCore) {
|
||||||
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
||||||
|
|
||||||
// Both alice and bob are in the room.
|
// Both alice and bob are in the room.
|
||||||
for (key, peer) in [(&*ALICE_PRIV, &*BOB), (&*BOB_PRIV, &*ALICE)] {
|
for (key, peer) in [(&*ALICE, &*BOB), (&*BOB, &*ALICE)] {
|
||||||
let mut expect_meta = RoomMetadata {
|
let mut expect_meta = RoomMetadata {
|
||||||
rid,
|
rid,
|
||||||
title: None,
|
title: None,
|
||||||
|
@ -769,7 +794,7 @@ async fn peer_chat(server: Server, ref mut rng: impl RngCore) {
|
||||||
assert_eq!(meta, expect_meta);
|
assert_eq!(meta, expect_meta);
|
||||||
|
|
||||||
expect_meta.member_permission = Some(MemberPermission::MAX_PEER_CHAT);
|
expect_meta.member_permission = Some(MemberPermission::MAX_PEER_CHAT);
|
||||||
expect_meta.peer_user = Some(peer.clone());
|
expect_meta.peer_user = Some(peer.pubkeys.id_key.clone());
|
||||||
let rooms = server
|
let rooms = server
|
||||||
.get::<RoomList>("/room?filter=joined", Some(&auth(key, rng)))
|
.get::<RoomList>("/room?filter=joined", Some(&auth(key, rng)))
|
||||||
.await
|
.await
|
||||||
|
@ -789,14 +814,14 @@ async fn peer_chat(server: Server, ref mut rng: impl RngCore) {
|
||||||
async fn register(server: Server) {
|
async fn register(server: Server) {
|
||||||
let rid = server
|
let rid = server
|
||||||
.create_room(
|
.create_room(
|
||||||
&ALICE_PRIV,
|
&ALICE,
|
||||||
RoomAttrs::PUBLIC_READABLE | RoomAttrs::PUBLIC_JOINABLE,
|
RoomAttrs::PUBLIC_READABLE | RoomAttrs::PUBLIC_JOINABLE,
|
||||||
"public room",
|
"public room",
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let get_me = |user: Option<&SigningKey>| {
|
let get_me = |user: Option<&User>| {
|
||||||
let auth = user.map(|user| auth(user, &mut *server.rng()));
|
let auth = user.map(|user| auth(user, &mut *server.rng()));
|
||||||
server
|
server
|
||||||
.request::<(), ()>(Method::GET, "/user/me", auth.as_deref(), None)
|
.request::<(), ()>(Method::GET, "/user/me", auth.as_deref(), None)
|
||||||
|
@ -819,10 +844,10 @@ async fn register(server: Server) {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Alice is registered.
|
// Alice is registered.
|
||||||
get_me(Some(&ALICE_PRIV)).await.unwrap();
|
get_me(Some(&ALICE)).await.unwrap();
|
||||||
|
|
||||||
// Carol is not registered.
|
// Carol is not registered.
|
||||||
let (challenge_nonce, diff) = get_me(Some(&CAROL_PRIV)).await.unwrap_err();
|
let (challenge_nonce, diff) = get_me(Some(&CAROL)).await.unwrap_err();
|
||||||
assert_eq!(diff, REGISTER_DIFFICULTY);
|
assert_eq!(diff, REGISTER_DIFFICULTY);
|
||||||
|
|
||||||
// Without token.
|
// Without token.
|
||||||
|
@ -830,7 +855,7 @@ async fn register(server: Server) {
|
||||||
assert_eq!(ret2, (challenge_nonce, diff));
|
assert_eq!(ret2, (challenge_nonce, diff));
|
||||||
|
|
||||||
let mut req = UserRegisterPayload {
|
let mut req = UserRegisterPayload {
|
||||||
id_key: CAROL.clone(),
|
id_key: CAROL.pubkeys.id_key.clone(),
|
||||||
// Fake values.
|
// Fake values.
|
||||||
server_url: "http://invalid.example.com".parse().unwrap(),
|
server_url: "http://invalid.example.com".parse().unwrap(),
|
||||||
id_url: "file:///etc/passwd".parse().unwrap(),
|
id_url: "file:///etc/passwd".parse().unwrap(),
|
||||||
|
@ -842,7 +867,7 @@ async fn register(server: Server) {
|
||||||
.map_ok(|_| {})
|
.map_ok(|_| {})
|
||||||
};
|
};
|
||||||
let sign_with_difficulty = |req: &UserRegisterPayload, pass: bool| loop {
|
let sign_with_difficulty = |req: &UserRegisterPayload, pass: bool| loop {
|
||||||
let signed = sign(&CAROL_PRIV, &mut *server.rng(), req.clone());
|
let signed = server.sign(&CAROL, req.clone());
|
||||||
let mut h = Sha256::new();
|
let mut h = Sha256::new();
|
||||||
h.update(signed.canonical_signee());
|
h.update(signed.canonical_signee());
|
||||||
let h = h.finalize();
|
let h = h.finalize();
|
||||||
|
@ -850,8 +875,7 @@ async fn register(server: Server) {
|
||||||
return signed;
|
return signed;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let register_fast =
|
let register_fast = |req: &UserRegisterPayload| register(server.sign(&CAROL, req.clone()));
|
||||||
|req: &UserRegisterPayload| register(sign(&CAROL_PRIV, &mut *server.rng(), req.clone()));
|
|
||||||
|
|
||||||
register_fast(&req)
|
register_fast(&req)
|
||||||
.await
|
.await
|
||||||
|
@ -938,26 +962,32 @@ async fn register(server: Server) {
|
||||||
(StatusCode::OK, desc.clone())
|
(StatusCode::OK, desc.clone())
|
||||||
}}
|
}}
|
||||||
};
|
};
|
||||||
|
let sign_profile = |url: Url| {
|
||||||
|
server.sign(
|
||||||
|
&CAROL,
|
||||||
|
UserProfile {
|
||||||
|
preferred_chat_server_urls: Vec::new(),
|
||||||
|
id_urls: vec![url],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
};
|
||||||
let mut id_desc = {
|
let mut id_desc = {
|
||||||
let act_key = sign(
|
// Sign using id_key.
|
||||||
&CAROL_PRIV,
|
let act_key = Signed::sign(
|
||||||
|
&CAROL.pubkeys.id_key,
|
||||||
|
&CAROL.id_priv,
|
||||||
|
get_timestamp(),
|
||||||
&mut *server.rng(),
|
&mut *server.rng(),
|
||||||
UserActKeyDesc {
|
UserActKeyDesc {
|
||||||
act_key: UserKey(CAROL_ACT_PRIV.verifying_key().to_bytes()),
|
act_key: CAROL.pubkeys.act_key.clone(),
|
||||||
expire_time: u64::MAX,
|
expire_time: u64::MAX,
|
||||||
comment: "comment".into(),
|
comment: "comment".into(),
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
let profile = sign(
|
.unwrap();
|
||||||
&CAROL_ACT_PRIV,
|
let profile = sign_profile(req.id_url.join("/mismatch").unwrap());
|
||||||
&mut *server.rng(),
|
|
||||||
UserProfile {
|
|
||||||
preferred_chat_server_urls: Vec::new(),
|
|
||||||
id_urls: vec![req.id_url.join("/mismatch").unwrap()],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
UserIdentityDesc {
|
UserIdentityDesc {
|
||||||
id_key: CAROL.clone(),
|
id_key: CAROL.pubkeys.id_key.clone(),
|
||||||
act_keys: vec![act_key],
|
act_keys: vec![act_key],
|
||||||
profile,
|
profile,
|
||||||
}
|
}
|
||||||
|
@ -970,28 +1000,21 @@ async fn register(server: Server) {
|
||||||
.expect_api_err(StatusCode::UNAUTHORIZED, "invalid_id_description");
|
.expect_api_err(StatusCode::UNAUTHORIZED, "invalid_id_description");
|
||||||
|
|
||||||
// Still not registered.
|
// Still not registered.
|
||||||
get_me(Some(&CAROL_PRIV)).await.unwrap_err();
|
get_me(Some(&CAROL)).await.unwrap_err();
|
||||||
server
|
server
|
||||||
.join_room(rid, &CAROL_PRIV, MemberPermission::MAX_SELF_ADD)
|
.join_room(rid, &CAROL, MemberPermission::MAX_SELF_ADD)
|
||||||
.await
|
.await
|
||||||
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
||||||
|
|
||||||
// Finally pass.
|
// Finally pass.
|
||||||
id_desc.profile = sign(
|
id_desc.profile = sign_profile(req.id_url.clone());
|
||||||
&CAROL_ACT_PRIV,
|
|
||||||
&mut *server.rng(),
|
|
||||||
UserProfile {
|
|
||||||
preferred_chat_server_urls: Vec::new(),
|
|
||||||
id_urls: vec![req.id_url.clone()],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
set_id_desc(&id_desc);
|
set_id_desc(&id_desc);
|
||||||
register(sign_with_difficulty(&req, true)).await.unwrap();
|
register(sign_with_difficulty(&req, true)).await.unwrap();
|
||||||
|
|
||||||
// Registered now.
|
// Registered now.
|
||||||
get_me(Some(&CAROL_PRIV)).await.unwrap();
|
get_me(Some(&CAROL)).await.unwrap();
|
||||||
server
|
server
|
||||||
.join_room(rid, &CAROL_PRIV, MemberPermission::MAX_SELF_ADD)
|
.join_room(rid, &CAROL, MemberPermission::MAX_SELF_ADD)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -579,6 +579,13 @@ components:
|
||||||
nonce:
|
nonce:
|
||||||
type: integer
|
type: integer
|
||||||
format: uint32
|
format: uint32
|
||||||
|
timestamp:
|
||||||
|
type: integer
|
||||||
|
format: uint64
|
||||||
|
id_key:
|
||||||
|
type: string
|
||||||
|
act_key:
|
||||||
|
type: string
|
||||||
payload:
|
payload:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -597,6 +604,13 @@ components:
|
||||||
nonce:
|
nonce:
|
||||||
type: integer
|
type: integer
|
||||||
format: uint32
|
format: uint32
|
||||||
|
timestamp:
|
||||||
|
type: integer
|
||||||
|
format: uint64
|
||||||
|
id_key:
|
||||||
|
type: string
|
||||||
|
act_key:
|
||||||
|
type: string
|
||||||
payload:
|
payload:
|
||||||
oneOf:
|
oneOf:
|
||||||
|
|
||||||
|
@ -625,19 +639,6 @@ components:
|
||||||
user:
|
user:
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
|
||||||
example:
|
|
||||||
sig: 99a77e836538268839ed3419c649eefb043cb51d448f641cc2a1c523811aab4aacd09f92e7c0688ffd659bfc6acb764fea79979a491e132bf6a56dd23adc1d09
|
|
||||||
signee:
|
|
||||||
nonce: 670593955
|
|
||||||
payload:
|
|
||||||
permission: 1
|
|
||||||
room: 7ed9e067-ec37-4054-9fc2-b1bd890929bd
|
|
||||||
typ: add_member
|
|
||||||
user: 83ce46ced47ec0391c64846cbb6c507250ead4985b6a044d68751edc46015dd7
|
|
||||||
timestamp: 1724966284
|
|
||||||
user: 83ce46ced47ec0391c64846cbb6c507250ead4985b6a044d68751edc46015dd7
|
|
||||||
|
|
||||||
Signed-Chat:
|
Signed-Chat:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -649,6 +650,13 @@ components:
|
||||||
nonce:
|
nonce:
|
||||||
type: integer
|
type: integer
|
||||||
format: uint32
|
format: uint32
|
||||||
|
timestamp:
|
||||||
|
type: integer
|
||||||
|
format: uint64
|
||||||
|
id_key:
|
||||||
|
type: string
|
||||||
|
act_key:
|
||||||
|
type: string
|
||||||
payload:
|
payload:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -659,16 +667,6 @@ components:
|
||||||
type: string
|
type: string
|
||||||
rich_text:
|
rich_text:
|
||||||
$ref: '$/components/schemas/RichText'
|
$ref: '$/components/schemas/RichText'
|
||||||
example:
|
|
||||||
sig: 99a77e836538268839ed3419c649eefb043cb51d448f641cc2a1c523811aab4aacd09f92e7c0688ffd659bfc6acb764fea79979a491e132bf6a56dd23adc1d09
|
|
||||||
signee:
|
|
||||||
nonce: 670593955
|
|
||||||
payload:
|
|
||||||
typ: chat
|
|
||||||
room: 7ed9e067-ec37-4054-9fc2-b1bd890929bd
|
|
||||||
rich_text: ["before ",["bold ",{"b":true}],["italic bold ",{"b":true,"i":true}],"end"]
|
|
||||||
timestamp: 1724966284
|
|
||||||
user: 83ce46ced47ec0391c64846cbb6c507250ead4985b6a044d68751edc46015dd7
|
|
||||||
|
|
||||||
WithMsgId-Signed-Chat:
|
WithMsgId-Signed-Chat:
|
||||||
allOf:
|
allOf:
|
||||||
|
@ -690,6 +688,13 @@ components:
|
||||||
nonce:
|
nonce:
|
||||||
type: integer
|
type: integer
|
||||||
format: uint32
|
format: uint32
|
||||||
|
timestamp:
|
||||||
|
type: integer
|
||||||
|
format: uint64
|
||||||
|
id_key:
|
||||||
|
type: string
|
||||||
|
act_key:
|
||||||
|
type: string
|
||||||
payload:
|
payload:
|
||||||
oneOf:
|
oneOf:
|
||||||
- type: object
|
- type: object
|
||||||
|
@ -707,20 +712,6 @@ components:
|
||||||
peer:
|
peer:
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
example:
|
|
||||||
sig: 99a77e836538268839ed3419c649eefb043cb51d448f641cc2a1c523811aab4aacd09f92e7c0688ffd659bfc6acb764fea79979a491e132bf6a56dd23adc1d09
|
|
||||||
signee:
|
|
||||||
nonce: 670593955
|
|
||||||
payload:
|
|
||||||
typ: create_room
|
|
||||||
attrs: 1 # PUBLIC_READABLE
|
|
||||||
title: 'hello room'
|
|
||||||
members:
|
|
||||||
- user: 83ce46ced47ec0391c64846cbb6c507250ead4985b6a044d68751edc46015dd7
|
|
||||||
permission: -1
|
|
||||||
timestamp: 1724966284
|
|
||||||
user: 83ce46ced47ec0391c64846cbb6c507250ead4985b6a044d68751edc46015dd7
|
|
||||||
|
|
||||||
Signed-UserRegister:
|
Signed-UserRegister:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -732,6 +723,13 @@ components:
|
||||||
nonce:
|
nonce:
|
||||||
type: integer
|
type: integer
|
||||||
format: uint32
|
format: uint32
|
||||||
|
timestamp:
|
||||||
|
type: integer
|
||||||
|
format: uint64
|
||||||
|
id_key:
|
||||||
|
type: string
|
||||||
|
act_key:
|
||||||
|
type: string
|
||||||
payload:
|
payload:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -740,10 +738,14 @@ components:
|
||||||
const: 'user_register'
|
const: 'user_register'
|
||||||
server_url:
|
server_url:
|
||||||
type: string
|
type: string
|
||||||
description: The server URL to register on. Must matches chat server's base_url.
|
description: |
|
||||||
|
The server URL to register on. Must matches chat server's base_url.
|
||||||
|
It's path segment must be normalized, eg. always contains a `/` path for top-level.
|
||||||
id_url:
|
id_url:
|
||||||
type: string
|
type: string
|
||||||
description: The identity server URL. Must be in form `https://<domain>`.
|
description: |
|
||||||
|
The identity server URL. Must be in form `https://<domain>/`.
|
||||||
|
It's path segment must be normalized, eg. always contains a `/` path for top-level.
|
||||||
id_key:
|
id_key:
|
||||||
type: string
|
type: string
|
||||||
description: Hex encoded user primary key `id_key`.
|
description: Hex encoded user primary key `id_key`.
|
||||||
|
@ -771,6 +773,13 @@ components:
|
||||||
nonce:
|
nonce:
|
||||||
type: integer
|
type: integer
|
||||||
format: uint32
|
format: uint32
|
||||||
|
timestamp:
|
||||||
|
type: integer
|
||||||
|
format: uint64
|
||||||
|
id_key:
|
||||||
|
type: string
|
||||||
|
act_key:
|
||||||
|
type: string
|
||||||
payload:
|
payload:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -796,6 +805,13 @@ components:
|
||||||
nonce:
|
nonce:
|
||||||
type: integer
|
type: integer
|
||||||
format: uint32
|
format: uint32
|
||||||
|
timestamp:
|
||||||
|
type: integer
|
||||||
|
format: uint64
|
||||||
|
id_key:
|
||||||
|
type: string
|
||||||
|
act_key:
|
||||||
|
type: string
|
||||||
payload:
|
payload:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
Loading…
Add table
Reference in a new issue