Compare commits

...

32 commits

Author SHA1 Message Date
Timo Kösters
3c93c81204 Merge branch 'docs' into 'next'
fix: config options for well_known have changed

Closes #474

See merge request famedly/conduit!723
2024-10-07 00:00:34 +00:00
Timo Kösters
6767ca8bc8
fix: config options for well_known have changed 2024-10-07 00:03:35 +02:00
Timo Kösters
f8d7ef04e6 Merge branch 'bump' into 'next'
Bump version

See merge request famedly/conduit!722
2024-10-06 14:17:22 +00:00
Timo Kösters
892fb8846a
Bump version 2024-10-06 14:18:54 +02:00
Timo Kösters
bca8d1f70f Merge branch 'mediafixes' into 'next'
fix: old media used spaces in content disposition without quotes

See merge request famedly/conduit!717
2024-09-25 11:53:05 +00:00
Timo Kösters
65fe6b0ab5
fix: Empty content dispositions could create problems 2024-09-25 09:06:43 +02:00
Timo Kösters
fea85b0894
fix: Migration typo for media 2024-09-24 23:07:19 +02:00
Timo Kösters
a7405cddc0
fix: Matrix media repo 2024-09-24 19:43:26 +02:00
Timo Kösters
3df21e8257
fix: old media used spaces in content disposition without quotes 2024-09-24 16:46:32 +02:00
Timo Kösters
e4d6202840 Merge branch 'config-env-var-split' into 'next'
feat(config): split on __, allowing for setting individual values in a table

See merge request famedly/conduit!706
2024-09-21 14:51:39 +00:00
Timo Kösters
c4810a3a08 Merge branch 'well-know-fix' into 'next'
Fix parsing of CONFIG_WELL_KNOW_* env variables

See merge request famedly/conduit!718
2024-09-21 14:50:25 +00:00
Timo Kösters
73d0536cd3 Merge branch 'braid/msc3916-verson-response' into 'next'
fix: add missing msc3916 unstable feature in version response

Closes #473

See merge request famedly/conduit!720
2024-09-21 14:49:16 +00:00
The one with the braid
a6797ca0a2 fix: add missing msc3916 unstable feature in version response
Fixes: #473

Signed-off-by: The one with the braid <info@braid.business>
2024-09-21 10:54:01 +02:00
Leonardo José
cdd03dfec0 Fix parsing of CONFIG_WELL_KNOW_* env variables into the internal configuration object 2024-09-12 03:33:49 +00:00
Matthias Ahouansou
2bab8869d0 Merge branch 'authenticated-media' into 'next'
feat: authenticated media

See merge request famedly/conduit!716
2024-08-28 10:59:41 +00:00
Matthias Ahouansou
cbd3b07ca7
feat(media): use authenticated endpoints when fetching remote media 2024-08-28 11:25:28 +01:00
Matthias Ahouansou
27d6d94355
feat: add support for authenticated media requests 2024-08-28 11:25:28 +01:00
Matthias Ahouansou
a3716a7d5a
chore: upgrade request client matrix version
this is needed so that new endpoints use stable paths
2024-08-28 11:25:28 +01:00
Matthias Ahouansou
a9c3867287 Merge branch 'content-disposition-type' into 'next'
chore: Use Content-Disposition type for HTTP header

Closes #463

See merge request famedly/conduit!713
2024-08-22 18:36:25 +00:00
avdb13
423b0928d5
use ruma content disposition type in place of string
Co-Authored-By: Matthias Ahouansou <matthias@ahouansou.cz>
2024-08-22 19:03:32 +01:00
Matthias Ahouansou
44dd21f432 Merge branch 'TheDidek1-next-patch-00204' into 'next'
Update docs to point at new Synapse location

See merge request famedly/conduit!715
2024-07-19 08:58:05 +00:00
Dawid Rejowski
75a0f68349 Update docs to point at new Synapse location 2024-07-18 19:58:27 +00:00
Matthias Ahouansou
8abab8c8a0 Merge branch 'Crftbt-next-patch-43247' into 'next'
Update docker.md specifying port so that others don't also run into trying to...

See merge request famedly/conduit!712
2024-07-10 08:36:34 +00:00
Craft
324e1beabf Update docker.md specifying port so that others don't also run into trying to figure this out when following this md. 2024-07-09 21:49:55 +00:00
Matthias Ahouansou
00c9ef7b56 Merge branch 'send-invalid-pdu-correction' into 'next'
fix: don't fail the entire request if any PDU's format is invalid

See merge request famedly/conduit!709
2024-07-07 21:13:38 +00:00
Matthias Ahouansou
6455e918be
fix: don't always assume ruma can generate reference hashes 2024-07-07 21:40:56 +01:00
Matthias Ahouansou
ea3e7045b4
fix: don't fail the entire transaction if any PDU's format is invalid 2024-07-07 21:40:37 +01:00
Matthias Ahouansou
b8a1b4fee5 Merge branch 'remove-tls-override-when-no-srv-response' into 'next'
fix: remove TLS name override when no SRV record is present (but properly)

See merge request famedly/conduit!708
2024-07-06 16:49:38 +00:00
Matthias Ahouansou
d95345377b
fix: remove TLS name override when no SRV record is present (but properly)
The previous attempt only did so when no IP could be resolved, which isn't enough
2024-07-06 17:31:31 +01:00
Matthias Ahouansou
75322af8c7 Merge branch 'remove-tls-override-when-no-srv-response' into 'next'
fix: remove TLS name override when no SRV record is present

See merge request famedly/conduit!707
2024-07-06 16:24:32 +00:00
Matthias Ahouansou
11187b3fad
fix: remove TLS name override when no SRV record is present
this could have been an issue in cases where there was previously a SRV record, but later got removed
2024-07-06 17:06:11 +01:00
Matthias Ahouansou
35ed731a46
feat(config): split on __, allowing for setting individual values in a table 2024-07-02 15:39:45 +01:00
19 changed files with 668 additions and 235 deletions

40
Cargo.lock generated
View file

