From 61fd9166f643a1775c133fe4fd05e81ebf6af461 Mon Sep 17 00:00:00 2001 From: Reiner Herrmann Date: Sat, 29 Jul 2023 00:38:58 +0200 Subject: [PATCH] Change URL preview setting from bool to a mode, and add support for an allowlist --- conduit-example.toml | 10 ++++-- debian/postinst | 10 ++++-- src/api/client_server/media.rs | 56 +++++++++++++++++++++++++--------- src/config/mod.rs | 31 +++++++++++++++++-- src/service/globals/mod.rs | 9 ++++-- 5 files changed, 90 insertions(+), 26 deletions(-) diff --git a/conduit-example.toml b/conduit-example.toml index 969c0074..260517ae 100644 --- a/conduit-example.toml +++ b/conduit-example.toml @@ -47,9 +47,6 @@ registration_token = "" allow_check_for_updates = true allow_federation = true -# Allows clients to request a URL preview -allow_url_preview = false - # Enable the display name lightning bolt on registration. enable_lightning_bolt = true @@ -69,6 +66,13 @@ trusted_servers = ["matrix.org"] address = "127.0.0.1" # This makes sure Conduit can only be reached using the reverse proxy #address = "0.0.0.0" # If Conduit is running in a container, make sure the reverse proxy (ie. Traefik) can reach it. +# possible URL preview modes: +# None: previews disabled +# All: previews for any URL allowed +# Allowlist: only domains in `url_preview_allowlist` are allowed +url_preview_mode = "None" +url_preview_allowlist = ["google.com", "youtube.com", "www.youtube.com"] + [global.well_known] # Conduit handles the /.well-known/matrix/* endpoints, making both clients and servers try to access conduit with the host # server_name and port 443 by default. diff --git a/debian/postinst b/debian/postinst index 8738ffe8..27afc03b 100644 --- a/debian/postinst +++ b/debian/postinst @@ -84,9 +84,6 @@ allow_check_for_updates = true # Enable the display name lightning bolt on registration. enable_lightning_bolt = true -# Allows clients to request a URL preview -allow_url_preview = false - # Servers listed here will be used to gather public keys of other servers. # Generally, copying this exactly should be enough. (Currently, Conduit doesn't # support batched key requests, so this list should only contain Synapse @@ -99,6 +96,13 @@ trusted_servers = ["matrix.org"] # # [0]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives #log = "..." + +# possible URL preview modes: +# None: previews disabled +# All: previews for any URL allowed +# Allowlist: only domains in \`url_preview_allowlist\` are allowed +url_preview_mode = "None" +url_preview_allowlist = ["google.com", "youtube.com", "www.youtube.com"] EOF fi ;; diff --git a/src/api/client_server/media.rs b/src/api/client_server/media.rs index 75492893..16f0d90a 100644 --- a/src/api/client_server/media.rs +++ b/src/api/client_server/media.rs @@ -11,9 +11,11 @@ use ruma::api::client::{ #[cfg(feature = "url_preview")] use { + crate::config::UrlPreviewMode, crate::service::media::UrlPreviewData, webpage::HTML, - std::{io::Cursor, net::IpAddr, sync::Arc, time::Duration}, + reqwest::Url, + std::{io::Cursor, net::IpAddr, sync::Arc}, tokio::sync::Notify, image::io::Reader as ImgReader, }; @@ -123,9 +125,9 @@ fn url_request_allowed(addr: &IpAddr) -> bool { } #[cfg(feature = "url_preview")] -async fn request_url_preview(url: String) -> Result { +async fn request_url_preview(url: &str) -> Result { let client = services().globals.default_client(); - let response = client.head(&url).send().await?; + let response = client.head(url).send().await?; if !response .remote_addr() @@ -151,8 +153,8 @@ async fn request_url_preview(url: String) -> Result { } }; let data = match content_type { - html if html.starts_with("text/html") => download_html(&client, &url).await?, - img if img.starts_with("image/") => download_image(&client, &url).await?, + html if html.starts_with("text/html") => download_html(&client, url).await?, + img if img.starts_with("image/") => download_image(&client, url).await?, _ => { return Err(Error::BadRequest( ErrorKind::Unknown, @@ -161,14 +163,14 @@ async fn request_url_preview(url: String) -> Result { } }; - services().media.set_url_preview(&url, &data).await?; + services().media.set_url_preview(url, &data).await?; Ok(data) } #[cfg(feature = "url_preview")] -async fn get_url_preview(url: String) -> Result { - if let Some(preview) = services().media.get_url_preview(&url).await { +async fn get_url_preview(url: &str) -> Result { + if let Some(preview) = services().media.get_url_preview(url).await { return Ok(preview); } @@ -177,7 +179,7 @@ async fn get_url_preview(url: String) -> Result { .url_preview_requests .read() .unwrap() - .get(&url) + .get(url) .cloned(); match notif_opt { @@ -188,15 +190,15 @@ async fn get_url_preview(url: String) -> Result { .url_preview_requests .write() .unwrap() - .insert(url.clone(), notifier.clone()); + .insert(url.to_string(), notifier.clone()); } - let data = request_url_preview(url.clone()).await; + let data = request_url_preview(url).await; notifier.notify_waiters(); { - services().media.url_preview_requests.write().unwrap().remove(&url); + services().media.url_preview_requests.write().unwrap().remove(url); } data @@ -208,7 +210,7 @@ async fn get_url_preview(url: String) -> Result { notifier.await; services().media - .get_url_preview(&url) + .get_url_preview(url) .await .ok_or(Error::BadRequest( ErrorKind::Unknown, @@ -218,6 +220,29 @@ async fn get_url_preview(url: String) -> Result { } } +#[cfg(feature = "url_preview")] +fn url_preview_allowed(url_str: &str) -> bool { + let url = match Url::parse(url_str) { + Ok(u) => u, + Err(_) => return false, + }; + if ["http", "https"].iter().all(|&scheme| scheme != url.scheme().to_lowercase()) { + return false; + } + match services().globals.url_preview_mode() { + UrlPreviewMode::All => true, + UrlPreviewMode::None => false, + UrlPreviewMode::Allowlist => { + match url.host_str() { + None => false, + Some(host) => { + services().globals.url_preview_allowlist().contains(&host.to_string()) + } + } + } + } +} + /// # `GET /_matrix/media/r0/preview_url` /// /// Returns URL preview. @@ -225,14 +250,15 @@ async fn get_url_preview(url: String) -> Result { pub async fn get_media_preview_route( body: Ruma, ) -> Result { - if !services().globals.allow_url_preview() { + let url = &body.url; + if !url_preview_allowed(url) { return Err(Error::BadRequest( ErrorKind::Unknown, "Previewing URL not allowed", )); } - if let Ok(preview) = get_url_preview(body.url.clone()).await { + if let Ok(preview) = get_url_preview(url).await { let res = serde_json::value::to_raw_value(&preview).expect("Converting to JSON failed"); return Ok(get_media_preview::v3::Response::from_raw_value(res)); } diff --git a/src/config/mod.rs b/src/config/mod.rs index d661a5de..cd19d38f 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -13,6 +13,23 @@ mod proxy; use self::proxy::ProxyConfig; +#[derive(Clone, Copy, Debug, Deserialize)] +pub enum UrlPreviewMode { + All, + None, + Allowlist, +} + +impl ToString for UrlPreviewMode { + fn to_string(&self) -> String { + match *self { + UrlPreviewMode::All => "All".to_string(), + UrlPreviewMode::None => "None".to_string(), + UrlPreviewMode::Allowlist => "Allowlist".to_string(), + } + } +} + #[derive(Clone, Debug, Deserialize)] pub struct Config { #[serde(default = "default_address")] @@ -53,8 +70,6 @@ pub struct Config { pub allow_encryption: bool, #[serde(default = "false_fn")] pub allow_federation: bool, - #[serde(default = "false_fn")] - pub allow_url_preview: bool, #[serde(default = "true_fn")] pub allow_room_creation: bool, #[serde(default = "true_fn")] @@ -87,6 +102,11 @@ pub struct Config { pub emergency_password: Option, + #[serde(default = "default_url_preview_mode")] + pub url_preview_mode: UrlPreviewMode, + #[serde(default = "Vec::new")] + pub url_preview_allowlist: Vec, + #[serde(flatten)] pub catchall: BTreeMap, } @@ -186,7 +206,6 @@ impl fmt::Display for Config { ), ("Allow encryption", &self.allow_encryption.to_string()), ("Allow federation", &self.allow_federation.to_string()), - ("Allow URL preview", &self.allow_url_preview.to_string()), ("Allow room creation", &self.allow_room_creation.to_string()), ( "JWT secret", @@ -235,6 +254,8 @@ impl fmt::Display for Config { }), ("Well-known server name", well_known_server.as_str()), ("Well-known client URL", &self.well_known_client()), + ("URL preview mode", &self.url_preview_mode.to_string()), + ("URL preview allowlist", &self.url_preview_allowlist.join(", ")), ]; let mut msg: String = "Active config values:\n\n".to_owned(); @@ -315,3 +336,7 @@ fn default_openid_token_ttl() -> u64 { pub fn default_default_room_version() -> RoomVersionId { RoomVersionId::V10 } + +pub fn default_url_preview_mode() -> UrlPreviewMode { + UrlPreviewMode::None +} diff --git a/src/service/globals/mod.rs b/src/service/globals/mod.rs index 513f2bc7..38962aae 100644 --- a/src/service/globals/mod.rs +++ b/src/service/globals/mod.rs @@ -12,6 +12,7 @@ use futures_util::FutureExt; use hickory_resolver::TokioAsyncResolver; use hyper_util::client::legacy::connect::dns::{GaiResolver, Name as HyperName}; use reqwest::dns::{Addrs, Name, Resolve, Resolving}; +use crate::config::UrlPreviewMode; use ruma::{ api::{client::sync::sync_events, federation::discovery::ServerSigningKeys}, DeviceId, RoomVersionId, ServerName, UserId, @@ -324,8 +325,12 @@ impl Service { self.config.allow_federation } - pub fn allow_url_preview(&self) -> bool { - self.config.allow_url_preview + pub fn url_preview_mode(&self) -> UrlPreviewMode { + self.config.url_preview_mode + } + + pub fn url_preview_allowlist(&self) -> &Vec { + &self.config.url_preview_allowlist } pub fn allow_room_creation(&self) -> bool {