diff --git a/DEPLOY.md b/DEPLOY.md index ec7dd461..4605a982 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -172,6 +172,7 @@ max_request_size = 20_000_000 # in bytes allow_registration = true allow_federation = true +allow_check_for_updates = true # Server to get public keys from. You probably shouldn't change this trusted_servers = ["matrix.org"] diff --git a/complement/Dockerfile b/complement/Dockerfile index 43416fa1..50173a1c 100644 --- a/complement/Dockerfile +++ b/complement/Dockerfile @@ -30,6 +30,7 @@ ENV CONDUIT_CONFIG=/workdir/conduit.toml RUN sed -i "s/port = 6167/port = 8008/g" conduit.toml RUN echo "allow_federation = true" >> conduit.toml +RUN echo "allow_check_for_updates = true" >> conduit.toml RUN echo "allow_encryption = true" >> conduit.toml RUN echo "allow_registration = true" >> conduit.toml RUN echo "log = \"warn,_=off,sled=off\"" >> conduit.toml diff --git a/conduit-example.toml b/conduit-example.toml index 6089aa5a..836db654 100644 --- a/conduit-example.toml +++ b/conduit-example.toml @@ -39,6 +39,7 @@ max_request_size = 20_000_000 # in bytes allow_registration = true allow_federation = true +allow_check_for_updates = true # Enable the display name lightning bolt on registration. enable_lightning_bolt = true diff --git a/debian/postinst b/debian/postinst index dfa599de..69a766a0 100644 --- a/debian/postinst +++ b/debian/postinst @@ -73,6 +73,7 @@ max_request_size = 20_000_000 # in bytes allow_registration = true allow_federation = true +allow_check_for_updates = true trusted_servers = ["matrix.org"] diff --git a/docker-compose.yml b/docker-compose.yml index d9c32b51..5bcf84f7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,7 @@ services: CONDUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB CONDUIT_ALLOW_REGISTRATION: 'true' CONDUIT_ALLOW_FEDERATION: 'true' + CONDUIT_ALLOW_CHECK_FOR_UPDATES: 'true' CONDUIT_TRUSTED_SERVERS: '["matrix.org"]' #CONDUIT_MAX_CONCURRENT_REQUESTS: 100 #CONDUIT_LOG: warn,rocket=off,_=off,sled=off diff --git a/docker/docker-compose.for-traefik.yml b/docker/docker-compose.for-traefik.yml index 474299f6..bed734f1 100644 --- a/docker/docker-compose.for-traefik.yml +++ b/docker/docker-compose.for-traefik.yml @@ -29,6 +29,7 @@ services: CONDUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB CONDUIT_ALLOW_REGISTRATION: 'true' CONDUIT_ALLOW_FEDERATION: 'true' + CONDUIT_ALLOW_CHECK_FOR_UPDATES: 'true' CONDUIT_TRUSTED_SERVERS: '["matrix.org"]' #CONDUIT_MAX_CONCURRENT_REQUESTS: 100 #CONDUIT_LOG: warn,rocket=off,_=off,sled=off diff --git a/docker/docker-compose.with-traefik.yml b/docker/docker-compose.with-traefik.yml index 79ebef4b..fda942bc 100644 --- a/docker/docker-compose.with-traefik.yml +++ b/docker/docker-compose.with-traefik.yml @@ -35,8 +35,9 @@ services: # Available levels are: error, warn, info, debug, trace - more info at: https://docs.rs/env_logger/*/env_logger/#enabling-logging # CONDUIT_LOG: info # default is: "warn,_=off,sled=off" # CONDUIT_ALLOW_JAEGER: 'false' - # CONDUIT_ALLOW_ENCRYPTION: 'false' - # CONDUIT_ALLOW_FEDERATION: 'false' + # CONDUIT_ALLOW_ENCRYPTION: 'true' + # CONDUIT_ALLOW_FEDERATION: 'true' + # CONDUIT_ALLOW_CHECK_FOR_UPDATES: 'true' # CONDUIT_DATABASE_PATH: /srv/conduit/.local/share/conduit # CONDUIT_WORKERS: 10 # CONDUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB diff --git a/src/config/mod.rs b/src/config/mod.rs index 4dad9f79..e2c2ff12 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -28,6 +28,8 @@ pub struct Config { pub db_cache_capacity_mb: f64, #[serde(default = "true_fn")] pub enable_lightning_bolt: bool, + #[serde(default = "true_fn")] + pub allow_check_for_updates: bool, #[serde(default = "default_conduit_cache_capacity_modifier")] pub conduit_cache_capacity_modifier: f64, #[serde(default = "default_rocksdb_max_open_files")] diff --git a/src/database/key_value/globals.rs b/src/database/key_value/globals.rs index 1e024591..11aa0641 100644 --- a/src/database/key_value/globals.rs +++ b/src/database/key_value/globals.rs @@ -12,6 +12,7 @@ use ruma::{ use crate::{database::KeyValueDatabase, service, services, utils, Error, Result}; pub const COUNTER: &[u8] = b"c"; +pub const LAST_CHECK_FOR_UPDATES_COUNT: &[u8] = b"u"; #[async_trait] impl service::globals::Data for KeyValueDatabase { @@ -27,6 +28,23 @@ impl service::globals::Data for KeyValueDatabase { }) } + fn last_check_for_updates_id(&self) -> Result { + self.global + .get(LAST_CHECK_FOR_UPDATES_COUNT)? + .map_or(Ok(0_u64), |bytes| { + utils::u64_from_bytes(&bytes).map_err(|_| { + Error::bad_database("last check for updates count has invalid bytes.") + }) + }) + } + + fn update_check_for_updates_id(&self, id: u64) -> Result<()> { + self.global + .insert(LAST_CHECK_FOR_UPDATES_COUNT, &id.to_be_bytes())?; + + Ok(()) + } + async fn watch(&self, user_id: &UserId, device_id: &DeviceId) -> Result<()> { let userid_bytes = user_id.as_bytes().to_vec(); let mut userid_prefix = userid_bytes.clone(); diff --git a/src/database/mod.rs b/src/database/mod.rs index 4e7bda6b..b36347df 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -18,6 +18,7 @@ use ruma::{ CanonicalJsonValue, EventId, OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId, }; +use serde::Deserialize; use std::{ collections::{BTreeMap, HashMap, HashSet}, fs::{self, remove_dir_all}, @@ -25,7 +26,9 @@ use std::{ mem::size_of, path::Path, sync::{Arc, Mutex, RwLock}, + time::Duration, }; +use tokio::time::interval; use tracing::{debug, error, info, warn}; @@ -982,6 +985,9 @@ impl KeyValueDatabase { services().sending.start_handler(); Self::start_cleanup_task().await; + if services().globals.allow_check_for_updates() { + Self::start_check_for_updates_task(); + } Ok(()) } @@ -998,9 +1004,61 @@ impl KeyValueDatabase { } #[tracing::instrument] - pub async fn start_cleanup_task() { - use tokio::time::interval; + pub fn start_check_for_updates_task() { + tokio::spawn(async move { + let timer_interval = Duration::from_secs(60 * 60); + let mut i = interval(timer_interval); + loop { + i.tick().await; + let _ = Self::try_handle_updates().await; + } + }); + } + async fn try_handle_updates() -> Result<()> { + let response = services() + .globals + .default_client() + .get("https://conduit.rs/check-for-updates/stable") + .send() + .await?; + + #[derive(Deserialize)] + struct CheckForUpdatesResponseEntry { + id: u64, + date: String, + message: String, + } + #[derive(Deserialize)] + struct CheckForUpdatesResponse { + updates: Vec, + } + + let response = serde_json::from_str::(&response.text().await?) + .map_err(|_| Error::BadServerResponse("Bad version check response"))?; + + let mut last_update_id = services().globals.last_check_for_updates_id()?; + for update in response.updates { + last_update_id = last_update_id.max(update.id); + if update.id > services().globals.last_check_for_updates_id()? { + println!("{}", update.message); + services() + .admin + .send_message(RoomMessageEventContent::text_plain(format!( + "@room: The following is a message from the Conduit developers. It was sent on '{}':\n\n{}", + update.date, update.message + ))) + } + } + services() + .globals + .update_check_for_updates_id(last_update_id)?; + + Ok(()) + } + + #[tracing::instrument] + pub async fn start_cleanup_task() { #[cfg(unix)] use tokio::signal::unix::{signal, SignalKind}; diff --git a/src/service/admin/mod.rs b/src/service/admin/mod.rs index 9250a3ef..6c3e3d02 100644 --- a/src/service/admin/mod.rs +++ b/src/service/admin/mod.rs @@ -1,7 +1,7 @@ use std::{ collections::BTreeMap, convert::{TryFrom, TryInto}, - sync::Arc, + sync::{Arc, RwLock}, time::Instant, }; @@ -163,6 +163,20 @@ enum AdminCommand { DisableRoom { room_id: Box }, /// Enables incoming federation handling for a room again. EnableRoom { room_id: Box }, + + /// Verify json signatures + /// [commandbody] + /// # ``` + /// # json here + /// # ``` + SignJson, + + /// Verify json signatures + /// [commandbody] + /// # ``` + /// # json here + /// # ``` + VerifyJson, } #[derive(Debug)] @@ -754,6 +768,60 @@ impl Service { ) } } + AdminCommand::SignJson => { + if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```" + { + let string = body[1..body.len() - 1].join("\n"); + match serde_json::from_str(&string) { + Ok(mut value) => { + ruma::signatures::sign_json( + services().globals.server_name().as_str(), + services().globals.keypair(), + &mut value, + ) + .expect("our request json is what ruma expects"); + let json_text = serde_json::to_string_pretty(&value) + .expect("canonical json is valid json"); + RoomMessageEventContent::text_plain(json_text) + } + Err(e) => RoomMessageEventContent::text_plain(format!("Invalid json: {e}")), + } + } else { + RoomMessageEventContent::text_plain( + "Expected code block in command body. Add --help for details.", + ) + } + } + AdminCommand::VerifyJson => { + if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```" + { + let string = body[1..body.len() - 1].join("\n"); + match serde_json::from_str(&string) { + Ok(value) => { + let pub_key_map = RwLock::new(BTreeMap::new()); + + services() + .rooms + .event_handler + .fetch_required_signing_keys(&value, &pub_key_map) + .await?; + + let pub_key_map = pub_key_map.read().unwrap(); + match ruma::signatures::verify_json(&pub_key_map, &value) { + Ok(_) => RoomMessageEventContent::text_plain("Signature correct"), + Err(e) => RoomMessageEventContent::text_plain(format!( + "Signature verification failed: {e}" + )), + } + } + Err(e) => RoomMessageEventContent::text_plain(format!("Invalid json: {e}")), + } + } else { + RoomMessageEventContent::text_plain( + "Expected code block in command body. Add --help for details.", + ) + } + } }; Ok(reply_message_content) diff --git a/src/service/globals/data.rs b/src/service/globals/data.rs index 171b3fec..8a66751b 100644 --- a/src/service/globals/data.rs +++ b/src/service/globals/data.rs @@ -13,6 +13,8 @@ use crate::Result; pub trait Data: Send + Sync { fn next_count(&self) -> Result; fn current_count(&self) -> Result; + fn last_check_for_updates_id(&self) -> Result; + fn update_check_for_updates_id(&self, id: u64) -> Result<()>; async fn watch(&self, user_id: &UserId, device_id: &DeviceId) -> Result<()>; fn cleanup(&self) -> Result<()>; fn memory_usage(&self) -> String; diff --git a/src/service/globals/mod.rs b/src/service/globals/mod.rs index 44235b31..875a4574 100644 --- a/src/service/globals/mod.rs +++ b/src/service/globals/mod.rs @@ -209,6 +209,16 @@ impl Service { self.db.current_count() } + #[tracing::instrument(skip(self))] + pub fn last_check_for_updates_id(&self) -> Result { + self.db.last_check_for_updates_id() + } + + #[tracing::instrument(skip(self))] + pub fn update_check_for_updates_id(&self, id: u64) -> Result<()> { + self.db.update_check_for_updates_id(id) + } + pub async fn watch(&self, user_id: &UserId, device_id: &DeviceId) -> Result<()> { self.db.watch(user_id, device_id).await } @@ -257,6 +267,10 @@ impl Service { self.config.enable_lightning_bolt } + pub fn allow_check_for_updates(&self) -> bool { + self.config.allow_check_for_updates + } + pub fn trusted_servers(&self) -> &[OwnedServerName] { &self.config.trusted_servers }