2024-02-09 23:16:06 -05:00
use std ::{ io ::Cursor , net ::IpAddr , sync ::Arc , time ::Duration } ;
2023-06-25 19:31:40 +02:00
2024-02-09 23:16:06 -05:00
use crate ::{
service ::media ::{ FileMeta , UrlPreviewData } ,
services , utils , Error , Result , Ruma ,
} ;
use image ::io ::Reader as ImgReader ;
use reqwest ::Url ;
2020-07-30 18:14:47 +02:00
use ruma ::api ::client ::{
error ::ErrorKind ,
2022-02-18 15:33:14 +01:00
media ::{
2022-01-27 00:25:20 +01:00
create_content , get_content , get_content_as_filename , get_content_thumbnail ,
2024-02-09 23:16:06 -05:00
get_media_config , get_media_preview ,
2022-01-27 00:25:20 +01:00
} ,
2020-07-30 18:14:47 +02:00
} ;
2024-02-10 12:28:49 -05:00
use tracing ::{ debug , error , info , warn } ;
2024-02-09 23:16:06 -05:00
use webpage ::HTML ;
2020-07-30 18:14:47 +02:00
2024-01-16 20:44:20 -05:00
/// generated MXC ID (`media-id`) length
2020-09-25 14:18:36 -04:00
const MXC_LENGTH : usize = 32 ;
2020-07-30 18:14:47 +02:00
2024-01-16 20:44:20 -05:00
/// # `GET /_matrix/media/v3/config`
2021-08-31 19:14:37 +02:00
///
/// Returns max upload size.
2020-10-21 21:28:02 +02:00
pub async fn get_media_config_route (
2022-02-18 15:33:14 +01:00
_body : Ruma < get_media_config ::v3 ::Request > ,
) -> Result < get_media_config ::v3 ::Response > {
Ok ( get_media_config ::v3 ::Response {
2022-09-06 23:15:09 +02:00
upload_size : services ( ) . globals . max_request_size ( ) . into ( ) ,
2022-01-22 16:58:32 +01:00
} )
2020-07-30 18:14:47 +02:00
}
2024-02-09 23:16:06 -05:00
/// # `GET /_matrix/media/v3/preview_url`
///
/// Returns URL preview.
pub async fn get_media_preview_route (
body : Ruma < get_media_preview ::v3 ::Request > ,
) -> Result < get_media_preview ::v3 ::Response > {
let url = & body . url ;
if ! url_preview_allowed ( url ) {
return Err ( Error ::BadRequest (
ErrorKind ::Forbidden ,
" URL is not allowed to be previewed " ,
) ) ;
}
if let Ok ( preview ) = get_url_preview ( url ) . await {
let res = serde_json ::value ::to_raw_value ( & preview ) . map_err ( | e | {
error! (
" Failed to convert UrlPreviewData into a serde json value: {} " ,
e
) ;
Error ::BadRequest (
ErrorKind ::Unknown ,
" Unknown error occurred parsing URL preview " ,
)
} ) ? ;
return Ok ( get_media_preview ::v3 ::Response ::from_raw_value ( res ) ) ;
}
Err ( Error ::BadRequest (
ErrorKind ::LimitExceeded {
retry_after_ms : Some ( Duration ::from_secs ( 5 ) ) ,
} ,
" Retry later " ,
) )
}
2024-01-16 20:44:20 -05:00
/// # `POST /_matrix/media/v3/upload`
2021-08-31 19:14:37 +02:00
///
/// Permanently save media in the server.
///
/// - Some metadata will be saved in the database
/// - Media will be saved in the media/ directory
2020-10-21 21:28:02 +02:00
pub async fn create_content_route (
2022-12-14 13:09:10 +01:00
body : Ruma < create_content ::v3 ::Request > ,
2022-02-18 15:33:14 +01:00
) -> Result < create_content ::v3 ::Response > {
2020-07-30 18:14:47 +02:00
let mxc = format! (
" mxc://{}/{} " ,
2022-09-06 23:15:09 +02:00
services ( ) . globals . server_name ( ) ,
2020-07-30 18:14:47 +02:00
utils ::random_string ( MXC_LENGTH )
) ;
2021-06-06 16:58:32 +04:30
2022-10-05 20:34:31 +02:00
services ( )
. media
2021-06-06 16:58:32 +04:30
. create (
mxc . clone ( ) ,
2022-10-05 20:34:31 +02:00
body . filename
2021-06-06 16:58:32 +04:30
. as_ref ( )
. map ( | filename | " inline; filename= " . to_owned ( ) + filename )
. as_deref ( ) ,
2022-10-05 15:33:57 +02:00
body . content_type . as_deref ( ) ,
2021-06-06 16:58:32 +04:30
& body . file ,
)
. await ? ;
2020-07-30 18:14:47 +02:00
2023-11-29 21:36:02 -05:00
let content_uri = mxc . into ( ) ;
2022-02-18 15:33:14 +01:00
Ok ( create_content ::v3 ::Response {
2023-11-29 21:36:02 -05:00
content_uri ,
2020-12-19 16:00:11 +01:00
blurhash : None ,
2022-01-22 16:58:32 +01:00
} )
2020-07-30 18:14:47 +02:00
}
2024-01-16 20:44:20 -05:00
/// helper method to fetch remote media from other servers over federation
2022-01-27 16:12:39 +01:00
pub async fn get_remote_content (
mxc : & str ,
server_name : & ruma ::ServerName ,
2022-12-14 13:09:10 +01:00
media_id : String ,
2024-01-21 20:30:39 -05:00
allow_redirect : bool ,
timeout_ms : Duration ,
2022-02-18 15:33:14 +01:00
) -> Result < get_content ::v3 ::Response , Error > {
2024-01-17 23:18:10 -05:00
// we'll lie to the client and say the blocked server's media was not found and log.
// the client has no way of telling anyways so this is a security bonus.
if services ( )
. globals
. prevent_media_downloads_from ( )
. contains ( & server_name . to_owned ( ) )
{
info! ( " Received request for remote media `{}` but server is in our media server blocklist. Returning 404. " , mxc ) ;
return Err ( Error ::BadRequest ( ErrorKind ::NotFound , " Media not found. " ) ) ;
}
2022-09-06 23:15:09 +02:00
let content_response = services ( )
2022-01-27 16:12:39 +01:00
. sending
. send_federation_request (
server_name ,
2022-02-18 15:33:14 +01:00
get_content ::v3 ::Request {
2024-01-21 20:30:39 -05:00
allow_remote : true ,
2022-12-14 13:09:10 +01:00
server_name : server_name . to_owned ( ) ,
2022-01-27 17:08:04 +01:00
media_id ,
2024-01-21 20:30:39 -05:00
timeout_ms ,
allow_redirect ,
2022-01-27 16:12:39 +01:00
} ,
)
. await ? ;
2022-10-05 20:34:31 +02:00
services ( )
. media
2022-01-27 16:12:39 +01:00
. create (
2022-11-21 09:51:39 +01:00
mxc . to_owned ( ) ,
2022-10-05 15:33:57 +02:00
content_response . content_disposition . as_deref ( ) ,
content_response . content_type . as_deref ( ) ,
2022-01-27 16:12:39 +01:00
& content_response . file ,
)
. await ? ;
2022-01-27 17:00:08 +01:00
Ok ( content_response )
2022-01-27 16:12:39 +01:00
}
2024-01-17 19:54:17 -05:00
/// # `GET /_matrix/media/v3/download/{serverName}/{mediaId}`
2021-08-31 19:14:37 +02:00
///
/// Load media from our server or over federation.
///
/// - Only allows federation if `allow_remote` is true
2024-01-21 20:30:39 -05:00
/// - Only redirects if `allow_redirect` is true
/// - Uses client-provided `timeout_ms` if available, else defaults to 20 seconds
2020-09-14 14:20:38 +02:00
pub async fn get_content_route (
2022-12-14 13:09:10 +01:00
body : Ruma < get_content ::v3 ::Request > ,
2022-02-18 15:33:14 +01:00
) -> Result < get_content ::v3 ::Response > {
2020-09-15 08:16:20 +02:00
let mxc = format! ( " mxc:// {} / {} " , body . server_name , body . media_id ) ;
2020-09-14 14:20:38 +02:00
2020-07-30 18:14:47 +02:00
if let Some ( FileMeta {
2021-05-30 21:55:43 +02:00
content_disposition ,
2020-07-30 18:14:47 +02:00
content_type ,
file ,
2022-09-07 13:25:51 +02:00
} ) = services ( ) . media . get ( mxc . clone ( ) ) . await ?
2020-07-30 18:14:47 +02:00
{
2022-02-18 15:33:14 +01:00
Ok ( get_content ::v3 ::Response {
2020-07-30 18:14:47 +02:00
file ,
2020-11-18 08:36:12 -05:00
content_type ,
2021-05-30 21:55:43 +02:00
content_disposition ,
2022-10-11 21:51:20 +02:00
cross_origin_resource_policy : Some ( " cross-origin " . to_owned ( ) ) ,
2022-01-22 16:58:32 +01:00
} )
2022-09-06 23:15:09 +02:00
} else if & * body . server_name ! = services ( ) . globals . server_name ( ) & & body . allow_remote {
2024-01-21 20:30:39 -05:00
let remote_content_response = get_remote_content (
& mxc ,
& body . server_name ,
body . media_id . clone ( ) ,
body . allow_redirect ,
body . timeout_ms ,
)
. await ? ;
2022-01-22 16:58:32 +01:00
Ok ( remote_content_response )
2020-07-30 18:14:47 +02:00
} else {
Err ( Error ::BadRequest ( ErrorKind ::NotFound , " Media not found. " ) )
}
}
2024-01-17 19:54:17 -05:00
/// # `GET /_matrix/media/v3/download/{serverName}/{mediaId}/{fileName}`
2022-01-27 00:25:20 +01:00
///
/// Load media from our server or over federation, permitting desired filename.
///
/// - Only allows federation if `allow_remote` is true
2024-01-21 20:30:39 -05:00
/// - Only redirects if `allow_redirect` is true
/// - Uses client-provided `timeout_ms` if available, else defaults to 20 seconds
2022-01-27 00:25:20 +01:00
pub async fn get_content_as_filename_route (
2022-12-14 13:09:10 +01:00
body : Ruma < get_content_as_filename ::v3 ::Request > ,
2022-02-18 15:33:14 +01:00
) -> Result < get_content_as_filename ::v3 ::Response > {
2022-01-27 00:25:20 +01:00
let mxc = format! ( " mxc:// {} / {} " , body . server_name , body . media_id ) ;
if let Some ( FileMeta {
2024-01-14 22:39:08 -05:00
content_type , file , ..
2022-09-07 13:25:51 +02:00
} ) = services ( ) . media . get ( mxc . clone ( ) ) . await ?
2022-01-27 00:25:20 +01:00
{
2022-02-18 15:33:14 +01:00
Ok ( get_content_as_filename ::v3 ::Response {
2022-01-27 00:25:20 +01:00
file ,
content_type ,
content_disposition : Some ( format! ( " inline; filename= {} " , body . filename ) ) ,
2022-10-11 21:51:20 +02:00
cross_origin_resource_policy : Some ( " cross-origin " . to_owned ( ) ) ,
2022-01-22 16:58:32 +01:00
} )
2022-09-06 23:15:09 +02:00
} else if & * body . server_name ! = services ( ) . globals . server_name ( ) & & body . allow_remote {
2024-01-21 20:30:39 -05:00
let remote_content_response = get_remote_content (
& mxc ,
& body . server_name ,
body . media_id . clone ( ) ,
body . allow_redirect ,
body . timeout_ms ,
)
. await ? ;
2022-01-27 16:12:39 +01:00
2022-02-18 15:33:14 +01:00
Ok ( get_content_as_filename ::v3 ::Response {
2022-01-27 16:12:39 +01:00
content_disposition : Some ( format! ( " inline: filename= {} " , body . filename ) ) ,
2022-01-27 17:00:08 +01:00
content_type : remote_content_response . content_type ,
2022-01-27 17:08:04 +01:00
file : remote_content_response . file ,
2022-10-11 21:51:20 +02:00
cross_origin_resource_policy : Some ( " cross-origin " . to_owned ( ) ) ,
2022-01-22 16:58:32 +01:00
} )
2022-01-27 00:25:20 +01:00
} else {
Err ( Error ::BadRequest ( ErrorKind ::NotFound , " Media not found. " ) )
}
}
2024-01-17 19:54:17 -05:00
/// # `GET /_matrix/media/v3/thumbnail/{serverName}/{mediaId}`
2021-08-31 19:14:37 +02:00
///
/// Load media thumbnail from our server or over federation.
///
/// - Only allows federation if `allow_remote` is true
2024-01-21 20:30:39 -05:00
/// - Only redirects if `allow_redirect` is true
/// - Uses client-provided `timeout_ms` if available, else defaults to 20 seconds
2020-09-14 14:20:38 +02:00
pub async fn get_content_thumbnail_route (
2022-12-14 13:09:10 +01:00
body : Ruma < get_content_thumbnail ::v3 ::Request > ,
2022-02-18 15:33:14 +01:00
) -> Result < get_content_thumbnail ::v3 ::Response > {
2020-09-15 08:16:20 +02:00
let mxc = format! ( " mxc:// {} / {} " , body . server_name , body . media_id ) ;
2020-07-30 18:14:47 +02:00
if let Some ( FileMeta {
content_type , file , ..
2022-09-06 23:15:09 +02:00
} ) = services ( )
2021-06-06 16:58:32 +04:30
. media
. get_thumbnail (
2022-09-07 13:25:51 +02:00
mxc . clone ( ) ,
2021-06-06 16:58:32 +04:30
body . width
. try_into ( )
. map_err ( | _ | Error ::BadRequest ( ErrorKind ::InvalidParam , " Width is invalid. " ) ) ? ,
body . height
. try_into ( )
2024-01-20 12:37:29 -05:00
. map_err ( | _ | Error ::BadRequest ( ErrorKind ::InvalidParam , " Height is invalid. " ) ) ? ,
2021-06-06 16:58:32 +04:30
)
. await ?
{
2022-10-11 21:51:20 +02:00
Ok ( get_content_thumbnail ::v3 ::Response {
file ,
content_type ,
cross_origin_resource_policy : Some ( " cross-origin " . to_owned ( ) ) ,
} )
2022-09-06 23:15:09 +02:00
} else if & * body . server_name ! = services ( ) . globals . server_name ( ) & & body . allow_remote {
2024-01-17 23:18:10 -05:00
// we'll lie to the client and say the blocked server's media was not found and log.
// the client has no way of telling anyways so this is a security bonus.
if services ( )
. globals
. prevent_media_downloads_from ( )
. contains ( & body . server_name . to_owned ( ) )
{
info! ( " Received request for remote media `{}` but server is in our media server blocklist. Returning 404. " , mxc ) ;
return Err ( Error ::BadRequest ( ErrorKind ::NotFound , " Media not found. " ) ) ;
}
2022-09-06 23:15:09 +02:00
let get_thumbnail_response = services ( )
2020-12-19 16:00:11 +01:00
. sending
. send_federation_request (
2021-01-14 14:39:56 -05:00
& body . server_name ,
2022-02-18 15:33:14 +01:00
get_content_thumbnail ::v3 ::Request {
2024-01-21 20:30:39 -05:00
allow_remote : body . allow_remote ,
2020-12-19 16:00:11 +01:00
height : body . height ,
width : body . width ,
2020-12-31 08:40:49 -05:00
method : body . method . clone ( ) ,
2022-12-14 13:09:10 +01:00
server_name : body . server_name . clone ( ) ,
media_id : body . media_id . clone ( ) ,
2024-01-21 20:30:39 -05:00
timeout_ms : body . timeout_ms ,
allow_redirect : body . allow_redirect ,
2020-12-19 16:00:11 +01:00
} ,
)
. await ? ;
2020-09-14 14:20:38 +02:00
2022-10-05 20:34:31 +02:00
services ( )
. media
2021-06-06 16:58:32 +04:30
. upload_thumbnail (
mxc ,
2022-10-05 15:33:57 +02:00
None ,
get_thumbnail_response . content_type . as_deref ( ) ,
2021-06-06 16:58:32 +04:30
body . width . try_into ( ) . expect ( " all UInts are valid u32s " ) ,
body . height . try_into ( ) . expect ( " all UInts are valid u32s " ) ,
& get_thumbnail_response . file ,
)
. await ? ;
2020-09-14 14:20:38 +02:00
2022-01-22 16:58:32 +01:00
Ok ( get_thumbnail_response )
2020-07-30 18:14:47 +02:00
} else {
Err ( Error ::BadRequest ( ErrorKind ::NotFound , " Media not found. " ) )
}
}
2024-02-09 23:16:06 -05:00
async fn download_image ( client : & reqwest ::Client , url : & str ) -> Result < UrlPreviewData > {
let image = client . get ( url ) . send ( ) . await ? . bytes ( ) . await ? ;
let mxc = format! (
" mxc://{}/{} " ,
services ( ) . globals . server_name ( ) ,
utils ::random_string ( MXC_LENGTH )
) ;
services ( )
. media
. create ( mxc . clone ( ) , None , None , & image )
. await ? ;
let ( width , height ) = match ImgReader ::new ( Cursor ::new ( & image ) ) . with_guessed_format ( ) {
Err ( _ ) = > ( None , None ) ,
Ok ( reader ) = > match reader . into_dimensions ( ) {
Err ( _ ) = > ( None , None ) ,
Ok ( ( width , height ) ) = > ( Some ( width ) , Some ( height ) ) ,
} ,
} ;
Ok ( UrlPreviewData {
image : Some ( mxc ) ,
image_size : Some ( image . len ( ) ) ,
image_width : width ,
image_height : height ,
.. Default ::default ( )
} )
}
async fn download_html ( client : & reqwest ::Client , url : & str ) -> Result < UrlPreviewData > {
let mut response = client . get ( url ) . send ( ) . await ? ;
let mut bytes : Vec < u8 > = Vec ::new ( ) ;
while let Some ( chunk ) = response . chunk ( ) . await ? {
bytes . extend_from_slice ( & chunk ) ;
2024-02-10 13:29:12 -05:00
if bytes . len ( ) > services ( ) . globals . url_preview_max_spider_size ( ) {
debug! ( " Response body from URL {} exceeds url_preview_max_spider_size ({}), not processing the rest of the response body and assuming our necessary data is in this range. " , url , services ( ) . globals . url_preview_max_spider_size ( ) ) ;
2024-02-09 23:16:06 -05:00
break ;
}
}
let body = String ::from_utf8_lossy ( & bytes ) ;
let html = match HTML ::from_string ( body . to_string ( ) , Some ( url . to_owned ( ) ) ) {
Ok ( html ) = > html ,
Err ( _ ) = > {
return Err ( Error ::BadRequest (
ErrorKind ::Unknown ,
" Failed to parse HTML " ,
) )
}
} ;
let mut data = match html . opengraph . images . first ( ) {
None = > UrlPreviewData ::default ( ) ,
Some ( obj ) = > download_image ( client , & obj . url ) . await ? ,
} ;
let props = html . opengraph . properties ;
/* use OpenGraph title / description, but fall back to HTML if not available */
data . title = props . get ( " title " ) . cloned ( ) . or ( html . title ) ;
data . description = props . get ( " description " ) . cloned ( ) . or ( html . description ) ;
Ok ( data )
}
fn url_request_allowed ( addr : & IpAddr ) -> bool {
// TODO: make this check ip_range_denylist
// could be implemented with reqwest when it supports IP filtering:
// https://github.com/seanmonstar/reqwest/issues/1515
// These checks have been taken from the Rust core/net/ipaddr.rs crate,
// IpAddr::V4.is_global() and IpAddr::V6.is_global(), as .is_global is not
// yet stabilized. TODO: Once this is stable, this match can be simplified.
match addr {
IpAddr ::V4 ( ip4 ) = > {
! ( ip4 . octets ( ) [ 0 ] = = 0 // "This network"
| | ip4 . is_private ( )
| | ( ip4 . octets ( ) [ 0 ] = = 100 & & ( ip4 . octets ( ) [ 1 ] & 0b1100_0000 = = 0b0100_0000 ) ) // is_shared()
| | ip4 . is_loopback ( )
| | ip4 . is_link_local ( )
// addresses reserved for future protocols (`192.0.0.0/24`)
| | ( ip4 . octets ( ) [ 0 ] = = 192 & & ip4 . octets ( ) [ 1 ] = = 0 & & ip4 . octets ( ) [ 2 ] = = 0 )
| | ip4 . is_documentation ( )
| | ( ip4 . octets ( ) [ 0 ] = = 198 & & ( ip4 . octets ( ) [ 1 ] & 0xfe ) = = 18 ) // is_benchmarking()
| | ( ip4 . octets ( ) [ 0 ] & 240 = = 240 & & ! ip4 . is_broadcast ( ) ) // is_reserved()
| | ip4 . is_broadcast ( ) )
}
IpAddr ::V6 ( ip6 ) = > {
! ( ip6 . is_unspecified ( )
| | ip6 . is_loopback ( )
// IPv4-mapped Address (`::ffff:0:0/96`)
| | matches! ( ip6 . segments ( ) , [ 0 , 0 , 0 , 0 , 0 , 0xffff , _ , _ ] )
// IPv4-IPv6 Translat. (`64:ff9b:1::/48`)
| | matches! ( ip6 . segments ( ) , [ 0x64 , 0xff9b , 1 , _ , _ , _ , _ , _ ] )
// Discard-Only Address Block (`100::/64`)
| | matches! ( ip6 . segments ( ) , [ 0x100 , 0 , 0 , 0 , _ , _ , _ , _ ] )
// IETF Protocol Assignments (`2001::/23`)
| | ( matches! ( ip6 . segments ( ) , [ 0x2001 , b , _ , _ , _ , _ , _ , _ ] if b < 0x200 )
& & ! (
// Port Control Protocol Anycast (`2001:1::1`)
u128 ::from_be_bytes ( ip6 . octets ( ) ) = = 0x2001_0001_0000_0000_0000_0000_0000_0001
// Traversal Using Relays around NAT Anycast (`2001:1::2`)
| | u128 ::from_be_bytes ( ip6 . octets ( ) ) = = 0x2001_0001_0000_0000_0000_0000_0000_0002
// AMT (`2001:3::/32`)
| | matches! ( ip6 . segments ( ) , [ 0x2001 , 3 , _ , _ , _ , _ , _ , _ ] )
// AS112-v6 (`2001:4:112::/48`)
| | matches! ( ip6 . segments ( ) , [ 0x2001 , 4 , 0x112 , _ , _ , _ , _ , _ ] )
// ORCHIDv2 (`2001:20::/28`)
| | matches! ( ip6 . segments ( ) , [ 0x2001 , b , _ , _ , _ , _ , _ , _ ] if ( 0x20 ..= 0x2F ) . contains ( & b ) )
) )
| | ( ( ip6 . segments ( ) [ 0 ] = = 0x2001 ) & & ( ip6 . segments ( ) [ 1 ] = = 0xdb8 ) ) // is_documentation()
| | ( ( ip6 . segments ( ) [ 0 ] & 0xfe00 ) = = 0xfc00 ) // is_unique_local()
| | ( ( ip6 . segments ( ) [ 0 ] & 0xffc0 ) = = 0xfe80 ) ) // is_unicast_link_local
}
}
}
async fn request_url_preview ( url : & str ) -> Result < UrlPreviewData > {
2024-02-10 11:22:25 -05:00
let client = services ( ) . globals . url_preview_client ( ) ;
2024-02-09 23:16:06 -05:00
let response = client . head ( url ) . send ( ) . await ? ;
if ! response
. remote_addr ( )
. map_or ( false , | a | url_request_allowed ( & a . ip ( ) ) )
{
return Err ( Error ::BadRequest (
ErrorKind ::Forbidden ,
" Requesting from this address is forbidden " ,
) ) ;
}
let content_type = match response
. headers ( )
. get ( reqwest ::header ::CONTENT_TYPE )
. and_then ( | x | x . to_str ( ) . ok ( ) )
{
Some ( ct ) = > ct ,
None = > {
return Err ( Error ::BadRequest (
ErrorKind ::Unknown ,
" Unknown Content-Type " ,
) )
}
} ;
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 ? ,
_ = > {
return Err ( Error ::BadRequest (
ErrorKind ::Unknown ,
" Unsupported Content-Type " ,
) )
}
} ;
services ( ) . media . set_url_preview ( url , & data ) . await ? ;
Ok ( data )
}
async fn get_url_preview ( url : & str ) -> Result < UrlPreviewData > {
if let Some ( preview ) = services ( ) . media . get_url_preview ( url ) . await {
return Ok ( preview ) ;
}
// ensure that only one request is made per URL
let mutex_request = Arc ::clone (
services ( )
. media
. url_preview_mutex
. write ( )
. unwrap ( )
. entry ( url . to_owned ( ) )
. or_default ( ) ,
) ;
let _request_lock = mutex_request . lock ( ) . await ;
match services ( ) . media . get_url_preview ( url ) . await {
Some ( preview ) = > Ok ( preview ) ,
None = > request_url_preview ( url ) . await ,
}
}
fn url_preview_allowed ( url_str : & str ) -> bool {
let url : Url = match Url ::parse ( url_str ) {
Ok ( u ) = > u ,
2024-02-10 12:28:49 -05:00
Err ( e ) = > {
warn! ( " Failed to parse URL from a str: {} " , e ) ;
return false ;
}
2024-02-09 23:16:06 -05:00
} ;
if [ " http " , " https " ]
. iter ( )
. all ( | & scheme | scheme ! = url . scheme ( ) . to_lowercase ( ) )
{
debug! ( " Ignoring non-HTTP/HTTPS URL to preview: {} " , url ) ;
return false ;
}
let host = match url . host_str ( ) {
None = > {
debug! (
" Ignoring URL preview for a URL that does not have a host (?): {} " ,
url
) ;
return false ;
}
Some ( h ) = > h . to_owned ( ) ,
} ;
let allowlist_domain_contains = services ( ) . globals . url_preview_domain_contains_allowlist ( ) ;
let allowlist_domain_explicit = services ( ) . globals . url_preview_domain_explicit_allowlist ( ) ;
let allowlist_url_contains = services ( ) . globals . url_preview_url_contains_allowlist ( ) ;
if allowlist_domain_contains . contains ( & " * " . to_owned ( ) )
| | allowlist_domain_explicit . contains ( & " * " . to_owned ( ) )
| | allowlist_url_contains . contains ( & " * " . to_owned ( ) )
{
debug! (
" Config key contains * which is allowing all URL previews. Allowing URL {} " ,
url
) ;
return true ;
}
if ! host . is_empty ( ) {
if allowlist_domain_explicit . contains ( & host ) {
return true ;
}
debug! (
" Host {} is allowed by url_preview_domain_explicit_allowlist (check 1/3) " ,
& host
) ;
if allowlist_domain_contains
. iter ( )
. any ( | domain_s | domain_s . contains ( & host . clone ( ) ) )
{
return true ;
}
debug! (
" Host {} is allowed by url_preview_domain_contains_allowlist (check 2/3) " ,
& host
) ;
if allowlist_url_contains
. iter ( )
2024-02-10 12:28:49 -05:00
. any ( | url_s | url . to_string ( ) . contains ( & url_s . to_string ( ) ) )
2024-02-09 23:16:06 -05:00
{
return true ;
}
debug! (
" URL {} is allowed by url_preview_url_contains_allowlist (check 3/3) " ,
& host
) ;
}
false
}