Compare commits

..

No commits in common. "next" and "config-env-var-split" have entirely different histories.

19 changed files with 234 additions and 665 deletions

40
Cargo.lock generated
View file

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

View file

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

View file

@ -58,8 +58,7 @@ The `global` section contains the following fields:
| `turn_secret` | `string` | The TURN secret | `""` |
| `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 |
| `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) |
| `well_known` | `table` | Used for [delegation](delegation.md) | See [delegation](delegation.md) |
### 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
To configure it, use the following options:
To configure it, use the following options in the `global.well_known` table:
| Field | Type | Description | Default |
| --- | --- | --- | --- |
| `well_known_client` | `String` | The URL that clients should use to connect to Conduit | `https://<server_name>` |
| `well_known_server` | `String` | The hostname and port servers should use to connect to Conduit | `<server_name>:443` |
| `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` |
### Example
```toml
[global]
well_known_client = "https://matrix.example.org"
well_known_server = "matrix.example.org:443"
[global.well_known]
client = "https://matrix.example.org"
server = "matrix.example.org:443"
```
## Manual

View file

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

View file

@ -2,7 +2,7 @@
## 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/element-hq/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/matrix-org/synapse/blob/develop/docs/turn-howto.md).
## Edit/Add a few settings to your existing conduit.toml

View file