@ -487,7 +487,7 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]] [[package]]
name = "conduit" name = "conduit"
version = "0.9.0-alpha" version = "0.10.0-alpha"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum 0.7.5", "axum 0.7.5",
@ -1203,9 +1203,9 @@ dependencies = [
[[package]] [[package]]
name = "httparse" name = "httparse"
version = "1.8.0" version = "1.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
[[package]] [[package]]
name = "httpdate" name = "httpdate"
@ -2232,7 +2232,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma" name = "ruma"
version = "0.10.1" version = "0.10.1"
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2" source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
dependencies = [ dependencies = [
"assign", "assign",
"js_int", "js_int",
@ -2253,7 +2253,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-appservice-api" name = "ruma-appservice-api"
version = "0.10.0" version = "0.10.0"
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2" source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@ -2265,7 +2265,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-client-api" name = "ruma-client-api"
version = "0.18.0" version = "0.18.0"
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2" source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"assign", "assign",
@ -2288,7 +2288,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-common" name = "ruma-common"
version = "0.13.0" version = "0.13.0"
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2" source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"base64 0.22.1", "base64 0.22.1",
@ -2318,7 +2318,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-events" name = "ruma-events"
version = "0.28.1" version = "0.28.1"
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2" source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"indexmap 2.2.6", "indexmap 2.2.6",
@ -2334,15 +2334,22 @@ dependencies = [
"thiserror", "thiserror",
"tracing", "tracing",
"url", "url",
"web-time",
"wildmatch", "wildmatch",
] ]
[[package]] [[package]]
name = "ruma-federation-api" name = "ruma-federation-api"
version = "0.9.0" version = "0.9.0"
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2" source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
dependencies = [ dependencies = [
"bytes",
"http 1.1.0",
"httparse",
"js_int", "js_int",
"memchr",
"mime",
"rand",
"ruma-common", "ruma-common",
"ruma-events", "ruma-events",
"serde", "serde",
@ -2352,7 +2359,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-identifiers-validation" name = "ruma-identifiers-validation"
version = "0.9.5" version = "0.9.5"
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2" source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
dependencies = [ dependencies = [
"js_int", "js_int",
"thiserror", "thiserror",
@ -2361,7 +2368,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-identity-service-api" name = "ruma-identity-service-api"
version = "0.9.0" version = "0.9.0"
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2" source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@ -2371,8 +2378,9 @@ dependencies = [
[[package]] [[package]]
name = "ruma-macros" name = "ruma-macros"
version = "0.13.0" version = "0.13.0"
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2" source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
dependencies = [ dependencies = [
"cfg-if",
"once_cell", "once_cell",
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
@ -2386,7 +2394,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-push-gateway-api" name = "ruma-push-gateway-api"
version = "0.9.0" version = "0.9.0"
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2" source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@ -2398,7 +2406,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-server-util" name = "ruma-server-util"
version = "0.3.0" version = "0.3.0"
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2" source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
dependencies = [ dependencies = [
"headers", "headers",
"http 1.1.0", "http 1.1.0",
@ -2411,7 +2419,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-signatures" name = "ruma-signatures"
version = "0.15.0" version = "0.15.0"
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2" source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"ed25519-dalek", "ed25519-dalek",
@ -2427,7 +2435,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-state-res" name = "ruma-state-res"
version = "0.11.0" version = "0.11.0"
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2" source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
dependencies = [ dependencies = [
"itertools", "itertools",
"js_int", "js_int",

View file

@ -16,7 +16,7 @@ license = "Apache-2.0"
name = "conduit" name = "conduit"
readme = "README.md" readme = "README.md"
repository = "https://gitlab.com/famedly/conduit" repository = "https://gitlab.com/famedly/conduit"
version = "0.9.0-alpha" version = "0.10.0-alpha"
# See also `rust-toolchain.toml` # See also `rust-toolchain.toml`
rust-version = "1.79.0" rust-version = "1.79.0"

View file

@ -6,6 +6,8 @@
> **Note:** If you update the configuration file, you must restart Conduit for the changes to take effect > **Note:** If you update the configuration file, you must restart Conduit for the changes to take effect
> **Note:** You can also configure Conduit by using `CONDUIT_{field_name}` environment variables. To set values inside a table, use `CONDUIT_{table_name}__{field_name}`. Example: `CONDUIT_SERVER_NAME="example.org"`
Conduit's configuration file is divided into the following sections: Conduit's configuration file is divided into the following sections:
- [Global](#global) - [Global](#global)
@ -56,7 +58,8 @@ The `global` section contains the following fields:
| `turn_secret` | `string` | The TURN secret | `""` | | `turn_secret` | `string` | The TURN secret | `""` |
| `turn_ttl` | `integer` | The TURN TTL in seconds | `86400` | | `turn_ttl` | `integer` | The TURN TTL in seconds | `86400` |
| `emergency_password` | `string` | Set a password to login as the `conduit` user in case of emergency | N/A | | `emergency_password` | `string` | Set a password to login as the `conduit` user in case of emergency | N/A |
| `well_known` | `table` | Used for [delegation](delegation.md) | See [delegation](delegation.md) | | `well_known_client` | `string` | Used for [delegation](delegation.md) | See [delegation](delegation.md) |
| `well_known_server` | `string` | Used for [delegation](delegation.md) | See [delegation](delegation.md) |
### TLS ### TLS

View file

@ -16,18 +16,18 @@ are connected to the server running Conduit using something like a VPN.
> **Note**: this will automatically allow you to use [sliding sync][0] without any extra configuration > **Note**: this will automatically allow you to use [sliding sync][0] without any extra configuration
To configure it, use the following options in the `global.well_known` table: To configure it, use the following options:
| Field | Type | Description | Default | | Field | Type | Description | Default |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `client` | `String` | The URL that clients should use to connect to Conduit | `https://<server_name>` | | `well_known_client` | `String` | The URL that clients should use to connect to Conduit | `https://<server_name>` |
| `server` | `String` | The hostname and port servers should use to connect to Conduit | `<server_name>:443` | | `well_known_server` | `String` | The hostname and port servers should use to connect to Conduit | `<server_name>:443` |
### Example ### Example
```toml ```toml
[global.well_known] [global]
client = "https://matrix.example.org" well_known_client = "https://matrix.example.org"
server = "matrix.example.org:443" well_known_server = "matrix.example.org:443"
``` ```
## Manual ## Manual

View file

@ -64,6 +64,7 @@ docker run -d -p 8448:6167 \
-e CONDUIT_MAX_REQUEST_SIZE="20000000" \ -e CONDUIT_MAX_REQUEST_SIZE="20000000" \
-e CONDUIT_TRUSTED_SERVERS="[\"matrix.org\"]" \ -e CONDUIT_TRUSTED_SERVERS="[\"matrix.org\"]" \
-e CONDUIT_MAX_CONCURRENT_REQUESTS="100" \ -e CONDUIT_MAX_CONCURRENT_REQUESTS="100" \
-e CONDUIT_PORT="6167" \
--name conduit <link> --name conduit <link>
``` ```

View file

@ -2,7 +2,7 @@
## General instructions ## General instructions
* It is assumed you have a [Coturn server](https://github.com/coturn/coturn) up and running. See [Synapse reference implementation](https://github.com/matrix-org/synapse/blob/develop/docs/turn-howto.md). * It is assumed you have a [Coturn server](https://github.com/coturn/coturn) up and running. See [Synapse reference implementation](https://github.com/element-hq/synapse/blob/develop/docs/turn-howto.md).
## Edit/Add a few settings to your existing conduit.toml ## Edit/Add a few settings to your existing conduit.toml

View file

@ -4,12 +4,21 @@
use std::time::Duration; use std::time::Duration;
use crate::{service::media::FileMeta, services, utils, Error, Result, Ruma}; use crate::{service::media::FileMeta, services, utils, Error, Result, Ruma};
use ruma::api::client::{ use http::header::{CONTENT_DISPOSITION, CONTENT_TYPE};
error::ErrorKind, use ruma::{
media::{ api::{
create_content, get_content, get_content_as_filename, get_content_thumbnail, client::{
get_media_config, authenticated_media::{
get_content, get_content_as_filename, get_content_thumbnail, get_media_config,
},
error::ErrorKind,
media::{self, create_content},
},
federation::authenticated_media::{self as federation_media, FileOrLocation},
}, },
http_headers::{ContentDisposition, ContentDispositionType},
media::Method,
ServerName, UInt,
}; };
const MXC_LENGTH: usize = 32; const MXC_LENGTH: usize = 32;
@ -18,9 +27,20 @@ const MXC_LENGTH: usize = 32;
/// ///
/// Returns max upload size. /// Returns max upload size.
pub async fn get_media_config_route( pub async fn get_media_config_route(
_body: Ruma<get_media_config::v3::Request>, _body: Ruma<media::get_media_config::v3::Request>,
) -> Result<get_media_config::v3::Response> { ) -> Result<media::get_media_config::v3::Response> {
Ok(get_media_config::v3::Response { Ok(media::get_media_config::v3::Response {
upload_size: services().globals.max_request_size().into(),
})
}
/// # `GET /_matrix/client/v1/media/config`
///
/// Returns max upload size.
pub async fn get_media_config_auth_route(
_body: Ruma<get_media_config::v1::Request>,
) -> Result<get_media_config::v1::Response> {
Ok(get_media_config::v1::Response {
upload_size: services().globals.max_request_size().into(), upload_size: services().globals.max_request_size().into(),
}) })
} }
@ -44,10 +64,10 @@ pub async fn create_content_route(
.media .media
.create( .create(
mxc.clone(), mxc.clone(),
body.filename Some(
.as_ref() ContentDisposition::new(ContentDispositionType::Inline)
.map(|filename| "inline; filename=".to_owned() + filename) .with_filename(body.filename.clone()),
.as_deref(), ),
body.content_type.as_deref(), body.content_type.as_deref(),
&body.file, &body.file,
) )
@ -61,28 +81,67 @@ pub async fn create_content_route(
pub async fn get_remote_content( pub async fn get_remote_content(
mxc: &str, mxc: &str,
server_name: &ruma::ServerName, server_name: &ServerName,
media_id: String, media_id: String,
) -> Result<get_content::v3::Response, Error> { ) -> Result<get_content::v1::Response, Error> {
let content_response = services() let content_response = match services()
.sending .sending
.send_federation_request( .send_federation_request(
server_name, server_name,
get_content::v3::Request { federation_media::get_content::v1::Request {
allow_remote: false, media_id: media_id.clone(),
server_name: server_name.to_owned(),
media_id,
timeout_ms: Duration::from_secs(20), timeout_ms: Duration::from_secs(20),
allow_redirect: false,
}, },
) )
.await?; .await
{
Ok(federation_media::get_content::v1::Response {
metadata: _,
content: FileOrLocation::File(content),
}) => get_content::v1::Response {
file: content.file,
content_type: content.content_type,
content_disposition: content.content_disposition,
},
Ok(federation_media::get_content::v1::Response {
metadata: _,
content: FileOrLocation::Location(url),
}) => get_location_content(url).await?,
Err(Error::BadRequest(ErrorKind::Unrecognized, _)) => {
let media::get_content::v3::Response {
file,
content_type,
content_disposition,
..
} = services()
.sending
.send_federation_request(
server_name,
media::get_content::v3::Request {
server_name: server_name.to_owned(),
media_id,
timeout_ms: Duration::from_secs(20),
allow_remote: false,
allow_redirect: true,
},
)
.await?;
get_content::v1::Response {
file,
content_type,
content_disposition,
}
}
Err(e) => return Err(e),
};
services() services()
.media .media
.create( .create(
mxc.to_owned(), mxc.to_owned(),
content_response.content_disposition.as_deref(), content_response.content_disposition.clone(),
content_response.content_type.as_deref(), content_response.content_type.as_deref(),
&content_response.file, &content_response.file,
) )
@ -97,31 +156,57 @@ pub async fn get_remote_content(
/// ///
/// - Only allows federation if `allow_remote` is true /// - Only allows federation if `allow_remote` is true
pub async fn get_content_route( pub async fn get_content_route(
body: Ruma<get_content::v3::Request>, body: Ruma<media::get_content::v3::Request>,
) -> Result<get_content::v3::Response> { ) -> Result<media::get_content::v3::Response> {
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id); let get_content::v1::Response {
file,
content_disposition,
content_type,
} = get_content(&body.server_name, body.media_id.clone(), body.allow_remote).await?;
if let Some(FileMeta { Ok(media::get_content::v3::Response {
file,
content_type,
content_disposition,
cross_origin_resource_policy: Some("cross-origin".to_owned()),
})
}
/// # `GET /_matrix/client/v1/media/download/{serverName}/{mediaId}`
///
/// Load media from our server or over federation.
pub async fn get_content_auth_route(
body: Ruma<get_content::v1::Request>,
) -> Result<get_content::v1::Response> {
get_content(&body.server_name, body.media_id.clone(), true).await
}
async fn get_content(
server_name: &ServerName,
media_id: String,
allow_remote: bool,
) -> Result<get_content::v1::Response, Error> {
let mxc = format!("mxc://{}/{}", server_name, media_id);
if let Ok(Some(FileMeta {
content_disposition, content_disposition,
content_type, content_type,
file, file,
}) = services().media.get(mxc.clone()).await? })) = services().media.get(mxc.clone()).await
{ {
Ok(get_content::v3::Response { Ok(get_content::v1::Response {
file, file,
content_type, content_type,
content_disposition, content_disposition: Some(content_disposition),
cross_origin_resource_policy: Some("cross-origin".to_owned()),
}) })
} else if &*body.server_name != services().globals.server_name() && body.allow_remote { } else if server_name != services().globals.server_name() && allow_remote {
let remote_content_response = let remote_content_response =
get_remote_content(&mxc, &body.server_name, body.media_id.clone()).await?; get_remote_content(&mxc, server_name, media_id.clone()).await?;
Ok(get_content::v3::Response { Ok(get_content::v1::Response {
content_disposition: remote_content_response.content_disposition, content_disposition: remote_content_response.content_disposition,
content_type: remote_content_response.content_type, content_type: remote_content_response.content_type,
file: remote_content_response.file, file: remote_content_response.file,
cross_origin_resource_policy: Some("cross-origin".to_owned()),
}) })
} else { } else {
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found.")) Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
@ -134,29 +219,74 @@ pub async fn get_content_route(
/// ///
/// - Only allows federation if `allow_remote` is true /// - Only allows federation if `allow_remote` is true
pub async fn get_content_as_filename_route( pub async fn get_content_as_filename_route(
body: Ruma<get_content_as_filename::v3::Request>, body: Ruma<media::get_content_as_filename::v3::Request>,
) -> Result<get_content_as_filename::v3::Response> { ) -> Result<media::get_content_as_filename::v3::Response> {
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id); let get_content_as_filename::v1::Response {
file,
content_type,
content_disposition,
} = get_content_as_filename(
&body.server_name,
body.media_id.clone(),
body.filename.clone(),
body.allow_remote,
)
.await?;
if let Some(FileMeta { Ok(media::get_content_as_filename::v3::Response {
file,
content_type,
content_disposition,
cross_origin_resource_policy: Some("cross-origin".to_owned()),
})
}
/// # `GET /_matrix/client/v1/media/download/{serverName}/{mediaId}/{fileName}`
///
/// Load media from our server or over federation, permitting desired filename.
pub async fn get_content_as_filename_auth_route(
body: Ruma<get_content_as_filename::v1::Request>,
) -> Result<get_content_as_filename::v1::Response, Error> {
get_content_as_filename(
&body.server_name,
body.media_id.clone(),
body.filename.clone(),
true,
)
.await
}
async fn get_content_as_filename(
server_name: &ServerName,
media_id: String,
filename: String,
allow_remote: bool,
) -> Result<get_content_as_filename::v1::Response, Error> {
let mxc = format!("mxc://{}/{}", server_name, media_id);
if let Ok(Some(FileMeta {
file, content_type, .. file, content_type, ..
}) = services().media.get(mxc.clone()).await? })) = services().media.get(mxc.clone()).await
{ {
Ok(get_content_as_filename::v3::Response { Ok(get_content_as_filename::v1::Response {
file, file,
content_type, content_type,
content_disposition: Some(format!("inline; filename={}", body.filename)), content_disposition: Some(
cross_origin_resource_policy: Some("cross-origin".to_owned()), ContentDisposition::new(ContentDispositionType::Inline)
.with_filename(Some(filename.clone())),
),
}) })
} else if &*body.server_name != services().globals.server_name() && body.allow_remote { } else if server_name != services().globals.server_name() && allow_remote {
let remote_content_response = let remote_content_response =
get_remote_content(&mxc, &body.server_name, body.media_id.clone()).await?; get_remote_content(&mxc, server_name, media_id.clone()).await?;
Ok(get_content_as_filename::v3::Response { Ok(get_content_as_filename::v1::Response {
content_disposition: Some(format!("inline: filename={}", body.filename)), content_disposition: Some(
ContentDisposition::new(ContentDispositionType::Inline)
.with_filename(Some(filename.clone())),
),
content_type: remote_content_response.content_type, content_type: remote_content_response.content_type,
file: remote_content_response.file, file: remote_content_response.file,
cross_origin_resource_policy: Some("cross-origin".to_owned()),
}) })
} else { } else {
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found.")) Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
@ -169,63 +299,169 @@ pub async fn get_content_as_filename_route(
/// ///
/// - Only allows federation if `allow_remote` is true /// - Only allows federation if `allow_remote` is true
pub async fn get_content_thumbnail_route( pub async fn get_content_thumbnail_route(
body: Ruma<get_content_thumbnail::v3::Request>, body: Ruma<media::get_content_thumbnail::v3::Request>,
) -> Result<get_content_thumbnail::v3::Response> { ) -> Result<media::get_content_thumbnail::v3::Response> {
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id); let get_content_thumbnail::v1::Response { file, content_type } = get_content_thumbnail(
&body.server_name,
body.media_id.clone(),
body.height,
body.width,
body.method.clone(),
body.animated,
body.allow_remote,
)
.await?;
if let Some(FileMeta { Ok(media::get_content_thumbnail::v3::Response {
file,
content_type,
cross_origin_resource_policy: Some("cross-origin".to_owned()),
})
}
/// # `GET /_matrix/client/v1/media/thumbnail/{serverName}/{mediaId}`
///
/// Load media thumbnail from our server or over federation.
pub async fn get_content_thumbnail_auth_route(
body: Ruma<get_content_thumbnail::v1::Request>,
) -> Result<get_content_thumbnail::v1::Response> {
get_content_thumbnail(
&body.server_name,
body.media_id.clone(),
body.height,
body.width,
body.method.clone(),
body.animated,
true,
)
.await
}
async fn get_content_thumbnail(
server_name: &ServerName,
media_id: String,
height: UInt,
width: UInt,
method: Option<Method>,
animated: Option<bool>,
allow_remote: bool,
) -> Result<get_content_thumbnail::v1::Response, Error> {
let mxc = format!("mxc://{}/{}", server_name, media_id);
if let Ok(Some(FileMeta {
file, content_type, .. file, content_type, ..
}) = services() })) = services()
.media .media
.get_thumbnail( .get_thumbnail(
mxc.clone(), mxc.clone(),
body.width width
.try_into() .try_into()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid."))?, .map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid."))?,
body.height height
.try_into() .try_into()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid."))?, .map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Height is invalid."))?,
) )
.await? .await
{ {
Ok(get_content_thumbnail::v3::Response { Ok(get_content_thumbnail::v1::Response { file, content_type })
file, } else if server_name != services().globals.server_name() && allow_remote {
content_type, let thumbnail_response = match services()
cross_origin_resource_policy: Some("cross-origin".to_owned()),
})
} else if body.server_name != services().globals.server_name() && body.allow_remote {
let get_thumbnail_response = services()
.sending .sending
.send_federation_request( .send_federation_request(
&body.server_name, server_name,
get_content_thumbnail::v3::Request { federation_media::get_content_thumbnail::v1::Request {
allow_remote: false, height,
height: body.height, width,
width: body.width, method: method.clone(),
method: body.method.clone(), media_id: media_id.clone(),
server_name: body.server_name.clone(),
media_id: body.media_id.clone(),
timeout_ms: Duration::from_secs(20), timeout_ms: Duration::from_secs(20),
allow_redirect: false, animated,
animated: body.animated,
}, },
) )
.await?; .await
{
Ok(federation_media::get_content_thumbnail::v1::Response {
metadata: _,
content: FileOrLocation::File(content),
}) => get_content_thumbnail::v1::Response {
file: content.file,
content_type: content.content_type,
},
Ok(federation_media::get_content_thumbnail::v1::Response {
metadata: _,
content: FileOrLocation::Location(url),
}) => {
let get_content::v1::Response {
file, content_type, ..
} = get_location_content(url).await?;
get_content_thumbnail::v1::Response { file, content_type }
}
Err(Error::BadRequest(ErrorKind::Unrecognized, _)) => {
let media::get_content_thumbnail::v3::Response {
file, content_type, ..
} = services()
.sending
.send_federation_request(
server_name,
media::get_content_thumbnail::v3::Request {
height,
width,
method: method.clone(),
server_name: server_name.to_owned(),
media_id: media_id.clone(),
timeout_ms: Duration::from_secs(20),
allow_redirect: false,
animated,
allow_remote: false,
},
)
.await?;
get_content_thumbnail::v1::Response { file, content_type }
}
Err(e) => return Err(e),
};
services() services()
.media .media
.upload_thumbnail( .upload_thumbnail(
mxc, mxc,
None, thumbnail_response.content_type.as_deref(),
get_thumbnail_response.content_type.as_deref(), width.try_into().expect("all UInts are valid u32s"),
body.width.try_into().expect("all UInts are valid u32s"), height.try_into().expect("all UInts are valid u32s"),
body.height.try_into().expect("all UInts are valid u32s"), &thumbnail_response.file,
&get_thumbnail_response.file,
) )
.await?; .await?;
Ok(get_thumbnail_response) Ok(thumbnail_response)
} else { } else {
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found.")) Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
} }
} }
async fn get_location_content(url: String) -> Result<get_content::v1::Response, Error> {
let client = services().globals.default_client();
let response = client.get(url).send().await?;
let headers = response.headers();
let content_type = headers
.get(CONTENT_TYPE)
.and_then(|header| header.to_str().ok())
.map(ToOwned::to_owned);
let content_disposition = headers
.get(CONTENT_DISPOSITION)
.map(|header| header.as_bytes())
.map(TryFrom::try_from)
.and_then(Result::ok);
let file = response.bytes().await?.to_vec();
Ok(get_content::v1::Response {
file,
content_type,
content_disposition,
})
}

View file

@ -97,7 +97,7 @@ pub async fn join_room_by_id_or_alias_route(
let (servers, room_id) = match OwnedRoomId::try_from(body.room_id_or_alias) { let (servers, room_id) = match OwnedRoomId::try_from(body.room_id_or_alias) {
Ok(room_id) => { Ok(room_id) => {
let mut servers = body.server_name.clone(); let mut servers = body.via.clone();
servers.extend( servers.extend(
services() services()
.rooms .rooms
@ -627,7 +627,7 @@ async fn join_room_by_id_helper(
let event_id = format!( let event_id = format!(
"${}", "${}",
ruma::signatures::reference_hash(&join_event_stub, &room_version_id) ruma::signatures::reference_hash(&join_event_stub, &room_version_id)
.expect("ruma can calculate reference hashes") .expect("Event format validated when event was hashed")
); );
let event_id = <&EventId>::try_from(event_id.as_str()) let event_id = <&EventId>::try_from(event_id.as_str())
.expect("ruma's reference hashes are valid event ids"); .expect("ruma's reference hashes are valid event ids");
@ -1145,7 +1145,7 @@ async fn validate_and_add_event_id(
let event_id = EventId::parse(format!( let event_id = EventId::parse(format!(
"${}", "${}",
ruma::signatures::reference_hash(&value, room_version) ruma::signatures::reference_hash(&value, room_version)
.expect("ruma can calculate reference hashes") .map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Invalid PDU format"))?
)) ))
.expect("ruma's reference hashes are valid event ids"); .expect("ruma's reference hashes are valid event ids");
@ -1614,7 +1614,7 @@ async fn remote_leave_room(user_id: &UserId, room_id: &RoomId) -> Result<()> {
let event_id = EventId::parse(format!( let event_id = EventId::parse(format!(
"${}", "${}",
ruma::signatures::reference_hash(&leave_event_stub, &room_version_id) ruma::signatures::reference_hash(&leave_event_stub, &room_version_id)
.expect("ruma can calculate reference hashes") .expect("Event format validated when event was hashed")
)) ))
.expect("ruma's reference hashes are valid event ids"); .expect("ruma's reference hashes are valid event ids");

View file

@ -27,7 +27,10 @@ pub async fn get_supported_versions_route(
"v1.4".to_owned(), "v1.4".to_owned(),
"v1.5".to_owned(), "v1.5".to_owned(),
], ],
unstable_features: BTreeMap::from_iter([("org.matrix.e2e_cross_signing".to_owned(), true)]), unstable_features: BTreeMap::from_iter([
("org.matrix.e2e_cross_signing".to_owned(), true),
("org.matrix.msc3916.stable".to_owned(), true),
]),
}; };
Ok(resp) Ok(resp)

View file

@ -2,11 +2,15 @@
use crate::{ use crate::{
api::client_server::{self, claim_keys_helper, get_keys_helper}, api::client_server::{self, claim_keys_helper, get_keys_helper},
service::pdu::{gen_event_id_canonical_json, PduBuilder}, service::{
globals::SigningKeys,
media::FileMeta,
pdu::{gen_event_id_canonical_json, PduBuilder},
},
services, utils, Error, PduEvent, Result, Ruma, services, utils, Error, PduEvent, Result, Ruma,
}; };
use axum::{response::IntoResponse, Json}; use axum::{response::IntoResponse, Json};
use axum_extra::headers::{authorization::Credentials, CacheControl, Header}; use axum_extra::headers::{CacheControl, Header};
use get_profile_information::v1::ProfileField; use get_profile_information::v1::ProfileField;
use http::header::AUTHORIZATION; use http::header::AUTHORIZATION;
@ -14,6 +18,9 @@ use ruma::{
api::{ api::{
client::error::{Error as RumaError, ErrorKind}, client::error::{Error as RumaError, ErrorKind},
federation::{ federation::{
authenticated_media::{
get_content, get_content_thumbnail, Content, ContentMetadata, FileOrLocation,
},
authorization::get_event_authorization, authorization::get_event_authorization,
backfill::get_backfill, backfill::get_backfill,
device::get_devices::{self, v1::UserDevice}, device::get_devices::{self, v1::UserDevice},
@ -45,7 +52,6 @@ use ruma::{
StateEventType, TimelineEventType, StateEventType, TimelineEventType,
}, },
serde::{Base64, JsonObject, Raw}, serde::{Base64, JsonObject, Raw},
server_util::authorization::XMatrix,
to_device::DeviceIdOrAllDevices, to_device::DeviceIdOrAllDevices,
uint, user_id, CanonicalJsonObject, CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch, uint, user_id, CanonicalJsonObject, CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch,
OwnedEventId, OwnedRoomId, OwnedServerName, OwnedServerSigningKeyId, OwnedUserId, RoomId, OwnedEventId, OwnedRoomId, OwnedServerName, OwnedServerSigningKeyId, OwnedUserId, RoomId,
@ -204,7 +210,7 @@ where
.try_into_http_request::<Vec<u8>>( .try_into_http_request::<Vec<u8>>(
&actual_destination_str, &actual_destination_str,
SendAccessToken::IfRequired(""), SendAccessToken::IfRequired(""),
&[MatrixVersion::V1_4], &[MatrixVersion::V1_11],
) )
.map_err(|e| { .map_err(|e| {
warn!( warn!(
@ -268,15 +274,15 @@ where
for s in signature_server { for s in signature_server {
http_request.headers_mut().insert( http_request.headers_mut().insert(
AUTHORIZATION, AUTHORIZATION,
XMatrix::parse(&format!( format!(
"X-Matrix origin=\"{}\",destination=\"{}\",key=\"{}\",sig=\"{}\"", "X-Matrix origin=\"{}\",destination=\"{}\",key=\"{}\",sig=\"{}\"",
services().globals.server_name(), services().globals.server_name(),
destination, destination,
s.0, s.0,
s.1 s.1
)) )
.expect("When Ruma signs JSON, it produces a valid base64 signature. All other types are valid ServerNames or OwnedKeyId") .try_into()
.encode(), .unwrap(),
); );
} }
} }
@ -336,7 +342,7 @@ where
response.map_err(|e| { response.map_err(|e| {
warn!( warn!(
"Invalid 200 response from {} on: {} {}", "Invalid 200 response from {} on: {} {:?}",
&destination, url, e &destination, url, e
); );
Error::BadServerResponse("Server returned bad 200 response.") Error::BadServerResponse("Server returned bad 200 response.")
@ -564,6 +570,13 @@ async fn get_srv_destination(delegated_hostname: String) -> (FedDest, Option<Ins
(override_ip.iter().collect(), force_port.unwrap_or(8448)), (override_ip.iter().collect(), force_port.unwrap_or(8448)),
); );
} else { } else {
// Removing in case there was previously a SRV record
services()
.globals
.tls_name_override
.write()
.unwrap()
.remove(&delegated_hostname);
warn!("Using SRV record, but could not resolve to IP"); warn!("Using SRV record, but could not resolve to IP");
} }
@ -576,6 +589,13 @@ async fn get_srv_destination(delegated_hostname: String) -> (FedDest, Option<Ins
(add_port_to_hostname(&delegated_hostname), Some(timestamp)) (add_port_to_hostname(&delegated_hostname), Some(timestamp))
} }
} else { } else {
// Removing in case there was previously a SRV record
services()
.globals
.tls_name_override
.write()
.unwrap()
.remove(&delegated_hostname);
debug!("No SRV records found"); debug!("No SRV records found");
(add_port_to_hostname(&delegated_hostname), None) (add_port_to_hostname(&delegated_hostname), None)
} }
@ -793,17 +813,78 @@ pub fn parse_incoming_pdu(
let (event_id, value) = match gen_event_id_canonical_json(pdu, &room_version_id) { let (event_id, value) = match gen_event_id_canonical_json(pdu, &room_version_id) {
Ok(t) => t, Ok(t) => t,
Err(_) => { Err(e) => {
// Event could not be converted to canonical json // Event could not be converted to canonical json
return Err(Error::BadRequest( return Err(e);
ErrorKind::InvalidParam,
"Could not convert event to canonical json.",
));
} }
}; };
Ok((event_id, value, room_id)) Ok((event_id, value, room_id))
} }
/// Attempts to parse and append PDU to timeline.
/// If no event ID is returned, then the PDU was failed to be parsed.
/// If the Ok(()) is returned, then the PDU was successfully appended to the timeline.
async fn handle_pdu_in_transaction(
origin: &ServerName,
pub_key_map: &RwLock<BTreeMap<String, SigningKeys>>,
pdu: &RawJsonValue,
) -> (Option<OwnedEventId>, Result<()>) {
let (event_id, value, room_id) = match parse_incoming_pdu(pdu) {
Ok(t) => t,
Err(e) => {
warn!("Could not parse PDU: {e}");
warn!("Full PDU: {:?}", &pdu);
return (None, Err(Error::BadServerResponse("Could not parse PDU")));
}
};
// Makes use of the m.room.create event. If we cannot fetch this event,
// we must have never been in that room.
if services().rooms.state.get_room_version(&room_id).is_err() {
debug!("Room {room_id} is not known to this server");
return (
Some(event_id),
Err(Error::BadServerResponse("Room is not known to this server")),
);
}
// We do not add the event_id field to the pdu here because of signature and hashes checks
let mutex = Arc::clone(
services()
.globals
.roomid_mutex_federation
.write()
.await
.entry(room_id.to_owned())
.or_default(),
);
let mutex_lock = mutex.lock().await;
let start_time = Instant::now();
if let Err(e) = services()
.rooms
.event_handler
.handle_incoming_pdu(origin, &event_id, &room_id, value, true, pub_key_map)
.await
{
warn!("Error appending PDU to timeline: {}: {:?}", e, pdu);
return (Some(event_id), Err(e));
}
drop(mutex_lock);
let elapsed = start_time.elapsed();
debug!(
"Handling transaction of event {} took {}m{}s",
event_id,
elapsed.as_secs() / 60,
elapsed.as_secs() % 60
);
(Some(event_id), Ok(()))
}
/// # `PUT /_matrix/federation/v1/send/{txnId}` /// # `PUT /_matrix/federation/v1/send/{txnId}`
/// ///
/// Push EDUs and PDUs to this server. /// Push EDUs and PDUs to this server.
@ -828,77 +909,11 @@ pub async fn send_transaction_message_route(
// let mut auth_cache = EventMap::new(); // let mut auth_cache = EventMap::new();
for pdu in &body.pdus { for pdu in &body.pdus {
let value: CanonicalJsonObject = serde_json::from_str(pdu.get()).map_err(|e| { let (event_id, result) =
warn!("Error parsing incoming event {:?}: {:?}", pdu, e); handle_pdu_in_transaction(sender_servername, &pub_key_map, pdu).await;
Error::BadServerResponse("Invalid PDU in server response")
})?;
let room_id: OwnedRoomId = value
.get("room_id")
.and_then(|id| RoomId::parse(id.as_str()?).ok())
.ok_or(Error::BadRequest(
ErrorKind::InvalidParam,
"Invalid room id in pdu",
))?;
if services().rooms.state.get_room_version(&room_id).is_err() { if let Some(event_id) = event_id {
debug!("Server is not in room {room_id}"); resolved_map.insert(event_id.clone(), result.map_err(|e| e.sanitized_error()));
continue;
}
let r = parse_incoming_pdu(pdu);
let (event_id, value, room_id) = match r {
Ok(t) => t,
Err(e) => {
warn!("Could not parse PDU: {e}");
warn!("Full PDU: {:?}", &pdu);
continue;
}
};
// We do not add the event_id field to the pdu here because of signature and hashes checks
let mutex = Arc::clone(
services()
.globals
.roomid_mutex_federation
.write()
.await
.entry(room_id.to_owned())
.or_default(),
);
let mutex_lock = mutex.lock().await;
let start_time = Instant::now();
resolved_map.insert(
event_id.clone(),
services()
.rooms
.event_handler
.handle_incoming_pdu(
sender_servername,
&event_id,
&room_id,
value,
true,
&pub_key_map,
)
.await
.map(|_| ()),
);
drop(mutex_lock);
let elapsed = start_time.elapsed();
debug!(
"Handling transaction of event {} took {}m{}s",
event_id,
elapsed.as_secs() / 60,
elapsed.as_secs() % 60
);
}
for pdu in &resolved_map {
if let Err(e) = pdu.1 {
if matches!(e, Error::BadRequest(ErrorKind::NotFound, _)) {
warn!("Incoming PDU failed {:?}", pdu);
}
} }
} }
@ -1067,12 +1082,7 @@ pub async fn send_transaction_message_route(
} }
} }
Ok(send_transaction_message::v1::Response { Ok(send_transaction_message::v1::Response { pdus: resolved_map })
pdus: resolved_map
.into_iter()
.map(|(e, r)| (e, r.map_err(|e| e.sanitized_error())))
.collect(),
})
} }
/// # `GET /_matrix/federation/v1/event/{eventId}` /// # `GET /_matrix/federation/v1/event/{eventId}`
@ -1815,7 +1825,7 @@ pub async fn create_invite_route(
let event_id = EventId::parse(format!( let event_id = EventId::parse(format!(
"${}", "${}",
ruma::signatures::reference_hash(&signed_event, &body.room_version) ruma::signatures::reference_hash(&signed_event, &body.room_version)
.expect("ruma can calculate reference hashes") .expect("Event format validated when event was hashed")
)) ))
.expect("ruma's reference hashes are valid event ids"); .expect("ruma's reference hashes are valid event ids");
@ -1884,6 +1894,90 @@ pub async fn create_invite_route(
}) })
} }
/// # `GET /_matrix/federation/v1/media/download/{serverName}/{mediaId}`
///
/// Load media from our server.
pub async fn get_content_route(
body: Ruma<get_content::v1::Request>,
) -> Result<get_content::v1::Response> {
let mxc = format!(
"mxc://{}/{}",
services().globals.server_name(),
body.media_id
);
if let Some(FileMeta {
content_disposition,
content_type,
file,
}) = services().media.get(mxc.clone()).await?
{
Ok(get_content::v1::Response::new(
ContentMetadata::new(),
FileOrLocation::File(Content {
file,
content_type,
content_disposition: Some(content_disposition),
}),
))
} else {
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
}
}
/// # `GET /_matrix/federation/v1/media/thumbnail/{serverName}/{mediaId}`
///
/// Load media thumbnail from our server or over federation.
pub async fn get_content_thumbnail_route(
body: Ruma<get_content_thumbnail::v1::Request>,
) -> Result<get_content_thumbnail::v1::Response> {
let mxc = format!(
"mxc://{}/{}",
services().globals.server_name(),
body.media_id
);
let Some(FileMeta {
file,
content_type,
content_disposition,
}) = services()
.media
.get_thumbnail(
mxc.clone(),
body.width
.try_into()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid."))?,
body.height
.try_into()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid."))?,
)
.await?
else {
return Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."));
};
services()
.media
.upload_thumbnail(
mxc,
content_type.as_deref(),
body.width.try_into().expect("all UInts are valid u32s"),
body.height.try_into().expect("all UInts are valid u32s"),
&file,
)
.await?;
Ok(get_content_thumbnail::v1::Response::new(
ContentMetadata::new(),
FileOrLocation::File(Content {
file,
content_type,
content_disposition: Some(content_disposition),
}),
))
}
/// # `GET /_matrix/federation/v1/user/devices/{userId}` /// # `GET /_matrix/federation/v1/user/devices/{userId}`
/// ///
/// Gets information on all devices of the user. /// Gets information on all devices of the user.

View file

@ -59,7 +59,7 @@ pub struct Config {
pub allow_unstable_room_versions: bool, pub allow_unstable_room_versions: bool,
#[serde(default = "default_default_room_version")] #[serde(default = "default_default_room_version")]
pub default_room_version: RoomVersionId, pub default_room_version: RoomVersionId,
#[serde(default)] #[serde(default, flatten)]
pub well_known: WellKnownConfig, pub well_known: WellKnownConfig,
#[serde(default = "false_fn")] #[serde(default = "false_fn")]
pub allow_jaeger: bool, pub allow_jaeger: bool,
@ -97,7 +97,9 @@ pub struct TlsConfig {
#[derive(Clone, Debug, Deserialize, Default)] #[derive(Clone, Debug, Deserialize, Default)]
pub struct WellKnownConfig { pub struct WellKnownConfig {
#[serde(rename = "well_known_client")]
pub client: Option<Url>, pub client: Option<Url>,
#[serde(rename = "well_known_server")]
pub server: Option<OwnedServerName>, pub server: Option<OwnedServerName>,
} }

View file

@ -1,4 +1,4 @@
use ruma::api::client::error::ErrorKind; use ruma::{api::client::error::ErrorKind, http_headers::ContentDisposition};
use crate::{database::KeyValueDatabase, service, utils, Error, Result}; use crate::{database::KeyValueDatabase, service, utils, Error, Result};
@ -8,7 +8,7 @@ impl service::media::Data for KeyValueDatabase {
mxc: String, mxc: String,
width: u32, width: u32,
height: u32, height: u32,
content_disposition: Option<&str>, content_disposition: &ContentDisposition,
content_type: Option<&str>, content_type: Option<&str>,
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
let mut key = mxc.as_bytes().to_vec(); let mut key = mxc.as_bytes().to_vec();
@ -16,12 +16,7 @@ impl service::media::Data for KeyValueDatabase {
key.extend_from_slice(&width.to_be_bytes()); key.extend_from_slice(&width.to_be_bytes());
key.extend_from_slice(&height.to_be_bytes()); key.extend_from_slice(&height.to_be_bytes());
key.push(0xff); key.push(0xff);
key.extend_from_slice( key.extend_from_slice(content_disposition.to_string().as_bytes());
content_disposition
.as_ref()
.map(|f| f.as_bytes())
.unwrap_or_default(),
);
key.push(0xff); key.push(0xff);
key.extend_from_slice( key.extend_from_slice(
content_type content_type
@ -40,7 +35,7 @@ impl service::media::Data for KeyValueDatabase {
mxc: String, mxc: String,
width: u32, width: u32,
height: u32, height: u32,
) -> Result<(Option<String>, Option<String>, Vec<u8>)> { ) -> Result<(ContentDisposition, Option<String>, Vec<u8>)> {
let mut prefix = mxc.as_bytes().to_vec(); let mut prefix = mxc.as_bytes().to_vec();
prefix.push(0xff); prefix.push(0xff);
prefix.extend_from_slice(&width.to_be_bytes()); prefix.extend_from_slice(&width.to_be_bytes());
@ -68,15 +63,9 @@ impl service::media::Data for KeyValueDatabase {
.next() .next()
.ok_or_else(|| Error::bad_database("Media ID in db is invalid."))?; .ok_or_else(|| Error::bad_database("Media ID in db is invalid."))?;
let content_disposition = if content_disposition_bytes.is_empty() { let content_disposition = content_disposition_bytes.try_into().unwrap_or_else(|_| {
None ContentDisposition::new(ruma::http_headers::ContentDispositionType::Inline)
} else { });
Some(
utils::string_from_bytes(content_disposition_bytes).map_err(|_| {
Error::bad_database("Content Disposition in mediaid_file is invalid unicode.")
})?,
)
};
Ok((content_disposition, content_type, key)) Ok((content_disposition, content_type, key))
} }
} }

View file

@ -6,6 +6,7 @@ use crate::{
SERVICES, SERVICES,
}; };
use abstraction::{KeyValueDatabaseEngine, KvTree}; use abstraction::{KeyValueDatabaseEngine, KvTree};
use base64::{engine::general_purpose, Engine};
use directories::ProjectDirs; use directories::ProjectDirs;
use lru_cache::LruCache; use lru_cache::LruCache;
@ -424,7 +425,7 @@ impl KeyValueDatabase {
} }
// If the database has any data, perform data migrations before starting // If the database has any data, perform data migrations before starting
let latest_database_version = 13; let latest_database_version = 16;
if services().users.count()? > 0 { if services().users.count()? > 0 {
// MIGRATIONS // MIGRATIONS
@ -941,6 +942,86 @@ impl KeyValueDatabase {
warn!("Migration: 12 -> 13 finished"); warn!("Migration: 12 -> 13 finished");
} }
if services().globals.database_version()? < 16 {
// Reconstruct all media using the filesystem
db.mediaid_file.clear().unwrap();
for file in fs::read_dir(services().globals.get_media_folder()).unwrap() {
let file = file.unwrap();
let mediaid = general_purpose::URL_SAFE_NO_PAD
.decode(file.file_name().into_string().unwrap())
.unwrap();
let mut parts = mediaid.rsplit(|&b| b == 0xff);
let mut removed_bytes = 0;
let content_type_bytes = parts.next().unwrap();
removed_bytes += content_type_bytes.len() + 1;
let content_disposition_bytes = parts
.next()
.ok_or_else(|| Error::bad_database("Media ID in db is invalid."))?;
removed_bytes += content_disposition_bytes.len();
let mut content_disposition =
utils::string_from_bytes(content_disposition_bytes).map_err(|_| {
Error::bad_database("Content Disposition in mediaid_file is invalid.")
})?;
if content_disposition.contains("filename=")
&& !content_disposition.contains("filename=\"")
{
println!("{}", &content_disposition);
content_disposition =
content_disposition.replacen("filename=", "filename=\"", 1);
content_disposition.push('"');
println!("{}", &content_disposition);
let mut new_key = mediaid[..(mediaid.len() - removed_bytes)].to_vec();
assert!(*new_key.last().unwrap() == 0xff);
let mut shorter_key = new_key.clone();
shorter_key.extend(
ruma::http_headers::ContentDisposition::new(
ruma::http_headers::ContentDispositionType::Inline,
)
.to_string()
.as_bytes(),
);
shorter_key.push(0xff);
shorter_key.extend_from_slice(content_type_bytes);
new_key.extend_from_slice(content_disposition.to_string().as_bytes());
new_key.push(0xff);
new_key.extend_from_slice(content_type_bytes);
// Some file names are too long. Ignore those.
match fs::rename(
services().globals.get_media_file(&mediaid),
services().globals.get_media_file(&new_key),
) {
Ok(_) => {
db.mediaid_file.insert(&new_key, &[])?;
}
Err(_) => {
fs::rename(
services().globals.get_media_file(&mediaid),
services().globals.get_media_file(&shorter_key),
)
.unwrap();
db.mediaid_file.insert(&shorter_key, &[])?;
}
}
} else {
db.mediaid_file.insert(&mediaid, &[])?;
}
}
services().globals.bump_database_version(16)?;
warn!("Migration: 13 -> 16 finished");
}
assert_eq!( assert_eq!(
services().globals.database_version().unwrap(), services().globals.database_version().unwrap(),
latest_database_version latest_database_version

View file

@ -57,7 +57,7 @@ async fn main() {
)) ))
.nested(), .nested(),
) )
.merge(Env::prefixed("CONDUIT_").global()); .merge(Env::prefixed("CONDUIT_").global().split("__"));
let config = match raw_config.extract::<Config>() { let config = match raw_config.extract::<Config>() {
Ok(s) => s, Ok(s) => s,
@ -379,10 +379,14 @@ fn routes(config: &Config) -> Router {
.ruma_route(client_server::turn_server_route) .ruma_route(client_server::turn_server_route)
.ruma_route(client_server::send_event_to_device_route) .ruma_route(client_server::send_event_to_device_route)
.ruma_route(client_server::get_media_config_route) .ruma_route(client_server::get_media_config_route)
.ruma_route(client_server::get_media_config_auth_route)
.ruma_route(client_server::create_content_route) .ruma_route(client_server::create_content_route)
.ruma_route(client_server::get_content_route) .ruma_route(client_server::get_content_route)
.ruma_route(client_server::get_content_auth_route)
.ruma_route(client_server::get_content_as_filename_route) .ruma_route(client_server::get_content_as_filename_route)
.ruma_route(client_server::get_content_as_filename_auth_route)
.ruma_route(client_server::get_content_thumbnail_route) .ruma_route(client_server::get_content_thumbnail_route)
.ruma_route(client_server::get_content_thumbnail_auth_route)
.ruma_route(client_server::get_devices_route) .ruma_route(client_server::get_devices_route)
.ruma_route(client_server::get_device_route) .ruma_route(client_server::get_device_route)
.ruma_route(client_server::update_device_route) .ruma_route(client_server::update_device_route)
@ -440,6 +444,8 @@ fn routes(config: &Config) -> Router {
.ruma_route(server_server::create_join_event_v2_route) .ruma_route(server_server::create_join_event_v2_route)
.ruma_route(server_server::create_invite_route) .ruma_route(server_server::create_invite_route)
.ruma_route(server_server::get_devices_route) .ruma_route(server_server::get_devices_route)
.ruma_route(server_server::get_content_route)
.ruma_route(server_server::get_content_thumbnail_route)
.ruma_route(server_server::get_room_information_route) .ruma_route(server_server::get_room_information_route)
.ruma_route(server_server::get_profile_information_route) .ruma_route(server_server::get_profile_information_route)
.ruma_route(server_server::get_keys_route) .ruma_route(server_server::get_keys_route)

View file

@ -1,3 +1,5 @@
use ruma::http_headers::ContentDisposition;
use crate::Result; use crate::Result;
pub trait Data: Send + Sync { pub trait Data: Send + Sync {
@ -6,7 +8,7 @@ pub trait Data: Send + Sync {
mxc: String, mxc: String,
width: u32, width: u32,
height: u32, height: u32,
content_disposition: Option<&str>, content_disposition: &ContentDisposition,
content_type: Option<&str>, content_type: Option<&str>,
) -> Result<Vec<u8>>; ) -> Result<Vec<u8>>;
@ -16,5 +18,5 @@ pub trait Data: Send + Sync {
mxc: String, mxc: String,
width: u32, width: u32,
height: u32, height: u32,
) -> Result<(Option<String>, Option<String>, Vec<u8>)>; ) -> Result<(ContentDisposition, Option<String>, Vec<u8>)>;
} }

View file

@ -2,6 +2,7 @@ mod data;
use std::io::Cursor; use std::io::Cursor;
pub use data::Data; pub use data::Data;
use ruma::http_headers::{ContentDisposition, ContentDispositionType};
use crate::{services, Result}; use crate::{services, Result};
use image::imageops::FilterType; use image::imageops::FilterType;
@ -12,7 +13,7 @@ use tokio::{
}; };
pub struct FileMeta { pub struct FileMeta {
pub content_disposition: Option<String>, pub content_disposition: ContentDisposition,
pub content_type: Option<String>, pub content_type: Option<String>,
pub file: Vec<u8>, pub file: Vec<u8>,
} }
@ -26,14 +27,17 @@ impl Service {
pub async fn create( pub async fn create(
&self, &self,
mxc: String, mxc: String,
content_disposition: Option<&str>, content_disposition: Option<ContentDisposition>,
content_type: Option<&str>, content_type: Option<&str>,
file: &[u8], file: &[u8],
) -> Result<()> { ) -> Result<()> {
let content_disposition =
content_disposition.unwrap_or(ContentDisposition::new(ContentDispositionType::Inline));
// Width, Height = 0 if it's not a thumbnail // Width, Height = 0 if it's not a thumbnail
let key = self let key = self
.db .db
.create_file_metadata(mxc, 0, 0, content_disposition, content_type)?; .create_file_metadata(mxc, 0, 0, &content_disposition, content_type)?;
let path = services().globals.get_media_file(&key); let path = services().globals.get_media_file(&key);
let mut f = File::create(path).await?; let mut f = File::create(path).await?;
@ -46,15 +50,18 @@ impl Service {
pub async fn upload_thumbnail( pub async fn upload_thumbnail(
&self, &self,
mxc: String, mxc: String,
content_disposition: Option<&str>,
content_type: Option<&str>, content_type: Option<&str>,
width: u32, width: u32,
height: u32, height: u32,
file: &[u8], file: &[u8],
) -> Result<()> { ) -> Result<()> {
let key = let key = self.db.create_file_metadata(
self.db mxc,
.create_file_metadata(mxc, width, height, content_disposition, content_type)?; width,
height,
&ContentDisposition::new(ContentDispositionType::Inline),
content_type,
)?;
let path = services().globals.get_media_file(&key); let path = services().globals.get_media_file(&key);
let mut f = File::create(path).await?; let mut f = File::create(path).await?;
@ -198,7 +205,7 @@ impl Service {
mxc, mxc,
width, width,
height, height,
content_disposition.as_deref(), &content_disposition,
content_type.as_deref(), content_type.as_deref(),
)?; )?;

View file

@ -1,5 +1,6 @@
use crate::Error; use crate::Error;
use ruma::{ use ruma::{
api::client::error::ErrorKind,
canonical_json::redact_content_in_place, canonical_json::redact_content_in_place,
events::{ events::{
room::{member::RoomMemberEventContent, redaction::RoomRedactionEventContent}, room::{member::RoomMemberEventContent, redaction::RoomRedactionEventContent},
@ -443,7 +444,7 @@ pub(crate) fn gen_event_id_canonical_json(
"${}", "${}",
// Anything higher than version3 behaves the same // Anything higher than version3 behaves the same
ruma::signatures::reference_hash(&value, room_version_id) ruma::signatures::reference_hash(&value, room_version_id)
.expect("ruma can calculate reference hashes") .map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Invalid PDU format"))?
) )
.try_into() .try_into()
.expect("ruma's reference hashes are valid event ids"); .expect("ruma's reference hashes are valid event ids");

View file

@ -1477,7 +1477,7 @@ impl Service {
let event_id = format!( let event_id = format!(
"${}", "${}",
ruma::signatures::reference_hash(&value, room_version) ruma::signatures::reference_hash(&value, room_version)
.expect("ruma can calculate reference hashes") .map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Invalid PDU format"))?
); );
let event_id = <&EventId>::try_from(event_id.as_str()) let event_id = <&EventId>::try_from(event_id.as_str())
.expect("ruma's reference hashes are valid event ids"); .expect("ruma's reference hashes are valid event ids");

View file

@ -815,7 +815,7 @@ impl Service {
pdu.event_id = EventId::parse_arc(format!( pdu.event_id = EventId::parse_arc(format!(
"${}", "${}",
ruma::signatures::reference_hash(&pdu_json, &room_version_id) ruma::signatures::reference_hash(&pdu_json, &room_version_id)
.expect("ruma can calculate reference hashes") .expect("Event format validated when event was hashed")
)) ))
.expect("ruma's reference hashes are valid event ids"); .expect("ruma's reference hashes are valid event ids");