Compare commits
32 commits
restricted
...
next
Author | SHA1 | Date | |
---|---|---|---|
|
3c93c81204 | ||
|
6767ca8bc8 | ||
|
f8d7ef04e6 | ||
|
892fb8846a | ||
|
bca8d1f70f | ||
|
65fe6b0ab5 | ||
|
fea85b0894 | ||
|
a7405cddc0 | ||
|
3df21e8257 | ||
|
e4d6202840 | ||
|
c4810a3a08 | ||
|
73d0536cd3 | ||
|
a6797ca0a2 | ||
|
cdd03dfec0 | ||
|
2bab8869d0 | ||
|
cbd3b07ca7 | ||
|
27d6d94355 | ||
|
a3716a7d5a | ||
|
a9c3867287 | ||
|
423b0928d5 | ||
|
44dd21f432 | ||
|
75a0f68349 | ||
|
8abab8c8a0 | ||
|
324e1beabf | ||
|
00c9ef7b56 | ||
|
6455e918be | ||
|
ea3e7045b4 | ||
|
b8a1b4fee5 | ||
|
d95345377b | ||
|
75322af8c7 | ||
|
11187b3fad | ||
|
35ed731a46 |
19 changed files with 668 additions and 235 deletions
40
Cargo.lock
generated
40
Cargo.lock
generated
|
@ -487,7 +487,7 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
|||
|
||||
[[package]]
|
||||
name = "conduit"
|
||||
version = "0.9.0-alpha"
|
||||
version = "0.10.0-alpha"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum 0.7.5",
|
||||
|
@ -1203,9 +1203,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.8.0"
|
||||
version = "1.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
|
||||
checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
|
@ -2232,7 +2232,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "ruma"
|
||||
version = "0.10.1"
|
||||
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2"
|
||||
source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
|
||||
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#fec2152d879a6c6c2bccce984d4b8f424f460cb2"
|
||||
source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
|
||||
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#fec2152d879a6c6c2bccce984d4b8f424f460cb2"
|
||||
source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"assign",
|
||||
|
@ -2288,7 +2288,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "ruma-common"
|
||||
version = "0.13.0"
|
||||
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2"
|
||||
source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
|
||||
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#fec2152d879a6c6c2bccce984d4b8f424f460cb2"
|
||||
source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"indexmap 2.2.6",
|
||||
|
@ -2334,15 +2334,22 @@ dependencies = [
|
|||
"thiserror",
|
||||
"tracing",
|
||||
"url",
|
||||
"web-time",
|
||||
"wildmatch",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruma-federation-api"
|
||||
version = "0.9.0"
|
||||
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2"
|
||||
source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http 1.1.0",
|
||||
"httparse",
|
||||
"js_int",
|
||||
"memchr",
|
||||
"mime",
|
||||
"rand",
|
||||
"ruma-common",
|
||||
"ruma-events",
|
||||
"serde",
|
||||
|
@ -2352,7 +2359,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "ruma-identifiers-validation"
|
||||
version = "0.9.5"
|
||||
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2"
|
||||
source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"thiserror",
|
||||
|
@ -2361,7 +2368,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "ruma-identity-service-api"
|
||||
version = "0.9.0"
|
||||
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2"
|
||||
source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
|
@ -2371,8 +2378,9 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "ruma-macros"
|
||||
version = "0.13.0"
|
||||
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2"
|
||||
source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
|
@ -2386,7 +2394,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "ruma-push-gateway-api"
|
||||
version = "0.9.0"
|
||||
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2"
|
||||
source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
|
@ -2398,7 +2406,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "ruma-server-util"
|
||||
version = "0.3.0"
|
||||
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2"
|
||||
source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
|
||||
dependencies = [
|
||||
"headers",
|
||||
"http 1.1.0",
|
||||
|
@ -2411,7 +2419,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "ruma-signatures"
|
||||
version = "0.15.0"
|
||||
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2"
|
||||
source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"ed25519-dalek",
|
||||
|
@ -2427,7 +2435,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "ruma-state-res"
|
||||
version = "0.11.0"
|
||||
source = "git+https://github.com/ruma/ruma#fec2152d879a6c6c2bccce984d4b8f424f460cb2"
|
||||
source = "git+https://github.com/ruma/ruma#c06af4385e0e30c48a8e9ca3d488da32102d0db9"
|
||||
dependencies = [
|
||||
"itertools",
|
||||
"js_int",
|
||||
|
|
|
@ -16,7 +16,7 @@ license = "Apache-2.0"
|
|||
name = "conduit"
|
||||
readme = "README.md"
|
||||
repository = "https://gitlab.com/famedly/conduit"
|
||||
version = "0.9.0-alpha"
|
||||
version = "0.10.0-alpha"
|
||||
|
||||
# See also `rust-toolchain.toml`
|
||||
rust-version = "1.79.0"
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
> **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:
|
||||
|
||||
- [Global](#global)
|
||||
|
@ -56,7 +58,8 @@ 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` | `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
|
||||
|
|
|
@ -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 in the `global.well_known` table:
|
||||
To configure it, use the following options:
|
||||
| Field | Type | Description | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| `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_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` |
|
||||
|
||||
### Example
|
||||
|
||||
```toml
|
||||
[global.well_known]
|
||||
client = "https://matrix.example.org"
|
||||
server = "matrix.example.org:443"
|
||||
[global]
|
||||
well_known_client = "https://matrix.example.org"
|
||||
well_known_server = "matrix.example.org:443"
|
||||
```
|
||||
|
||||
## Manual
|
||||
|
|
|
@ -64,6 +64,7 @@ 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>
|
||||
```
|
||||
|
||||
|
|
|
@ -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/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
|
||||
|
||||
|
|
|
@ -4,12 +4,21 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use crate::{service::media::FileMeta, services, utils, Error, Result, Ruma};
|
||||
use ruma::api::client::{
|
||||
error::ErrorKind,
|
||||
media::{
|
||||
create_content, get_content, get_content_as_filename, get_content_thumbnail,
|
||||
get_media_config,
|
||||
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,
|
||||
},
|
||||
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;
|
||||
|
@ -18,9 +27,20 @@ const MXC_LENGTH: usize = 32;
|
|||
///
|
||||
/// Returns max upload size.
|
||||
pub async fn get_media_config_route(
|
||||
_body: Ruma<get_media_config::v3::Request>,
|
||||
) -> Result<get_media_config::v3::Response> {
|
||||
Ok(get_media_config::v3::Response {
|
||||
_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 {
|
||||
upload_size: services().globals.max_request_size().into(),
|
||||
})
|
||||
}
|
||||
|
@ -44,10 +64,10 @@ pub async fn create_content_route(
|
|||
.media
|
||||
.create(
|
||||
mxc.clone(),
|
||||
body.filename
|
||||
.as_ref()
|
||||
.map(|filename| "inline; filename=".to_owned() + filename)
|
||||
.as_deref(),
|
||||
Some(
|
||||
ContentDisposition::new(ContentDispositionType::Inline)
|
||||
.with_filename(body.filename.clone()),
|
||||
),
|
||||
body.content_type.as_deref(),
|
||||
&body.file,
|
||||
)
|
||||
|
@ -61,28 +81,67 @@ pub async fn create_content_route(
|
|||
|
||||
pub async fn get_remote_content(
|
||||
mxc: &str,
|
||||
server_name: &ruma::ServerName,
|
||||
server_name: &ServerName,
|
||||
media_id: String,
|
||||
) -> Result<get_content::v3::Response, Error> {
|
||||
let content_response = services()
|
||||
) -> Result<get_content::v1::Response, Error> {
|
||||
let content_response = match services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
server_name,
|
||||
get_content::v3::Request {
|
||||
allow_remote: false,
|
||||
server_name: server_name.to_owned(),
|
||||
media_id,
|
||||
federation_media::get_content::v1::Request {
|
||||
media_id: media_id.clone(),
|
||||
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()
|
||||
.media
|
||||
.create(
|
||||
mxc.to_owned(),
|
||||
content_response.content_disposition.as_deref(),
|
||||
content_response.content_disposition.clone(),
|
||||
content_response.content_type.as_deref(),
|
||||
&content_response.file,
|
||||
)
|
||||
|
@ -97,31 +156,57 @@ pub async fn get_remote_content(
|
|||
///
|
||||
/// - Only allows federation if `allow_remote` is true
|
||||
pub async fn get_content_route(
|
||||
body: Ruma<get_content::v3::Request>,
|
||||
) -> Result<get_content::v3::Response> {
|
||||
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id);
|
||||
body: Ruma<media::get_content::v3::Request>,
|
||||
) -> Result<media::get_content::v3::Response> {
|
||||
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_type,
|
||||
file,
|
||||
}) = services().media.get(mxc.clone()).await?
|
||||
})) = services().media.get(mxc.clone()).await
|
||||
{
|
||||
Ok(get_content::v3::Response {
|
||||
Ok(get_content::v1::Response {
|
||||
file,
|
||||
content_type,
|
||||
content_disposition,
|
||||
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
||||
content_disposition: Some(content_disposition),
|
||||
})
|
||||
} 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 =
|
||||
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_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."))
|
||||
|
@ -134,29 +219,74 @@ pub async fn get_content_route(
|
|||
///
|
||||
/// - Only allows federation if `allow_remote` is true
|
||||
pub async fn get_content_as_filename_route(
|
||||
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);
|
||||
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?;
|
||||
|
||||
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, ..
|
||||
}) = 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,
|
||||
content_type,
|
||||
content_disposition: Some(format!("inline; filename={}", body.filename)),
|
||||
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
||||
content_disposition: Some(
|
||||
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 =
|
||||
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 {
|
||||
content_disposition: Some(format!("inline: filename={}", body.filename)),
|
||||
Ok(get_content_as_filename::v1::Response {
|
||||
content_disposition: Some(
|
||||
ContentDisposition::new(ContentDispositionType::Inline)
|
||||
.with_filename(Some(filename.clone())),
|
||||
),
|
||||
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."))
|
||||
|
@ -169,63 +299,169 @@ pub async fn get_content_as_filename_route(
|
|||
///
|
||||
/// - Only allows federation if `allow_remote` is true
|
||||
pub async fn get_content_thumbnail_route(
|
||||
body: Ruma<get_content_thumbnail::v3::Request>,
|
||||
) -> Result<get_content_thumbnail::v3::Response> {
|
||||
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id);
|
||||
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?;
|
||||
|
||||
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, ..
|
||||
}) = services()
|
||||
})) = services()
|
||||
.media
|
||||
.get_thumbnail(
|
||||
mxc.clone(),
|
||||
body.width
|
||||
width
|
||||
.try_into()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid."))?,
|
||||
body.height
|
||||
height
|
||||
.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 {
|
||||
file,
|
||||
content_type,
|
||||
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()
|
||||
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(
|
||||
&body.server_name,
|
||||
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(),
|
||||
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),
|
||||
allow_redirect: false,
|
||||
animated: body.animated,
|
||||
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()
|
||||
.media
|
||||
.upload_thumbnail(
|
||||
mxc,
|
||||
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,
|
||||
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,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(get_thumbnail_response)
|
||||
Ok(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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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.server_name.clone();
|
||||
let mut servers = body.via.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("ruma can calculate reference hashes")
|
||||
.expect("Event format validated when event was hashed")
|
||||
);
|
||||
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)
|
||||
.expect("ruma can calculate reference hashes")
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Invalid PDU format"))?
|
||||
))
|
||||
.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("ruma can calculate reference hashes")
|
||||
.expect("Event format validated when event was hashed")
|
||||
))
|
||||
.expect("ruma's reference hashes are valid event ids");
|
||||
|
||||
|
|
|
@ -27,7 +27,10 @@ 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)]),
|
||||
unstable_features: BTreeMap::from_iter([
|
||||
("org.matrix.e2e_cross_signing".to_owned(), true),
|
||||
("org.matrix.msc3916.stable".to_owned(), true),
|
||||
]),
|
||||
};
|
||||
|
||||
Ok(resp)
|
||||
|
|
|
@ -2,11 +2,15 @@
|
|||
|
||||
use crate::{
|
||||
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,
|
||||
};
|
||||
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 http::header::AUTHORIZATION;
|
||||
|
||||
|
@ -14,6 +18,9 @@ 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},
|
||||
|
@ -45,7 +52,6 @@ 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,
|
||||
|
@ -204,7 +210,7 @@ where
|
|||
.try_into_http_request::<Vec<u8>>(
|
||||
&actual_destination_str,
|
||||
SendAccessToken::IfRequired(""),
|
||||
&[MatrixVersion::V1_4],
|
||||
&[MatrixVersion::V1_11],
|
||||
)
|
||||
.map_err(|e| {
|
||||
warn!(
|
||||
|
@ -268,15 +274,15 @@ where
|
|||
for s in signature_server {
|
||||
http_request.headers_mut().insert(
|
||||
AUTHORIZATION,
|
||||
XMatrix::parse(&format!(
|
||||
format!(
|
||||
"X-Matrix origin=\"{}\",destination=\"{}\",key=\"{}\",sig=\"{}\"",
|
||||
services().globals.server_name(),
|
||||
destination,
|
||||
s.0,
|
||||
s.1
|
||||
))
|
||||
.expect("When Ruma signs JSON, it produces a valid base64 signature. All other types are valid ServerNames or OwnedKeyId")
|
||||
.encode(),
|
||||
)
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -336,7 +342,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.")
|
||||
|
@ -564,6 +570,13 @@ 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");
|
||||
}
|
||||
|
||||
|
@ -576,6 +589,13 @@ 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)
|
||||
}
|
||||
|
@ -793,17 +813,78 @@ pub fn parse_incoming_pdu(
|
|||
|
||||
let (event_id, value) = match gen_event_id_canonical_json(pdu, &room_version_id) {
|
||||
Ok(t) => t,
|
||||
Err(_) => {
|
||||
Err(e) => {
|
||||
// Event could not be converted to canonical json
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Could not convert event to canonical json.",
|
||||
));
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
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.
|
||||
|
@ -828,77 +909,11 @@ pub async fn send_transaction_message_route(
|
|||
// let mut auth_cache = EventMap::new();
|
||||
|
||||
for pdu in &body.pdus {
|
||||
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",
|
||||
))?;
|
||||
let (event_id, result) =
|
||||
handle_pdu_in_transaction(sender_servername, &pub_key_map, pdu).await;
|
||||
|
||||
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);
|
||||
}
|
||||
if let Some(event_id) = event_id {
|
||||
resolved_map.insert(event_id.clone(), result.map_err(|e| e.sanitized_error()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1067,12 +1082,7 @@ pub async fn send_transaction_message_route(
|
|||
}
|
||||
}
|
||||
|
||||
Ok(send_transaction_message::v1::Response {
|
||||
pdus: resolved_map
|
||||
.into_iter()
|
||||
.map(|(e, r)| (e, r.map_err(|e| e.sanitized_error())))
|
||||
.collect(),
|
||||
})
|
||||
Ok(send_transaction_message::v1::Response { pdus: resolved_map })
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/federation/v1/event/{eventId}`
|
||||
|
@ -1815,7 +1825,7 @@ pub async fn create_invite_route(
|
|||
let event_id = EventId::parse(format!(
|
||||
"${}",
|
||||
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");
|
||||
|
||||
|
@ -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}`
|
||||
///
|
||||
/// Gets information on all devices of the user.
|
||||
|
|
|
@ -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)]
|
||||
#[serde(default, flatten)]
|
||||
pub well_known: WellKnownConfig,
|
||||
#[serde(default = "false_fn")]
|
||||
pub allow_jaeger: bool,
|
||||
|
@ -97,7 +97,9 @@ 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>,
|
||||
}
|
||||
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
@ -8,7 +8,7 @@ impl service::media::Data for KeyValueDatabase {
|
|||
mxc: String,
|
||||
width: u32,
|
||||
height: u32,
|
||||
content_disposition: Option<&str>,
|
||||
content_disposition: &ContentDisposition,
|
||||
content_type: Option<&str>,
|
||||
) -> Result<Vec<u8>> {
|
||||
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(&height.to_be_bytes());
|
||||
key.push(0xff);
|
||||
key.extend_from_slice(
|
||||
content_disposition
|
||||
.as_ref()
|
||||
.map(|f| f.as_bytes())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
key.extend_from_slice(content_disposition.to_string().as_bytes());
|
||||
key.push(0xff);
|
||||
key.extend_from_slice(
|
||||
content_type
|
||||
|
@ -40,7 +35,7 @@ impl service::media::Data for KeyValueDatabase {
|
|||
mxc: String,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<(Option<String>, Option<String>, Vec<u8>)> {
|
||||
) -> Result<(ContentDisposition, Option<String>, Vec<u8>)> {
|
||||
let mut prefix = mxc.as_bytes().to_vec();
|
||||
prefix.push(0xff);
|
||||
prefix.extend_from_slice(&width.to_be_bytes());
|
||||
|
@ -68,15 +63,9 @@ impl service::media::Data for KeyValueDatabase {
|
|||
.next()
|
||||
.ok_or_else(|| Error::bad_database("Media ID in db is invalid."))?;
|
||||
|
||||
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.")
|
||||
})?,
|
||||
)
|
||||
};
|
||||
let content_disposition = content_disposition_bytes.try_into().unwrap_or_else(|_| {
|
||||
ContentDisposition::new(ruma::http_headers::ContentDispositionType::Inline)
|
||||
});
|
||||
Ok((content_disposition, content_type, key))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ use crate::{
|
|||
SERVICES,
|
||||
};
|
||||
use abstraction::{KeyValueDatabaseEngine, KvTree};
|
||||
use base64::{engine::general_purpose, Engine};
|
||||
use directories::ProjectDirs;
|
||||
use lru_cache::LruCache;
|
||||
|
||||
|
@ -424,7 +425,7 @@ impl KeyValueDatabase {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
// MIGRATIONS
|
||||
|
@ -941,6 +942,86 @@ 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
|
||||
|
|
|
@ -57,7 +57,7 @@ async fn main() {
|
|||
))
|
||||
.nested(),
|
||||
)
|
||||
.merge(Env::prefixed("CONDUIT_").global());
|
||||
.merge(Env::prefixed("CONDUIT_").global().split("__"));
|
||||
|
||||
let config = match raw_config.extract::<Config>() {
|
||||
Ok(s) => s,
|
||||
|
@ -379,10 +379,14 @@ 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)
|
||||
|
@ -440,6 +444,8 @@ 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)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use ruma::http_headers::ContentDisposition;
|
||||
|
||||
use crate::Result;
|
||||
|
||||
pub trait Data: Send + Sync {
|
||||
|
@ -6,7 +8,7 @@ pub trait Data: Send + Sync {
|
|||
mxc: String,
|
||||
width: u32,
|
||||
height: u32,
|
||||
content_disposition: Option<&str>,
|
||||
content_disposition: &ContentDisposition,
|
||||
content_type: Option<&str>,
|
||||
) -> Result<Vec<u8>>;
|
||||
|
||||
|
@ -16,5 +18,5 @@ pub trait Data: Send + Sync {
|
|||
mxc: String,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<(Option<String>, Option<String>, Vec<u8>)>;
|
||||
) -> Result<(ContentDisposition, Option<String>, Vec<u8>)>;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ mod data;
|
|||
use std::io::Cursor;
|
||||
|
||||
pub use data::Data;
|
||||
use ruma::http_headers::{ContentDisposition, ContentDispositionType};
|
||||
|
||||
use crate::{services, Result};
|
||||
use image::imageops::FilterType;
|
||||
|
@ -12,7 +13,7 @@ use tokio::{
|
|||
};
|
||||
|
||||
pub struct FileMeta {
|
||||
pub content_disposition: Option<String>,
|
||||
pub content_disposition: ContentDisposition,
|
||||
pub content_type: Option<String>,
|
||||
pub file: Vec<u8>,
|
||||
}
|
||||
|
@ -26,14 +27,17 @@ impl Service {
|
|||
pub async fn create(
|
||||
&self,
|
||||
mxc: String,
|
||||
content_disposition: Option<&str>,
|
||||
content_disposition: Option<ContentDisposition>,
|
||||
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?;
|
||||
|
@ -46,15 +50,18 @@ 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, content_disposition, content_type)?;
|
||||
let key = self.db.create_file_metadata(
|
||||
mxc,
|
||||
width,
|
||||
height,
|
||||
&ContentDisposition::new(ContentDispositionType::Inline),
|
||||
content_type,
|
||||
)?;
|
||||
|
||||
let path = services().globals.get_media_file(&key);
|
||||
let mut f = File::create(path).await?;
|
||||
|
@ -198,7 +205,7 @@ impl Service {
|
|||
mxc,
|
||||
width,
|
||||
height,
|
||||
content_disposition.as_deref(),
|
||||
&content_disposition,
|
||||
content_type.as_deref(),
|
||||
)?;
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::Error;
|
||||
use ruma::{
|
||||
api::client::error::ErrorKind,
|
||||
canonical_json::redact_content_in_place,
|
||||
events::{
|
||||
room::{member::RoomMemberEventContent, redaction::RoomRedactionEventContent},
|
||||
|
@ -443,7 +444,7 @@ pub(crate) fn gen_event_id_canonical_json(
|
|||
"${}",
|
||||
// Anything higher than version3 behaves the same
|
||||
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()
|
||||
.expect("ruma's reference hashes are valid event ids");
|
||||
|
|
|
@ -1477,7 +1477,7 @@ impl Service {
|
|||
let event_id = format!(
|
||||
"${}",
|
||||
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())
|
||||
.expect("ruma's reference hashes are valid event ids");
|
||||
|
|
|
@ -815,7 +815,7 @@ impl Service {
|
|||
pdu.event_id = EventId::parse_arc(format!(
|
||||
"${}",
|
||||
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");
|
||||
|
||||
|
|
Loading…
Reference in a new issue