@ -4,21 +4,12 @@
use std::time::Duration;
use crate::{service::media::FileMeta, services, utils, Error, Result, Ruma};
use http::header::{CONTENT_DISPOSITION, CONTENT_TYPE};
use ruma::{
api::{
client::{
authenticated_media::{
get_content, get_content_as_filename, get_content_thumbnail, get_media_config,
},
use ruma::api::client::{
error::ErrorKind,
media::{self, create_content},
media::{
create_content, get_content, get_content_as_filename, get_content_thumbnail,
get_media_config,
},
federation::authenticated_media::{self as federation_media, FileOrLocation},
},
http_headers::{ContentDisposition, ContentDispositionType},
media::Method,
ServerName, UInt,
};
const MXC_LENGTH: usize = 32;
@ -27,20 +18,9 @@ const MXC_LENGTH: usize = 32;
///
/// Returns max upload size.
pub async fn get_media_config_route(
_body: Ruma<media::get_media_config::v3::Request>,
) -> Result<media::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 {
_body: Ruma<get_media_config::v3::Request>,
) -> Result<get_media_config::v3::Response> {
Ok(get_media_config::v3::Response {
upload_size: services().globals.max_request_size().into(),
})
}
@ -64,10 +44,10 @@ pub async fn create_content_route(
.media
.create(
mxc.clone(),
Some(
ContentDisposition::new(ContentDispositionType::Inline)
.with_filename(body.filename.clone()),
),
body.filename
.as_ref()
.map(|filename| "inline; filename=".to_owned() + filename)
.as_deref(),
body.content_type.as_deref(),
&body.file,
)
@ -81,67 +61,28 @@ pub async fn create_content_route(
pub async fn get_remote_content(
mxc: &str,
server_name: &ServerName,
server_name: &ruma::ServerName,
media_id: String,
) -> Result<get_content::v1::Response, Error> {
let content_response = match services()
) -> Result<get_content::v3::Response, Error> {
let content_response = services()
.sending
.send_federation_request(
server_name,
federation_media::get_content::v1::Request {
media_id: media_id.clone(),
timeout_ms: Duration::from_secs(20),
},
)
.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 {
get_content::v3::Request {
allow_remote: false,
server_name: server_name.to_owned(),
media_id,
timeout_ms: Duration::from_secs(20),
allow_remote: false,
allow_redirect: true,
allow_redirect: false,
},
)
.await?;
get_content::v1::Response {
file,
content_type,
content_disposition,
}
}
Err(e) => return Err(e),
};
services()
.media
.create(
mxc.to_owned(),
content_response.content_disposition.clone(),
content_response.content_disposition.as_deref(),
content_response.content_type.as_deref(),
&content_response.file,
)
@ -156,57 +97,31 @@ pub async fn get_remote_content(
///
/// - Only allows federation if `allow_remote` is true
pub async fn get_content_route(
body: Ruma<media::get_content::v3::Request>,
) -> Result<media::get_content::v3::Response> {
let get_content::v1::Response {
file,
body: Ruma<get_content::v3::Request>,
) -> Result<get_content::v3::Response> {
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id);
if let Some(FileMeta {
content_disposition,
content_type,
} = get_content(&body.server_name, body.media_id.clone(), body.allow_remote).await?;
Ok(media::get_content::v3::Response {
file,
}) = services().media.get(mxc.clone()).await?
{
Ok(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_type,
file,
})) = services().media.get(mxc.clone()).await
{
Ok(get_content::v1::Response {
file,
content_type,
content_disposition: Some(content_disposition),
})
} else if server_name != services().globals.server_name() && allow_remote {
} else if &*body.server_name != services().globals.server_name() && body.allow_remote {
let remote_content_response =
get_remote_content(&mxc, server_name, media_id.clone()).await?;
get_remote_content(&mxc, &body.server_name, body.media_id.clone()).await?;
Ok(get_content::v1::Response {
Ok(get_content::v3::Response {
content_disposition: remote_content_response.content_disposition,
content_type: remote_content_response.content_type,
file: remote_content_response.file,
cross_origin_resource_policy: Some("cross-origin".to_owned()),
})
} else {
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
@ -219,74 +134,29 @@ async fn get_content(
///
/// - Only allows federation if `allow_remote` is true
pub async fn get_content_as_filename_route(
body: Ruma<media::get_content_as_filename::v3::Request>,
) -> Result<media::get_content_as_filename::v3::Response> {
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?;
body: Ruma<get_content_as_filename::v3::Request>,
) -> Result<get_content_as_filename::v3::Response> {
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id);
Ok(media::get_content_as_filename::v3::Response {
if let Some(FileMeta {
file, content_type, ..
}) = services().media.get(mxc.clone()).await?
{
Ok(get_content_as_filename::v3::Response {
file,
content_type,
content_disposition,
content_disposition: Some(format!("inline; filename={}", body.filename)),
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, ..
})) = services().media.get(mxc.clone()).await
{
Ok(get_content_as_filename::v1::Response {
file,
content_type,
content_disposition: Some(
ContentDisposition::new(ContentDispositionType::Inline)
.with_filename(Some(filename.clone())),
),
})
} else if server_name != services().globals.server_name() && allow_remote {
} else if &*body.server_name != services().globals.server_name() && body.allow_remote {
let remote_content_response =
get_remote_content(&mxc, server_name, media_id.clone()).await?;
get_remote_content(&mxc, &body.server_name, body.media_id.clone()).await?;
Ok(get_content_as_filename::v1::Response {
content_disposition: Some(
ContentDisposition::new(ContentDispositionType::Inline)
.with_filename(Some(filename.clone())),
),
Ok(get_content_as_filename::v3::Response {
content_disposition: Some(format!("inline: filename={}", body.filename)),
content_type: remote_content_response.content_type,
file: remote_content_response.file,
cross_origin_resource_policy: Some("cross-origin".to_owned()),
})
} else {
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
@ -299,169 +169,63 @@ async fn get_content_as_filename(
///
/// - Only allows federation if `allow_remote` is true
pub async fn get_content_thumbnail_route(
body: Ruma<media::get_content_thumbnail::v3::Request>,
) -> Result<media::get_content_thumbnail::v3::Response> {
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?;
body: Ruma<get_content_thumbnail::v3::Request>,
) -> Result<get_content_thumbnail::v3::Response> {
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id);
Ok(media::get_content_thumbnail::v3::Response {
if let Some(FileMeta {
file, content_type, ..
}) = 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?
{
Ok(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(
} else if body.server_name != services().globals.server_name() && body.allow_remote {
let get_thumbnail_response = services()
.sending
.send_federation_request(
&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, ..
})) = services()
.media
.get_thumbnail(
mxc.clone(),
width
.try_into()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid."))?,
height
.try_into()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Height is invalid."))?,
)
.await
{
Ok(get_content_thumbnail::v1::Response { file, content_type })
} else if server_name != services().globals.server_name() && allow_remote {
let thumbnail_response = match services()
.sending
.send_federation_request(
server_name,
federation_media::get_content_thumbnail::v1::Request {
height,
width,
method: method.clone(),
media_id: media_id.clone(),
timeout_ms: Duration::from_secs(20),
animated,
},
)
.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(),
get_content_thumbnail::v3::Request {
allow_remote: false,
height: body.height,
width: body.width,
method: body.method.clone(),
server_name: body.server_name.clone(),
media_id: body.media_id.clone(),
timeout_ms: Duration::from_secs(20),
allow_redirect: false,
animated,
allow_remote: false,
animated: body.animated,
},
)
.await?;
get_content_thumbnail::v1::Response { file, content_type }
}
Err(e) => return Err(e),
};
services()
.media
.upload_thumbnail(
mxc,
thumbnail_response.content_type.as_deref(),
width.try_into().expect("all UInts are valid u32s"),
height.try_into().expect("all UInts are valid u32s"),
&thumbnail_response.file,
None,
get_thumbnail_response.content_type.as_deref(),
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?;
Ok(thumbnail_response)
Ok(get_thumbnail_response)
} else {
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) {
Ok(room_id) => {
let mut servers = body.via.clone();
let mut servers = body.server_name.clone();
servers.extend(
services()
.rooms
@ -627,7 +627,7 @@ async fn join_room_by_id_helper(
let event_id = format!(
"${}",
ruma::signatures::reference_hash(&join_event_stub, &room_version_id)
.expect("Event format validated when event was hashed")
.expect("ruma can calculate reference hashes")
);
let event_id = <&EventId>::try_from(event_id.as_str())
.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!(
"${}",
ruma::signatures::reference_hash(&value, room_version)
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Invalid PDU format"))?
.expect("ruma can calculate reference hashes")
))
.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!(
"${}",
ruma::signatures::reference_hash(&leave_event_stub, &room_version_id)
.expect("Event format validated when event was hashed")
.expect("ruma can calculate reference hashes")
))
.expect("ruma's reference hashes are valid event ids");

View file

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

View file

@ -2,15 +2,11 @@
use crate::{
api::client_server::{self, claim_keys_helper, get_keys_helper},
service::{
globals::SigningKeys,
media::FileMeta,
pdu::{gen_event_id_canonical_json, PduBuilder},
},
service::pdu::{gen_event_id_canonical_json, PduBuilder},
services, utils, Error, PduEvent, Result, Ruma,
};
use axum::{response::IntoResponse, Json};
use axum_extra::headers::{CacheControl, Header};
use axum_extra::headers::{authorization::Credentials, CacheControl, Header};
use get_profile_information::v1::ProfileField;
use http::header::AUTHORIZATION;
@ -18,9 +14,6 @@ use ruma::{
api::{
client::error::{Error as RumaError, ErrorKind},
federation::{
authenticated_media::{
get_content, get_content_thumbnail, Content, ContentMetadata, FileOrLocation,
},
authorization::get_event_authorization,
backfill::get_backfill,
device::get_devices::{self, v1::UserDevice},
@ -52,6 +45,7 @@ use ruma::{
StateEventType, TimelineEventType,
},
serde::{Base64, JsonObject, Raw},
server_util::authorization::XMatrix,
to_device::DeviceIdOrAllDevices,
uint, user_id, CanonicalJsonObject, CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch,
OwnedEventId, OwnedRoomId, OwnedServerName, OwnedServerSigningKeyId, OwnedUserId, RoomId,
@ -210,7 +204,7 @@ where
.try_into_http_request::<Vec<u8>>(
&actual_destination_str,
SendAccessToken::IfRequired(""),
&[MatrixVersion::V1_11],
&[MatrixVersion::V1_4],
)
.map_err(|e| {
warn!(
@ -274,15 +268,15 @@ where
for s in signature_server {
http_request.headers_mut().insert(
AUTHORIZATION,
format!(
XMatrix::parse(&format!(
"X-Matrix origin=\"{}\",destination=\"{}\",key=\"{}\",sig=\"{}\"",
services().globals.server_name(),
destination,
s.0,
s.1
)
.try_into()
.unwrap(),
))
.expect("When Ruma signs JSON, it produces a valid base64 signature. All other types are valid ServerNames or OwnedKeyId")
.encode(),
);
}
}
@ -342,7 +336,7 @@ where
response.map_err(|e| {
warn!(
"Invalid 200 response from {} on: {} {:?}",
"Invalid 200 response from {} on: {} {}",
&destination, url, e
);
Error::BadServerResponse("Server returned bad 200 response.")
@ -570,13 +564,6 @@ async fn get_srv_destination(delegated_hostname: String) -> (FedDest, Option<Ins
(override_ip.iter().collect(), force_port.unwrap_or(8448)),
);
} 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");
}
@ -589,13 +576,6 @@ async fn get_srv_destination(delegated_hostname: String) -> (FedDest, Option<Ins
(add_port_to_hostname(&delegated_hostname), Some(timestamp))
}
} 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");
(add_port_to_hostname(&delegated_hostname), None)
}
@ -813,78 +793,17 @@ pub fn parse_incoming_pdu(
let (event_id, value) = match gen_event_id_canonical_json(pdu, &room_version_id) {
Ok(t) => t,
Err(e) => {
Err(_) => {
// Event could not be converted to canonical json
return Err(e);
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Could not convert event to canonical json.",
));
}
};
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}`
///
/// Push EDUs and PDUs to this server.
@ -909,11 +828,77 @@ pub async fn send_transaction_message_route(
// let mut auth_cache = EventMap::new();
for pdu in &body.pdus {
let (event_id, result) =
handle_pdu_in_transaction(sender_servername, &pub_key_map, pdu).await;
let value: CanonicalJsonObject = serde_json::from_str(pdu.get()).map_err(|e| {
warn!("Error parsing incoming event {:?}: {:?}", pdu, e);
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 let Some(event_id) = event_id {
resolved_map.insert(event_id.clone(), result.map_err(|e| e.sanitized_error()));
if services().rooms.state.get_room_version(&room_id).is_err() {
debug!("Server is not in room {room_id}");
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);
}
}
}
@ -1082,7 +1067,12 @@ pub async fn send_transaction_message_route(
}
}
Ok(send_transaction_message::v1::Response { pdus: resolved_map })
Ok(send_transaction_message::v1::Response {
pdus: resolved_map
.into_iter()
.map(|(e, r)| (e, r.map_err(|e| e.sanitized_error())))
.collect(),
})
}
/// # `GET /_matrix/federation/v1/event/{eventId}`
@ -1825,7 +1815,7 @@ pub async fn create_invite_route(
let event_id = EventId::parse(format!(
"${}",
ruma::signatures::reference_hash(&signed_event, &body.room_version)
.expect("Event format validated when event was hashed")
.expect("ruma can calculate reference hashes")
))
.expect("ruma's reference hashes are valid event ids");
@ -1894,90 +1884,6 @@ 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}`
///
/// Gets information on all devices of the user.

View file

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

View file

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

View file

@ -6,7 +6,6 @@ use crate::{
SERVICES,
};
use abstraction::{KeyValueDatabaseEngine, KvTree};
use base64::{engine::general_purpose, Engine};
use directories::ProjectDirs;
use lru_cache::LruCache;
@ -425,7 +424,7 @@ impl KeyValueDatabase {
}
// If the database has any data, perform data migrations before starting
let latest_database_version = 16;
let latest_database_version = 13;
if services().users.count()? > 0 {
// MIGRATIONS
@ -942,86 +941,6 @@ impl KeyValueDatabase {
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!(
services().globals.database_version().unwrap(),
latest_database_version

View file

@ -379,14 +379,10 @@ fn routes(config: &Config) -> Router {
.ruma_route(client_server::turn_server_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_auth_route)
.ruma_route(client_server::create_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_auth_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_device_route)
.ruma_route(client_server::update_device_route)
@ -444,8 +440,6 @@ fn routes(config: &Config) -> Router {
.ruma_route(server_server::create_join_event_v2_route)
.ruma_route(server_server::create_invite_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_profile_information_route)
.ruma_route(server_server::get_keys_route)

View file

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

View file

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

View file

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

View file

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

View file

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