diff --git a/Cargo.lock b/Cargo.lock index 014c5cc..0a7334c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -267,6 +267,7 @@ checksum = "0dbbb57365263e881e805dc77d94697c9118fd94d8da011240555aa7b23445bd" name = "conduit" version = "0.1.0" dependencies = [ + "base64 0.12.3", "directories", "http", "image", @@ -1559,7 +1560,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.0.1" -source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" dependencies = [ "ruma-api", "ruma-client-api", @@ -1573,7 +1574,7 @@ dependencies = [ [[package]] name = "ruma-api" version = "0.17.0-alpha.1" -source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" dependencies = [ "http", "percent-encoding", @@ -1588,7 +1589,7 @@ dependencies = [ [[package]] name = "ruma-api-macros" version = "0.17.0-alpha.1" -source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1599,7 +1600,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.10.0-alpha.1" -source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" dependencies = [ "assign", "http", @@ -1617,7 +1618,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.2.0" -source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" dependencies = [ "js_int", "ruma-identifiers", @@ -1630,7 +1631,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.22.0-alpha.1" -source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" dependencies = [ "js_int", "ruma-common", @@ -1645,7 +1646,7 @@ dependencies = [ [[package]] name = "ruma-events-macros" version = "0.22.0-alpha.1" -source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1656,7 +1657,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.0.3" -source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" dependencies = [ "js_int", "ruma-api", @@ -1671,7 +1672,7 @@ dependencies = [ [[package]] name = "ruma-identifiers" version = "0.17.4" -source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" dependencies = [ "rand", "ruma-identifiers-macros", @@ -1683,7 +1684,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-macros" version = "0.17.4" -source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" dependencies = [ "proc-macro2", "quote", @@ -1694,7 +1695,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.1.1" -source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" dependencies = [ "ruma-serde", "serde", @@ -1705,7 +1706,7 @@ dependencies = [ [[package]] name = "ruma-serde" version = "0.2.3" -source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" dependencies = [ "form_urlencoded", "itoa", @@ -1717,7 +1718,7 @@ dependencies = [ [[package]] name = "ruma-signatures" version = "0.6.0-dev.1" -source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" dependencies = [ "base64 0.12.3", "ring", diff --git a/Cargo.toml b/Cargo.toml index 90633de..4945e3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,9 +16,10 @@ edition = "2018" #rocket = { git = "https://github.com/SergioBenitez/Rocket.git", rev = "8d779caa22c63b15a6c3ceb75d8f6d4971b2eb67", features = ["tls"] } # Used to handle requests rocket = { git = "https://github.com/timokoesters/Rocket.git", branch = "empty_parameters", features = ["tls"] } -tokio = "0.2.22" # Used for long polling -ruma = { git = "https://github.com/ruma/ruma", features = ["rand", "client-api", "federation-api", "unstable-pre-spec", "unstable-synapse-quirks"], rev = "987d48666cf166cf12100b5dbc61b5e3385c4014" } # Used for matrix spec type definitions and helpers +#ruma = { git = "https://github.com/ruma/ruma", features = ["rand", "client-api", "federation-api", "unstable-pre-spec", "unstable-synapse-quirks"], rev = "987d48666cf166cf12100b5dbc61b5e3385c4014" } # Used for matrix spec type definitions and helpers +ruma = { git = "https://github.com/timokoesters/ruma", features = ["rand", "client-api", "federation-api", "unstable-pre-spec", "unstable-synapse-quirks"], branch = "timo-fixes" } # Used for matrix spec type definitions and helpers #ruma = { path = "../ruma/ruma", features = ["rand", "client-api", "federation-api", "unstable-pre-spec", "unstable-synapse-quirks"] } +tokio = "0.2.22" # Used for long polling sled = "0.32.0" # Used for storing data permanently log = "0.4.8" # Used for emitting log entries http = "0.2.1" # Used for rocket<->ruma conversions @@ -31,6 +32,7 @@ rust-argon2 = "0.8.2" # Used to hash passwords reqwest = "0.10.6" # Used to send requests thiserror = "1.0.19" # Used for conduit::Error type image = { version = "0.23.4", default-features = false, features = ["jpeg", "png", "gif"] } # Used to generate thumbnails for images +base64 = "0.12.3" # Used to encode server public key [features] default = ["conduit_bin"] diff --git a/src/client_server/account.rs b/src/client_server/account.rs index 8764446..9837d1b 100644 --- a/src/client_server/account.rs +++ b/src/client_server/account.rs @@ -15,11 +15,18 @@ use ruma::{ UserId, }; +use register::RegistrationKind; #[cfg(feature = "conduit_bin")] use rocket::{get, post}; const GUEST_NAME_LENGTH: usize = 10; +/// # `GET /_matrix/client/r0/register/available` +/// +/// Checks if a username is valid and available on this server. +/// +/// - Returns true if no user or appservice on this server claimed this username +/// - This will not reserve the username, so the username might become invalid when trying to register #[cfg_attr( feature = "conduit_bin", get("/_matrix/client/r0/register/available", data = "") @@ -53,6 +60,15 @@ pub fn get_register_available_route( Ok(get_username_availability::Response { available: true }.into()) } +/// # `POST /_matrix/client/r0/register` +/// +/// Register an account on this homeserver. +/// +/// - Returns the device id and access_token unless `inhibit_login` is true +/// - When registering a guest account, all parameters except initial_device_display_name will be +/// ignored +/// - Creates a new account and a device for it +/// - The account will be populated with default account data #[cfg_attr( feature = "conduit_bin", post("/_matrix/client/r0/register", data = "") @@ -68,12 +84,24 @@ pub fn register_route( )); } + let is_guest = matches!(body.kind, Some(RegistrationKind::Guest)); + + let mut missing_username = false; + // Validate user id let user_id = UserId::parse_with_server_name( - body.username - .clone() - .unwrap_or_else(|| utils::random_string(GUEST_NAME_LENGTH)) - .to_lowercase(), + if is_guest { + utils::random_string(GUEST_NAME_LENGTH) + } else { + body.username.clone().unwrap_or_else(|| { + // If the user didn't send a username field, that means the client is just trying + // the get an UIAA error to see available flows + missing_username = true; + // Just give the user a random name. He won't be able to register with it anyway. + utils::random_string(GUEST_NAME_LENGTH) + }) + } + .to_lowercase(), db.globals.server_name(), ) .ok() @@ -84,7 +112,7 @@ pub fn register_route( ))?; // Check if username is creative enough - if db.users.exists(&user_id)? { + if !missing_username && db.users.exists(&user_id)? { return Err(Error::BadRequest( ErrorKind::UserInUse, "Desired user ID is already taken.", @@ -116,7 +144,19 @@ pub fn register_route( return Err(Error::Uiaa(uiaainfo)); } - let password = body.password.clone().unwrap_or_default(); + if missing_username { + return Err(Error::BadRequest( + ErrorKind::MissingParam, + "Missing username field.", + )); + } + + let password = if is_guest { + None + } else { + body.password.clone() + } + .unwrap_or_default(); // Create user db.users.create(&user_id, &password)?; @@ -134,7 +174,7 @@ pub fn register_route( &db.globals, )?; - if body.inhibit_login { + if !is_guest && body.inhibit_login { return Ok(register::Response { access_token: None, user_id, @@ -144,10 +184,12 @@ pub fn register_route( } // Generate new device id if the user didn't specify one - let device_id = body - .device_id - .clone() - .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into()); + let device_id = if is_guest { + None + } else { + body.device_id.clone() + } + .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into()); // Generate new token for the device let token = utils::random_string(TOKEN_LENGTH); @@ -168,6 +210,13 @@ pub fn register_route( .into()) } +/// # `POST /_matrix/client/r0/account/password` +/// +/// Changes the password of this account. +/// +/// - Invalidates all other access tokens if logout_devices is true +/// - Deletes all other devices and most of their data (to-device events, last seen, etc.) if +/// logout_devices is true #[cfg_attr( feature = "conduit_bin", post("/_matrix/client/r0/account/password", data = "") @@ -225,6 +274,11 @@ pub fn change_password_route( Ok(change_password::Response.into()) } +/// # `GET _matrix/client/r0/account/whoami` +/// +/// Get user_id of this account. +/// +/// - Also works for Application Services #[cfg_attr( feature = "conduit_bin", get("/_matrix/client/r0/account/whoami", data = "") @@ -237,6 +291,14 @@ pub fn whoami_route(body: Ruma) -> ConduitResult", data = "") )] -pub fn get_alias_route( +pub async fn get_alias_route( db: State<'_, Database>, body: Ruma, ) -> ConduitResult { if body.room_alias.server_name() != db.globals.server_name() { - todo!("ask remote server"); + let response = server_server::send_request( + &db, + body.room_alias.server_name().to_string(), + federation::query::get_room_information::v1::Request { + room_alias: body.room_alias.to_string(), + }, + ) + .await?; + + return Ok(get_alias::Response { + room_id: response.room_id, + servers: response.servers, + } + .into()); } let room_id = db diff --git a/src/client_server/capabilities.rs b/src/client_server/capabilities.rs index afa0604..ddf90f8 100644 --- a/src/client_server/capabilities.rs +++ b/src/client_server/capabilities.rs @@ -5,6 +5,9 @@ use std::collections::BTreeMap; #[cfg(feature = "conduit_bin")] use rocket::get; +/// # `GET /_matrix/client/r0/capabilities` +/// +/// Get information on this server's supported feature set and other relevent capabilities. #[cfg_attr(feature = "conduit_bin", get("/_matrix/client/r0/capabilities"))] pub fn get_capabilities_route() -> ConduitResult { let mut available = BTreeMap::new(); diff --git a/src/client_server/directory.rs b/src/client_server/directory.rs index 279df18..26188f7 100644 --- a/src/client_server/directory.rs +++ b/src/client_server/directory.rs @@ -1,15 +1,18 @@ use super::State; -use crate::{ConduitResult, Database, Error, Result, Ruma}; +use crate::{server_server, ConduitResult, Database, Error, Result, Ruma}; use ruma::{ - api::client::{ - error::ErrorKind, - r0::{ - directory::{ - self, get_public_rooms, get_public_rooms_filtered, get_room_visibility, - set_room_visibility, + api::{ + client::{ + error::ErrorKind, + r0::{ + directory::{ + self, get_public_rooms, get_public_rooms_filtered, get_room_visibility, + set_room_visibility, + }, + room, }, - room, }, + federation, }, events::{ room::{avatar, canonical_alias, guest_access, history_visibility, name, topic}, @@ -29,6 +32,46 @@ pub async fn get_public_rooms_filtered_route( db: State<'_, Database>, body: Ruma, ) -> ConduitResult { + if let Some(other_server) = body + .server + .clone() + .filter(|server| server != &db.globals.server_name().as_str()) + { + let response = server_server::send_request( + &db, + other_server, + federation::directory::get_public_rooms::v1::Request { + limit: body.limit, + since: body.since.clone(), + room_network: federation::directory::get_public_rooms::v1::RoomNetwork::Matrix, + }, + ) + .await?; + + return Ok(get_public_rooms_filtered::Response { + chunk: response + .chunk + .into_iter() + .map(|c| { + // Convert ruma::api::federation::directory::get_public_rooms::v1::PublicRoomsChunk + // to ruma::api::client::r0::directory::PublicRoomsChunk + Ok::<_, Error>( + serde_json::from_str( + &serde_json::to_string(&c) + .expect("PublicRoomsChunk::to_string always works"), + ) + .expect("federation and client-server PublicRoomsChunk are the same type"), + ) + }) + .filter_map(|r| r.ok()) + .collect(), + prev_batch: response.prev_batch, + next_batch: response.next_batch, + total_room_count_estimate: response.total_room_count_estimate, + } + .into()); + } + let limit = body.limit.map_or(10, u64::from); let mut since = 0_u64; @@ -169,26 +212,6 @@ pub async fn get_public_rooms_filtered_route( all_rooms.sort_by(|l, r| r.num_joined_members.cmp(&l.num_joined_members)); - /* - all_rooms.extend_from_slice( - &server_server::send_request( - &db, - "privacytools.io".to_owned(), - ruma::api::federation::v1::get_public_rooms::Request { - limit: Some(20_u32.into()), - since: None, - room_network: ruma::api::federation::v1::get_public_rooms::RoomNetwork::Matrix, - }, - ) - .await - ? - .chunk - .into_iter() - .map(|c| serde_json::from_str(&serde_json::to_string(&c)?)?) - .collect::>(), - ); - */ - let total_room_count_estimate = (all_rooms.len() as u32).into(); let chunk = all_rooms diff --git a/src/client_server/membership.rs b/src/client_server/membership.rs index 0ada7c4..84c0ebd 100644 --- a/src/client_server/membership.rs +++ b/src/client_server/membership.rs @@ -1,16 +1,24 @@ use super::State; -use crate::{pdu::PduBuilder, ConduitResult, Database, Error, Ruma}; +use crate::{ + client_server, pdu::PduBuilder, server_server, utils, ConduitResult, Database, Error, Ruma, +}; use ruma::{ - api::client::{ - error::ErrorKind, - r0::membership::{ - ban_user, forget_room, get_member_events, invite_user, join_room_by_id, - join_room_by_id_or_alias, joined_members, joined_rooms, kick_user, leave_room, - unban_user, + api::{ + client::{ + error::ErrorKind, + r0::{ + alias, + membership::{ + ban_user, forget_room, get_member_events, invite_user, join_room_by_id, + join_room_by_id_or_alias, joined_members, joined_rooms, kick_user, leave_room, + unban_user, + }, + }, }, + federation, }, events::{room::member, EventType}, - Raw, RoomId, + EventId, Raw, RoomId, RoomVersionId, }; use std::{collections::BTreeMap, convert::TryFrom}; @@ -21,13 +29,81 @@ use rocket::{get, post}; feature = "conduit_bin", post("/_matrix/client/r0/rooms/<_>/join", data = "") )] -pub fn join_room_by_id_route( +pub async fn join_room_by_id_route( db: State<'_, Database>, body: Ruma, ) -> ConduitResult { let sender_id = body.sender_id.as_ref().expect("user is authenticated"); - // TODO: Ask a remote server if we don't have this room + // Ask a remote server if we don't have this room + if !db.rooms.exists(&body.room_id)? && body.room_id.server_name() != db.globals.server_name() { + let make_join_response = server_server::send_request( + &db, + body.room_id.server_name().to_string(), + federation::membership::create_join_event_template::v1::Request { + room_id: body.room_id.clone(), + user_id: sender_id.clone(), + ver: vec![RoomVersionId::Version5, RoomVersionId::Version6], + }, + ) + .await?; + + let mut join_event_stub_value = + serde_json::from_str::(make_join_response.event.json().get()) + .map_err(|_| { + Error::BadServerResponse("Invalid make_join event json received from server.") + })?; + + let join_event_stub = + join_event_stub_value + .as_object_mut() + .ok_or(Error::BadServerResponse( + "Invalid make join event object received from server.", + ))?; + + join_event_stub.insert( + "origin".to_owned(), + db.globals.server_name().to_owned().to_string().into(), + ); + join_event_stub.insert( + "origin_server_ts".to_owned(), + utils::millis_since_unix_epoch().into(), + ); + + // Generate event id + let event_id = EventId::try_from(&*format!( + "${}", + ruma::signatures::reference_hash(&join_event_stub_value) + .expect("ruma can calculate reference hashes") + )) + .expect("ruma's reference hashes are valid event ids"); + + // We don't leave the event id into the pdu because that's only allowed in v1 or v2 rooms + let join_event_stub = join_event_stub_value.as_object_mut().unwrap(); + join_event_stub.remove("event_id"); + + ruma::signatures::hash_and_sign_event( + db.globals.server_name().as_str(), + db.globals.keypair(), + &mut join_event_stub_value, + ) + .expect("event is valid, we just created it"); + + let send_join_response = server_server::send_request( + &db, + body.room_id.server_name().to_string(), + federation::membership::create_join_event::v2::Request { + room_id: body.room_id.clone(), + event_id, + pdu_stub: serde_json::from_value::>(join_event_stub_value) + .expect("Raw::from_value always works"), + }, + ) + .await?; + + dbg!(send_join_response); + todo!("Take send_join_response and 'create' the room using that data"); + } let event = member::MemberEventContent { membership: member::MembershipState::Join, @@ -61,16 +137,28 @@ pub fn join_room_by_id_route( feature = "conduit_bin", post("/_matrix/client/r0/join/<_>", data = "") )] -pub fn join_room_by_id_or_alias_route( +pub async fn join_room_by_id_or_alias_route( db: State<'_, Database>, + db2: State<'_, Database>, body: Ruma, ) -> ConduitResult { - let room_id = RoomId::try_from(body.room_id_or_alias.clone()).or_else(|alias| { - Ok::<_, Error>(db.rooms.id_from_alias(&alias)?.ok_or(Error::BadRequest( - ErrorKind::NotFound, - "Room not found (TODO: Federation).", - ))?) - })?; + let room_id = match RoomId::try_from(body.room_id_or_alias.clone()) { + Ok(room_id) => room_id, + Err(room_alias) => { + client_server::get_alias_route( + db, + Ruma { + body: alias::get_alias::IncomingRequest { room_alias }, + sender_id: body.sender_id.clone(), + device_id: body.device_id.clone(), + json_body: None, + }, + ) + .await? + .0 + .room_id + } + }; let body = Ruma { sender_id: body.sender_id.clone(), @@ -83,7 +171,7 @@ pub fn join_room_by_id_or_alias_route( }; Ok(join_room_by_id_or_alias::Response { - room_id: join_room_by_id_route(db, body)?.0.room_id, + room_id: join_room_by_id_route(db2, body).await?.0.room_id, } .into()) } diff --git a/src/client_server/mod.rs b/src/client_server/mod.rs index 7703198..e5a36f3 100644 --- a/src/client_server/mod.rs +++ b/src/client_server/mod.rs @@ -17,6 +17,7 @@ mod push; mod read_marker; mod redact; mod room; +mod search; mod session; mod state; mod sync; @@ -47,6 +48,7 @@ pub use push::*; pub use read_marker::*; pub use redact::*; pub use room::*; +pub use search::*; pub use session::*; pub use state::*; pub use sync::*; diff --git a/src/client_server/room.rs b/src/client_server/room.rs index 54e57fd..b5f1529 100644 --- a/src/client_server/room.rs +++ b/src/client_server/room.rs @@ -92,13 +92,6 @@ pub fn create_room_route( &db.account_data, )?; - // Figure out preset. We need it for power levels and preset specific events - let visibility = body.visibility.unwrap_or(room::Visibility::Private); - let preset = body.preset.unwrap_or_else(|| match visibility { - room::Visibility::Private => create_room::RoomPreset::PrivateChat, - room::Visibility::Public => create_room::RoomPreset::PublicChat, - }); - // 3. Power levels let mut users = BTreeMap::new(); users.insert(sender_id.clone(), 100.into()); @@ -142,6 +135,14 @@ pub fn create_room_route( )?; // 4. Events set by preset + + // Figure out preset. We need it for preset specific events + let visibility = body.visibility.unwrap_or(room::Visibility::Private); + let preset = body.preset.unwrap_or_else(|| match visibility { + room::Visibility::Private => create_room::RoomPreset::PrivateChat, + room::Visibility::Public => create_room::RoomPreset::PublicChat, + }); + // 4.1 Join Rules db.rooms.append_pdu( PduBuilder { diff --git a/src/client_server/search.rs b/src/client_server/search.rs new file mode 100644 index 0000000..dec1ec9 --- /dev/null +++ b/src/client_server/search.rs @@ -0,0 +1,86 @@ +use super::State; +use crate::{ConduitResult, Database, Error, Ruma}; +use js_int::uint; +use ruma::api::client::{error::ErrorKind, r0::search::search_events}; + +#[cfg(feature = "conduit_bin")] +use rocket::post; +use search_events::{ResultCategories, ResultRoomEvents, SearchResult}; +use std::collections::BTreeMap; + +#[cfg_attr( + feature = "conduit_bin", + post("/_matrix/client/r0/search", data = "") +)] +pub fn search_events_route( + db: State<'_, Database>, + body: Ruma, +) -> ConduitResult { + let sender_id = body.sender_id.as_ref().expect("user is authenticated"); + + let search_criteria = body.search_categories.room_events.as_ref().unwrap(); + let filter = search_criteria.filter.as_ref().unwrap(); + + let room_id = filter.rooms.as_ref().unwrap().first().unwrap(); + + let limit = filter.limit.map_or(10, |l| u64::from(l) as usize); + + if !db.rooms.is_joined(sender_id, &room_id)? { + return Err(Error::BadRequest( + ErrorKind::Forbidden, + "You don't have permission to view this room.", + )); + } + + let skip = match body.next_batch.as_ref().map(|s| s.parse()) { + Some(Ok(s)) => s, + Some(Err(_)) => { + return Err(Error::BadRequest( + ErrorKind::InvalidParam, + "Invalid next_batch token.", + )) + } + None => 0, // Default to the start + }; + + let search = db + .rooms + .search_pdus(&room_id, &search_criteria.search_term)?; + + let results = search + .0 + .map(|result| { + Ok::<_, Error>(SearchResult { + context: None, + rank: None, + result: db + .rooms + .get_pdu_from_id(&result)? + .map(|pdu| pdu.to_room_event()), + }) + }) + .filter_map(|r| r.ok()) + .skip(skip) + .take(limit) + .collect::>(); + + let next_batch = if results.len() < limit as usize { + None + } else { + Some((skip + limit).to_string()) + }; + + Ok(search_events::Response { + search_categories: ResultCategories { + room_events: Some(ResultRoomEvents { + count: uint!(0), // TODO + groups: BTreeMap::new(), // TODO + next_batch, + results, + state: BTreeMap::new(), // TODO + highlights: search.1, + }), + }, + } + .into()) +} diff --git a/src/client_server/session.rs b/src/client_server/session.rs index a431d23..4011058 100644 --- a/src/client_server/session.rs +++ b/src/client_server/session.rs @@ -12,14 +12,28 @@ use ruma::{ #[cfg(feature = "conduit_bin")] use rocket::{get, post}; +/// # `GET /_matrix/client/r0/login` +/// +/// Get the homeserver's supported login types. One of these should be used as the `type` field +/// when logging in. #[cfg_attr(feature = "conduit_bin", get("/_matrix/client/r0/login"))] -pub fn get_login_route() -> ConduitResult { +pub fn get_login_types_route() -> ConduitResult { Ok(get_login_types::Response { flows: vec![get_login_types::LoginType::Password], } .into()) } +/// # `POST /_matrix/client/r0/login` +/// +/// Authenticates the user and returns an access token it can use in subsequent requests. +/// +/// - The returned access token is associated with the user and device +/// - Old access tokens of that device should be invalidated +/// - If `device_id` is unknown, a new device will be created +/// +/// Note: You can use [`GET /_matrix/client/r0/login`](fn.get_supported_versions_route.html) to see +/// supported login types. #[cfg_attr( feature = "conduit_bin", post("/_matrix/client/r0/login", data = "") @@ -74,6 +88,7 @@ pub fn login_route( // Generate a new token for the device let token = utils::random_string(TOKEN_LENGTH); + // TODO: Don't always create a new device // Add device db.users.create_device( &user_id, @@ -92,6 +107,12 @@ pub fn login_route( .into()) } +/// # `POST /_matrix/client/r0/logout` +/// +/// Log out the current device. +/// +/// - Invalidates the access token +/// - Deletes the device and most of it's data (to-device events, last seen, etc.) #[cfg_attr( feature = "conduit_bin", post("/_matrix/client/r0/logout", data = "") @@ -108,6 +129,15 @@ pub fn logout_route( Ok(logout::Response.into()) } +/// # `POST /_matrix/client/r0/logout/all` +/// +/// Log out all devices of this user. +/// +/// - Invalidates all access tokens +/// - Deletes devices and most of their data (to-device events, last seen, etc.) +/// +/// Note: This is equivalent to calling [`GET /_matrix/client/r0/logout`](fn.logout_route.html) +/// from each device of this user. #[cfg_attr( feature = "conduit_bin", post("/_matrix/client/r0/logout/all", data = "") diff --git a/src/client_server/sync.rs b/src/client_server/sync.rs index 4e670ec..2307f02 100644 --- a/src/client_server/sync.rs +++ b/src/client_server/sync.rs @@ -2,17 +2,29 @@ use super::State; use crate::{ConduitResult, Database, Error, Ruma}; use ruma::{ api::client::r0::sync::sync_events, - events::{AnySyncEphemeralRoomEvent, EventType}, - Raw, + events::{room::member::MembershipState, AnySyncEphemeralRoomEvent, EventType}, + Raw, RoomId, UserId, }; #[cfg(feature = "conduit_bin")] use rocket::{get, tokio}; use std::{ collections::{hash_map, BTreeMap, HashMap, HashSet}, + convert::TryFrom, time::Duration, }; +/// # `GET /_matrix/client/r0/sync` +/// +/// Synchronize the client's state with the latest state on the server. +/// +/// - This endpoint takes a `since` parameter which should be the `next_batch` value from a +/// previous request. +/// - Calling this endpoint without a `since` parameter will return all recent events, the state +/// of all rooms and more data. This should only be called on the initial login of the device. +/// - To get incremental updates, you can call this endpoint with a `since` parameter. This will +/// return all recent events, state updates and more data that happened since the last /sync +/// request. #[cfg_attr( feature = "conduit_bin", get("/_matrix/client/r0/sync", data = "") @@ -40,7 +52,9 @@ pub async fn sync_events_route( .unwrap_or(0); let mut presence_updates = HashMap::new(); + let mut left_encrypted_users = HashSet::new(); // Users that have left any encrypted rooms the sender was in let mut device_list_updates = HashSet::new(); + let mut device_list_left = HashSet::new(); // Look for device list updates of this account device_list_updates.extend( @@ -67,46 +81,100 @@ pub async fn sync_events_route( .rev() .collect::>(); + let send_notification_counts = !timeline_pdus.is_empty(); + // They /sync response doesn't always return all messages, so we say the output is // limited unless there are events in non_timeline_pdus - //let mut limited = false; + let mut limited = false; let mut state_pdus = Vec::new(); for pdu in non_timeline_pdus { if pdu.state_key.is_some() { state_pdus.push(pdu); } + limited = true; } + let encrypted_room = db + .rooms + .room_state_get(&room_id, &EventType::RoomEncryption, "")? + .is_some(); + + // TODO: optimize this? let mut send_member_count = false; let mut joined_since_last_sync = false; - let mut send_notification_counts = false; - for pdu in db + let mut new_encrypted_room = false; + for (state_key, pdu) in db .rooms .pdus_since(&sender_id, &room_id, since)? + .filter_map(|r| r.ok()) + .filter_map(|pdu| Some((pdu.state_key.clone()?, pdu))) { - let pdu = pdu?; - send_notification_counts = true; if pdu.kind == EventType::RoomMember { send_member_count = true; - if !joined_since_last_sync && pdu.state_key == Some(sender_id.to_string()) { - let content = serde_json::from_value::< - Raw, - >(pdu.content.clone()) - .expect("Raw::from_value always works") - .deserialize() - .map_err(|_| Error::bad_database("Invalid PDU in database."))?; - if content.membership == ruma::events::room::member::MembershipState::Join { - joined_since_last_sync = true; - // Both send_member_count and joined_since_last_sync are set. There's - // nothing more to do - break; + + let content = serde_json::from_value::< + Raw, + >(pdu.content.clone()) + .expect("Raw::from_value always works") + .deserialize() + .map_err(|_| Error::bad_database("Invalid PDU in database."))?; + + if pdu.state_key == Some(sender_id.to_string()) + && content.membership == MembershipState::Join + { + joined_since_last_sync = true; + } else if encrypted_room && content.membership == MembershipState::Join { + // A new user joined an encrypted room + let user_id = UserId::try_from(state_key) + .map_err(|_| Error::bad_database("Invalid UserId in member PDU."))?; + // Add encryption update if we didn't share an encrypted room already + if !share_encrypted_room(&db, &sender_id, &user_id, &room_id) { + device_list_updates.insert(user_id); } + } else if encrypted_room && content.membership == MembershipState::Leave { + // Write down users that have left encrypted rooms we are in + left_encrypted_users.insert( + UserId::try_from(state_key) + .map_err(|_| Error::bad_database("Invalid UserId in member PDU."))?, + ); } + } else if pdu.kind == EventType::RoomEncryption { + new_encrypted_room = true; } } - let members = db.rooms.room_state_type(&room_id, &EventType::RoomMember)?; + if joined_since_last_sync && encrypted_room || new_encrypted_room { + // If the user is in a new encrypted room, give them all joined users + device_list_updates.extend( + db.rooms + .room_members(&room_id) + .filter_map(|user_id| { + Some( + UserId::try_from(user_id.ok()?.clone()) + .map_err(|_| { + Error::bad_database("Invalid member event state key in db.") + }) + .ok()?, + ) + }) + .filter(|user_id| { + // Don't send key updates from the sender to the sender + sender_id != user_id + }) + .filter(|user_id| { + // Only send keys if the sender doesn't share an encrypted room with the target already + !share_encrypted_room(&db, sender_id, user_id, &room_id) + }), + ); + } + + // Look for device list updates in this room + device_list_updates.extend( + db.users + .keys_changed(&room_id.to_string(), since, None) + .filter_map(|r| r.ok()), + ); let (joined_member_count, invited_member_count, heroes) = if send_member_count { let joined_member_count = db.rooms.room_members(&room_id).count(); @@ -133,35 +201,17 @@ pub async fn sync_events_route( .map_err(|_| Error::bad_database("Invalid member event in database."))?; if let Some(state_key) = &pdu.state_key { - let current_content = serde_json::from_value::< - Raw, - >( - members - .get(state_key) - .ok_or_else(|| { - Error::bad_database( - "A user that joined once has no member event anymore.", - ) - })? - .content - .clone(), - ) - .expect("Raw::from_value always works") - .deserialize() - .map_err(|_| { - Error::bad_database("Invalid member event in database.") + let user_id = UserId::try_from(state_key.clone()).map_err(|_| { + Error::bad_database("Invalid UserId in member PDU.") })?; // The membership was and still is invite or join if matches!( content.membership, - ruma::events::room::member::MembershipState::Join - | ruma::events::room::member::MembershipState::Invite - ) && matches!( - current_content.membership, - ruma::events::room::member::MembershipState::Join - | ruma::events::room::member::MembershipState::Invite - ) { + MembershipState::Join | MembershipState::Invite + ) && (db.rooms.is_joined(&user_id, &room_id)? + || db.rooms.is_invited(&user_id, &room_id)?) + { Ok::<_, Error>(Some(state_key.clone())) } else { Ok(None) @@ -274,7 +324,7 @@ pub async fn sync_events_route( notification_count, }, timeline: sync_events::Timeline { - limited: joined_since_last_sync, + limited: limited || joined_since_last_sync, prev_batch, events: room_events, }, @@ -297,13 +347,6 @@ pub async fn sync_events_route( joined_rooms.insert(room_id.clone(), joined_room); } - // Look for device list updates in this room - device_list_updates.extend( - db.users - .keys_changed(&room_id.to_string(), since, None) - .filter_map(|r| r.ok()), - ); - // Take presence updates from this room for (user_id, presence) in db.rooms @@ -348,31 +391,6 @@ pub async fn sync_events_route( .map(|pdu| pdu.to_sync_room_event()) .collect(); - // TODO: Only until leave point - let mut edus = db - .rooms - .edus - .roomlatests_since(&room_id, since)? - .filter_map(|r| r.ok()) // Filter out buggy events - .collect::>(); - - if db - .rooms - .edus - .last_roomactive_update(&room_id, &db.globals)? - > since - { - edus.push( - serde_json::from_str( - &serde_json::to_string(&AnySyncEphemeralRoomEvent::Typing( - db.rooms.edus.roomactives_all(&room_id)?, - )) - .expect("event is valid, we just created it"), - ) - .expect("event is valid, we just created it"), - ); - } - let left_room = sync_events::LeftRoom { account_data: sync_events::AccountData { events: Vec::new() }, timeline: sync_events::Timeline { @@ -383,6 +401,49 @@ pub async fn sync_events_route( state: sync_events::State { events: Vec::new() }, }; + let mut left_since_last_sync = false; + for pdu in db.rooms.pdus_since(&sender_id, &room_id, since)? { + let pdu = pdu?; + if pdu.kind == EventType::RoomMember && pdu.state_key == Some(sender_id.to_string()) { + let content = serde_json::from_value::< + Raw, + >(pdu.content.clone()) + .expect("Raw::from_value always works") + .deserialize() + .map_err(|_| Error::bad_database("Invalid PDU in database."))?; + + if content.membership == MembershipState::Leave { + left_since_last_sync = true; + break; + } + } + } + + if left_since_last_sync { + device_list_left.extend( + db.rooms + .room_members(&room_id) + .filter_map(|user_id| { + Some( + UserId::try_from(user_id.ok()?.clone()) + .map_err(|_| { + Error::bad_database("Invalid member event state key in db.") + }) + .ok()?, + ) + }) + .filter(|user_id| { + // Don't send key updates from the sender to the sender + sender_id != user_id + }) + .filter(|user_id| { + // Only send if the sender doesn't share any encrypted room with the target + // anymore + !share_encrypted_room(&db, sender_id, user_id, &room_id) + }), + ); + } + if !left_room.is_empty() { left_rooms.insert(room_id.clone(), left_room); } @@ -392,23 +453,19 @@ pub async fn sync_events_route( for room_id in db.rooms.rooms_invited(&sender_id) { let room_id = room_id?; let mut invited_since_last_sync = false; - for pdu in db - .rooms - .pdus_since(&sender_id, &room_id, since)? - { + for pdu in db.rooms.pdus_since(&sender_id, &room_id, since)? { let pdu = pdu?; - if pdu.kind == EventType::RoomMember { - if pdu.state_key == Some(sender_id.to_string()) { - let content = serde_json::from_value::< - Raw, - >(pdu.content.clone()) - .expect("Raw::from_value always works") - .deserialize() - .map_err(|_| Error::bad_database("Invalid PDU in database."))?; - if content.membership == ruma::events::room::member::MembershipState::Invite { - invited_since_last_sync = true; - break; - } + if pdu.kind == EventType::RoomMember && pdu.state_key == Some(sender_id.to_string()) { + let content = serde_json::from_value::< + Raw, + >(pdu.content.clone()) + .expect("Raw::from_value always works") + .deserialize() + .map_err(|_| Error::bad_database("Invalid PDU in database."))?; + + if content.membership == MembershipState::Invite { + invited_since_last_sync = true; + break; } } } @@ -433,6 +490,27 @@ pub async fn sync_events_route( } } + for user_id in left_encrypted_users { + // If the user doesn't share an encrypted room with the target anymore, we need to tell + // them + if db + .rooms + .get_shared_rooms(vec![sender_id.clone(), user_id.clone()]) + .filter_map(|r| r.ok()) + .filter_map(|other_room_id| { + Some( + db.rooms + .room_state_get(&other_room_id, &EventType::RoomEncryption, "") + .ok()? + .is_some(), + ) + }) + .all(|encrypted| !encrypted) + { + device_list_left.insert(user_id); + } + } + // Remove all to-device events the device received *last time* db.users .remove_to_device_events(sender_id, device_id, since)?; @@ -464,7 +542,7 @@ pub async fn sync_events_route( }, device_lists: sync_events::DeviceLists { changed: device_list_updates.into_iter().collect(), - left: Vec::new(), // TODO + left: device_list_left.into_iter().collect(), }, device_one_time_keys_count: if db.users.last_one_time_keys_update(sender_id)? > since { db.users.count_one_time_keys(sender_id, device_id)? @@ -500,3 +578,24 @@ pub async fn sync_events_route( Ok(response.into()) } + +fn share_encrypted_room( + db: &Database, + sender_id: &UserId, + user_id: &UserId, + ignore_room: &RoomId, +) -> bool { + db.rooms + .get_shared_rooms(vec![sender_id.clone(), user_id.clone()]) + .filter_map(|r| r.ok()) + .filter(|room_id| room_id != ignore_room) + .filter_map(|other_room_id| { + Some( + db.rooms + .room_state_get(&other_room_id, &EventType::RoomEncryption, "") + .ok()? + .is_some(), + ) + }) + .any(|encrypted| encrypted) +} diff --git a/src/client_server/unversioned.rs b/src/client_server/unversioned.rs index e71c194..3ff8bec 100644 --- a/src/client_server/unversioned.rs +++ b/src/client_server/unversioned.rs @@ -5,6 +5,16 @@ use std::collections::BTreeMap; #[cfg(feature = "conduit_bin")] use rocket::get; +/// # `GET /_matrix/client/versions` +/// +/// Get the versions of the specification and unstable features supported by this server. +/// +/// - Versions take the form MAJOR.MINOR.PATCH +/// - Only the latest PATCH release will be reported for each MAJOR.MINOR value +/// - Unstable features should be namespaced and may include version information in their name +/// +/// Note: Unstable features are used while developing new features. Clients should avoid using +/// unstable features in their stable releases #[cfg_attr(feature = "conduit_bin", get("/_matrix/client/versions"))] pub fn get_supported_versions_route() -> ConduitResult { let mut unstable_features = BTreeMap::new(); diff --git a/src/database.rs b/src/database.rs index 844a1f4..7bbb6dd 100644 --- a/src/database.rs +++ b/src/database.rs @@ -104,6 +104,8 @@ impl Database { aliasid_alias: db.open_tree("alias_roomid")?, publicroomids: db.open_tree("publicroomids")?, + tokenids: db.open_tree("tokenids")?, + userroomid_joined: db.open_tree("userroomid_joined")?, roomuserid_joined: db.open_tree("roomuserid_joined")?, userroomid_invited: db.open_tree("userroomid_invited")?, @@ -127,7 +129,6 @@ impl Database { pub async fn watch(&self, user_id: &UserId, device_id: &DeviceId) { let userid_bytes = user_id.to_string().as_bytes().to_vec(); - let mut userid_prefix = userid_bytes.clone(); userid_prefix.push(0xff); @@ -151,7 +152,8 @@ impl Database { // Events for rooms we are in for room_id in self.rooms.rooms_joined(user_id).filter_map(|r| r.ok()) { - let mut roomid_prefix = room_id.to_string().as_bytes().to_vec(); + let roomid_bytes = room_id.to_string().as_bytes().to_vec(); + let mut roomid_prefix = roomid_bytes.clone(); roomid_prefix.push(0xff); // PDUs @@ -162,7 +164,7 @@ impl Database { self.rooms .edus .roomid_lastroomactiveupdate - .watch_prefix(&roomid_prefix), + .watch_prefix(&roomid_bytes), ); futures.push( diff --git a/src/database/rooms.rs b/src/database/rooms.rs index fe63318..d2cd5e9 100644 --- a/src/database/rooms.rs +++ b/src/database/rooms.rs @@ -35,6 +35,8 @@ pub struct Rooms { pub(super) aliasid_alias: sled::Tree, // AliasId = RoomId + Count pub(super) publicroomids: sled::Tree, + pub(super) tokenids: sled::Tree, // TokenId = RoomId + Token + PduId + pub(super) userroomid_joined: sled::Tree, pub(super) roomuserid_joined: sled::Tree, pub(super) userroomid_invited: sled::Tree, @@ -562,7 +564,7 @@ impl Rooms { self.pduid_pdu.insert(&pdu_id, &*pdu_json.to_string())?; self.eventid_pduid - .insert(pdu.event_id.to_string(), pdu_id)?; + .insert(pdu.event_id.to_string(), pdu_id.clone())?; if let Some(state_key) = pdu.state_key { let mut key = room_id.to_string().as_bytes().to_vec(); @@ -616,6 +618,21 @@ impl Rooms { )?; } } + EventType::RoomMessage => { + if let Some(body) = content.get("body").and_then(|b| b.as_str()) { + for word in body + .split_terminator(|c: char| !c.is_alphanumeric()) + .map(str::to_lowercase) + { + let mut key = room_id.to_string().as_bytes().to_vec(); + key.push(0xff); + key.extend_from_slice(word.as_bytes()); + key.push(0xff); + key.extend_from_slice(&pdu_id); + self.tokenids.insert(key, &[])?; + } + } + } _ => {} } self.edus.room_read_set(&room_id, &sender, index)?; @@ -928,6 +945,95 @@ impl Rooms { }) } + pub fn search_pdus<'a>( + &'a self, + room_id: &RoomId, + search_string: &str, + ) -> Result<(impl Iterator + 'a, Vec)> { + let mut prefix = room_id.to_string().as_bytes().to_vec(); + prefix.push(0xff); + + let words = search_string + .split_terminator(|c: char| !c.is_alphanumeric()) + .map(str::to_lowercase) + .collect::>(); + + let iterators = words.clone().into_iter().map(move |word| { + let mut prefix2 = prefix.clone(); + prefix2.extend_from_slice(word.as_bytes()); + prefix2.push(0xff); + self.tokenids + .scan_prefix(&prefix2) + .keys() + .rev() // Newest pdus first + .filter_map(|r| r.ok()) + .map(|key| { + let pduid_index = key + .iter() + .enumerate() + .filter(|(_, &b)| b == 0xff) + .nth(1) + .ok_or_else(|| Error::bad_database("Invalid tokenid in db."))? + .0 + + 1; // +1 because the pdu id starts AFTER the separator + + let pdu_id = key.subslice(pduid_index, key.len() - pduid_index); + + Ok::<_, Error>(pdu_id) + }) + .filter_map(|r| r.ok()) + }); + + Ok(( + utils::common_elements(iterators, |a, b| { + // We compare b with a because we reversed the iterator earlier + b.cmp(a) + }) + .unwrap(), + words, + )) + } + + pub fn get_shared_rooms<'a>( + &'a self, + users: Vec, + ) -> impl Iterator> + 'a { + let iterators = users.into_iter().map(move |user_id| { + let mut prefix = user_id.as_bytes().to_vec(); + prefix.push(0xff); + + self.userroomid_joined + .scan_prefix(&prefix) + .keys() + .filter_map(|r| r.ok()) + .map(|key| { + let roomid_index = key + .iter() + .enumerate() + .filter(|(_, &b)| b == 0xff) + .nth(0) + .ok_or_else(|| Error::bad_database("Invalid userroomid_joined in db."))? + .0 + + 1; // +1 because the room id starts AFTER the separator + + let room_id = key.subslice(roomid_index, key.len() - roomid_index); + + Ok::<_, Error>(room_id) + }) + .filter_map(|r| r.ok()) + }); + + // We use the default compare function because keys are sorted correctly (not reversed) + utils::common_elements(iterators, Ord::cmp) + .expect("users is not empty") + .map(|bytes| { + RoomId::try_from(utils::string_from_bytes(&*bytes).map_err(|_| { + Error::bad_database("Invalid RoomId bytes in userroomid_joined") + })?) + .map_err(|_| Error::bad_database("Invalid RoomId in userroomid_joined.")) + }) + } + /// Returns an iterator over all joined members of a room. pub fn room_members(&self, room_id: &RoomId) -> impl Iterator> { self.roomuserid_joined diff --git a/src/database/users.rs b/src/database/users.rs index 594cc2d..1b6a681 100644 --- a/src/database/users.rs +++ b/src/database/users.rs @@ -408,19 +408,7 @@ impl Users { &*serde_json::to_string(&device_keys).expect("DeviceKeys::to_string always works"), )?; - let count = globals.next_count()?.to_be_bytes(); - for room_id in rooms.rooms_joined(&user_id) { - let mut key = room_id?.to_string().as_bytes().to_vec(); - key.push(0xff); - key.extend_from_slice(&count); - - self.keychangeid_userid.insert(key, &*user_id.to_string())?; - } - - let mut key = user_id.to_string().as_bytes().to_vec(); - key.push(0xff); - key.extend_from_slice(&count); - self.keychangeid_userid.insert(key, &*user_id.to_string())?; + self.mark_device_key_update(user_id, rooms, globals)?; Ok(()) } @@ -520,19 +508,7 @@ impl Users { .insert(&*user_id.to_string(), user_signing_key_key)?; } - let count = globals.next_count()?.to_be_bytes(); - for room_id in rooms.rooms_joined(&user_id) { - let mut key = room_id?.to_string().as_bytes().to_vec(); - key.push(0xff); - key.extend_from_slice(&count); - - self.keychangeid_userid.insert(key, &*user_id.to_string())?; - } - - let mut key = user_id.to_string().as_bytes().to_vec(); - key.push(0xff); - key.extend_from_slice(&count); - self.keychangeid_userid.insert(key, &*user_id.to_string())?; + self.mark_device_key_update(user_id, rooms, globals)?; Ok(()) } @@ -576,21 +552,7 @@ impl Users { )?; // TODO: Should we notify about this change? - let count = globals.next_count()?.to_be_bytes(); - for room_id in rooms.rooms_joined(&target_id) { - let mut key = room_id?.to_string().as_bytes().to_vec(); - key.push(0xff); - key.extend_from_slice(&count); - - self.keychangeid_userid - .insert(key, &*target_id.to_string())?; - } - - let mut key = target_id.to_string().as_bytes().to_vec(); - key.push(0xff); - key.extend_from_slice(&count); - self.keychangeid_userid - .insert(key, &*target_id.to_string())?; + self.mark_device_key_update(target_id, rooms, globals)?; Ok(()) } @@ -628,6 +590,37 @@ impl Users { }) } + fn mark_device_key_update( + &self, + user_id: &UserId, + rooms: &super::rooms::Rooms, + globals: &super::globals::Globals, + ) -> Result<()> { + let count = globals.next_count()?.to_be_bytes(); + for room_id in rooms.rooms_joined(&user_id).filter_map(|r| r.ok()) { + // Don't send key updates to unencrypted rooms + if rooms + .room_state_get(&room_id, &EventType::RoomEncryption, "")? + .is_none() + { + return Ok(()); + } + + let mut key = room_id.to_string().as_bytes().to_vec(); + key.push(0xff); + key.extend_from_slice(&count); + + self.keychangeid_userid.insert(key, &*user_id.to_string())?; + } + + let mut key = user_id.to_string().as_bytes().to_vec(); + key.push(0xff); + key.extend_from_slice(&count); + self.keychangeid_userid.insert(key, &*user_id.to_string())?; + + Ok(()) + } + pub fn get_device_keys( &self, user_id: &UserId, @@ -859,7 +852,9 @@ impl Users { self.remove_device(&user_id, &device_id?)?; } - // Set the password to "" to indicate a deactivated account + // Set the password to "" to indicate a deactivated account. Hashes will never result in an + // empty string, so the user will not be able to log in again. Systems like changing the + // password without logging in should check if the account is deactivated. self.userid_password.insert(user_id.to_string(), "")?; // TODO: Unhook 3PID diff --git a/src/error.rs b/src/error.rs index af5405c..623aa0e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -27,6 +27,13 @@ pub enum Error { #[from] source: image::error::ImageError, }, + #[error("Could not connect to server.")] + ReqwestError { + #[from] + source: reqwest::Error, + }, + #[error("{0}")] + BadServerResponse(&'static str), #[error("{0}")] BadConfig(&'static str), #[error("{0}")] diff --git a/src/lib.rs b/src/lib.rs index 96236bf..eea32c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ mod error; mod pdu; mod push_rules; mod ruma_wrapper; +pub mod server_server; mod utils; pub use database::Database; diff --git a/src/main.rs b/src/main.rs index f91a10f..bbe7c96 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,13 @@ #![warn(rust_2018_idioms)] -pub mod push_rules; +pub mod client_server; +pub mod server_server; -mod client_server; mod database; mod error; mod pdu; +mod push_rules; mod ruma_wrapper; -//mod server_server; mod utils; pub use database::Database; @@ -26,7 +26,7 @@ fn setup_rocket() -> rocket::Rocket { client_server::get_supported_versions_route, client_server::get_register_available_route, client_server::register_route, - client_server::get_login_route, + client_server::get_login_types_route, client_server::login_route, client_server::whoami_route, client_server::logout_route, @@ -90,6 +90,7 @@ fn setup_rocket() -> rocket::Rocket { client_server::sync_events_route, client_server::get_context_route, client_server::get_message_events_route, + client_server::search_events_route, client_server::turn_server_route, client_server::send_event_to_device_route, client_server::get_media_config_route, @@ -110,10 +111,12 @@ fn setup_rocket() -> rocket::Rocket { client_server::get_key_changes_route, client_server::get_pushers_route, client_server::set_pushers_route, - //server_server::well_known_server, - //server_server::get_server_version, - //server_server::get_server_keys, - //server_server::get_server_keys_deprecated, + server_server::well_known_server, + server_server::get_server_version, + server_server::get_server_keys, + server_server::get_server_keys_deprecated, + server_server::get_public_rooms_route, + server_server::send_transaction_message_route, ], ) .attach(AdHoc::on_attach("Config", |mut rocket| async { diff --git a/src/pdu.rs b/src/pdu.rs index b689a3e..9936802 100644 --- a/src/pdu.rs +++ b/src/pdu.rs @@ -37,11 +37,12 @@ pub struct PduEvent { impl PduEvent { pub fn redact(&mut self) -> Result<()> { self.unsigned.clear(); - let allowed = match self.kind { - EventType::RoomMember => vec!["membership"], - EventType::RoomCreate => vec!["creator"], - EventType::RoomJoinRules => vec!["join_rule"], - EventType::RoomPowerLevels => vec![ + + let allowed: &[&str] = match self.kind { + EventType::RoomMember => &["membership"], + EventType::RoomCreate => &["creator"], + EventType::RoomJoinRules => &["join_rule"], + EventType::RoomPowerLevels => &[ "ban", "events", "events_default", @@ -51,8 +52,8 @@ impl PduEvent { "users", "users_default", ], - EventType::RoomHistoryVisibility => vec!["history_visibility"], - _ => vec![], + EventType::RoomHistoryVisibility => &["history_visibility"], + _ => &[], }; let old_content = self @@ -63,8 +64,8 @@ impl PduEvent { let mut new_content = serde_json::Map::new(); for key in allowed { - if let Some(value) = old_content.remove(key) { - new_content.insert(key.to_owned(), value); + if let Some(value) = old_content.remove(*key) { + new_content.insert((*key).to_owned(), value); } } diff --git a/src/server_server.rs b/src/server_server.rs index a214143..f48f502 100644 --- a/src/server_server.rs +++ b/src/server_server.rs @@ -1,16 +1,19 @@ -use crate::{Database, MatrixResult}; +use crate::{client_server, ConduitResult, Database, Error, Result, Ruma}; use http::header::{HeaderValue, AUTHORIZATION}; -use log::error; -use rocket::{get, response::content::Json, State}; -use ruma::api::Endpoint; -use ruma::api::client::error::Error; -use ruma::api::federation::discovery::{ - get_server_keys::v2 as get_server_keys, get_server_version::v1 as get_server_version, +use rocket::{get, post, put, response::content::Json, State}; +use ruma::api::federation::{ + directory::get_public_rooms, + discovery::{ + get_server_keys, get_server_version::v1 as get_server_version, ServerKey, VerifyKey, + }, + transactions::send_transaction_message, }; +use ruma::api::{client, OutgoingRequest}; use serde_json::json; use std::{ collections::BTreeMap, convert::TryFrom, + fmt::Debug, time::{Duration, SystemTime}, }; @@ -33,36 +36,51 @@ pub async fn request_well_known(db: &crate::Database, destination: &str) -> Opti Some(body.get("m.server")?.as_str()?.to_owned()) } -pub async fn send_request( +pub async fn send_request( db: &crate::Database, destination: String, request: T, -) -> Option { - let mut http_request: http::Request<_> = request.try_into().unwrap(); - +) -> Result +where + T: Debug, +{ let actual_destination = "https://".to_owned() + &request_well_known(db, &destination) .await .unwrap_or(destination.clone() + ":8448"); - *http_request.uri_mut() = (actual_destination + T::METADATA.path).parse().unwrap(); + + let mut http_request = request + .try_into_http_request(&actual_destination, Some("")) + .unwrap(); let mut request_map = serde_json::Map::new(); if !http_request.body().is_empty() { request_map.insert( "content".to_owned(), - serde_json::to_value(http_request.body()).unwrap(), + serde_json::from_slice(http_request.body()).unwrap(), ); }; request_map.insert("method".to_owned(), T::METADATA.method.to_string().into()); - request_map.insert("uri".to_owned(), T::METADATA.path.into()); - request_map.insert("origin".to_owned(), db.globals.server_name().into()); + request_map.insert( + "uri".to_owned(), + http_request + .uri() + .path_and_query() + .expect("all requests have a path") + .to_string() + .into(), + ); + request_map.insert( + "origin".to_owned(), + db.globals.server_name().as_str().into(), + ); request_map.insert("destination".to_owned(), destination.into()); let mut request_json = request_map.into(); ruma::signatures::sign_json( - db.globals.server_name(), + db.globals.server_name().as_str(), db.globals.keypair(), &mut request_json, ) @@ -72,31 +90,32 @@ pub async fn send_request( .as_object() .unwrap() .values() - .next() - .unwrap() - .as_object() - .unwrap() - .iter() - .map(|(k, v)| (k, v.as_str().unwrap())); + .map(|v| { + v.as_object() + .unwrap() + .iter() + .map(|(k, v)| (k, v.as_str().unwrap())) + }); - for s in signatures { - http_request.headers_mut().insert( - AUTHORIZATION, - HeaderValue::from_str(&format!( - "X-Matrix origin={},key=\"{}\",sig=\"{}\"", - db.globals.server_name(), - s.0, - s.1 - )) - .unwrap(), - ); + for signature_server in signatures { + for s in signature_server { + http_request.headers_mut().insert( + AUTHORIZATION, + HeaderValue::from_str(&format!( + "X-Matrix origin={},key=\"{}\",sig=\"{}\"", + db.globals.server_name(), + s.0, + s.1 + )) + .unwrap(), + ); + } } - let reqwest_response = db - .globals - .reqwest_client() - .execute(http_request.into()) - .await; + let reqwest_request = reqwest::Request::try_from(http_request) + .expect("all http requests are valid reqwest requests"); + + let reqwest_response = db.globals.reqwest_client().execute(reqwest_request).await; // Because reqwest::Response -> http::Response is complicated: match reqwest_response { @@ -117,59 +136,56 @@ pub async fn send_request( .unwrap() .into_iter() .collect(); - Some( - ::try_from(http_response.body(body).unwrap()) - .ok() - .unwrap(), + Ok( + T::IncomingResponse::try_from(http_response.body(body).unwrap()) + .expect("TODO: error handle other server errors"), ) } - Err(e) => { - error!("{}", e); - None - } + Err(e) => Err(e.into()), } } -#[cfg_attr(feature = "conduit_bin",get("/.well-known/matrix/server"))] +#[cfg_attr(feature = "conduit_bin", get("/.well-known/matrix/server"))] pub fn well_known_server() -> Json { - rocket::response::content::Json( - json!({ "m.server": "matrixtesting.koesters.xyz:14004"}).to_string(), - ) + rocket::response::content::Json(json!({ "m.server": "pc.koesters.xyz:59003"}).to_string()) } -#[cfg_attr(feature = "conduit_bin",get("/_matrix/federation/v1/version"))] -pub fn get_server_version() -> MatrixResult { - MatrixResult(Ok(get_server_version::Response { +#[cfg_attr(feature = "conduit_bin", get("/_matrix/federation/v1/version"))] +pub fn get_server_version() -> ConduitResult { + Ok(get_server_version::Response { server: Some(get_server_version::Server { name: Some("Conduit".to_owned()), version: Some(env!("CARGO_PKG_VERSION").to_owned()), }), - })) + } + .into()) } -#[cfg_attr(feature = "conduit_bin",get("/_matrix/key/v2/server"))] +#[cfg_attr(feature = "conduit_bin", get("/_matrix/key/v2/server"))] pub fn get_server_keys(db: State<'_, Database>) -> Json { let mut verify_keys = BTreeMap::new(); verify_keys.insert( format!("ed25519:{}", db.globals.keypair().version()), - get_server_keys::VerifyKey { + VerifyKey { key: base64::encode_config(db.globals.keypair().public_key(), base64::STANDARD_NO_PAD), }, ); let mut response = serde_json::from_slice( - http::Response::try_from(get_server_keys::Response { - server_name: db.globals.server_name().to_owned(), - verify_keys, - old_verify_keys: BTreeMap::new(), - signatures: BTreeMap::new(), - valid_until_ts: SystemTime::now() + Duration::from_secs(60 * 2), + http::Response::try_from(get_server_keys::v2::Response { + server_key: ServerKey { + server_name: db.globals.server_name().to_owned(), + verify_keys, + old_verify_keys: BTreeMap::new(), + signatures: BTreeMap::new(), + valid_until_ts: SystemTime::now() + Duration::from_secs(60 * 2), + }, }) .unwrap() .body(), ) .unwrap(); ruma::signatures::sign_json( - db.globals.server_name(), + db.globals.server_name().as_str(), db.globals.keypair(), &mut response, ) @@ -177,7 +193,88 @@ pub fn get_server_keys(db: State<'_, Database>) -> Json { Json(response.to_string()) } -#[cfg_attr(feature = "conduit_bin",get("/_matrix/key/v2/server/<_key_id>"))] -pub fn get_server_keys_deprecated(db: State<'_, Database>, _key_id: String) -> Json { +#[cfg_attr(feature = "conduit_bin", get("/_matrix/key/v2/server/<_>"))] +pub fn get_server_keys_deprecated(db: State<'_, Database>) -> Json { get_server_keys(db) } + +#[cfg_attr( + feature = "conduit_bin", + post("/_matrix/federation/v1/publicRooms", data = "") +)] +pub async fn get_public_rooms_route( + db: State<'_, Database>, + body: Ruma, +) -> ConduitResult { + let Ruma { + body: + get_public_rooms::v1::Request { + room_network: _room_network, // TODO + limit, + since, + }, + sender_id, + device_id, + json_body, + } = body; + + let client::r0::directory::get_public_rooms_filtered::Response { + chunk, + prev_batch, + next_batch, + total_room_count_estimate, + } = client_server::get_public_rooms_filtered_route( + db, + Ruma { + body: client::r0::directory::get_public_rooms_filtered::IncomingRequest { + filter: None, + limit, + room_network: client::r0::directory::get_public_rooms_filtered::RoomNetwork::Matrix, + server: None, + since, + }, + sender_id, + device_id, + json_body, + }, + ) + .await? + .0; + + Ok(get_public_rooms::v1::Response { + chunk: chunk + .into_iter() + .map(|c| { + // Convert ruma::api::federation::directory::get_public_rooms::v1::PublicRoomsChunk + // to ruma::api::client::r0::directory::PublicRoomsChunk + Ok::<_, Error>( + serde_json::from_str( + &serde_json::to_string(&c) + .expect("PublicRoomsChunk::to_string always works"), + ) + .expect("federation and client-server PublicRoomsChunk are the same type"), + ) + }) + .filter_map(|r| r.ok()) + .collect(), + prev_batch, + next_batch, + total_room_count_estimate, + } + .into()) +} + +#[cfg_attr( + feature = "conduit_bin", + put("/_matrix/federation/v1/send/<_>", data = "") +)] +pub fn send_transaction_message_route( + db: State<'_, Database>, + body: Ruma, +) -> ConduitResult { + dbg!(&*body); + Ok(send_transaction_message::v1::Response { + pdus: BTreeMap::new(), + } + .into()) +} diff --git a/src/utils.rs b/src/utils.rs index 0ab3bfa..8cf1b2c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,9 @@ use argon2::{Config, Variant}; +use cmp::Ordering; use rand::prelude::*; +use sled::IVec; use std::{ + cmp, convert::TryInto, time::{SystemTime, UNIX_EPOCH}, }; @@ -59,3 +62,31 @@ pub fn calculate_hash(password: &str) -> Result { let salt = random_string(32); argon2::hash_encoded(password.as_bytes(), salt.as_bytes(), &hashing_config) } + +pub fn common_elements( + mut iterators: impl Iterator>, + check_order: impl Fn(&IVec, &IVec) -> Ordering, +) -> Option> { + let first_iterator = iterators.next()?; + let mut other_iterators = iterators.map(|i| i.peekable()).collect::>(); + + Some(first_iterator.filter(move |target| { + other_iterators + .iter_mut() + .map(|it| { + while let Some(element) = it.peek() { + match check_order(element, target) { + Ordering::Greater => return false, // We went too far + Ordering::Equal => return true, // Element is in both iters + Ordering::Less => { + // Keep searching + it.next(); + } + } + } + + false + }) + .all(|b| b) + })) +}