Browse Source

Merge branch 'rate-limiting' into 'next'

Draft: feat: rate limiting

Closes #4

See merge request famedly/conduit!783
merge-requests/783/merge
Matthias Ahouansou 3 days ago
parent
commit
8c3c162c0e
  1. 3
      .cargo/config.toml
  2. 190
      Cargo.lock
  3. 246
      Cargo.toml
  4. 2
      complement/Dockerfile
  5. 39
      conduit-config/Cargo.toml
  6. 23
      conduit-config/src/error.rs
  7. 121
      conduit-config/src/lib.rs
  8. 35
      conduit-config/src/proxy.rs
  9. 909
      conduit-config/src/rate_limiting.rs
  10. 23
      conduit-macros/Cargo.toml
  11. 95
      conduit-macros/src/doc_generators.rs
  12. 17
      conduit-macros/src/lib.rs
  13. 210
      conduit/Cargo.toml
  14. 4
      conduit/src/api/appservice_server.rs
  15. 24
      conduit/src/api/client_server/account.rs
  16. 2
      conduit/src/api/client_server/alias.rs
  17. 2
      conduit/src/api/client_server/appservice.rs
  18. 2
      conduit/src/api/client_server/backup.rs
  19. 2
      conduit/src/api/client_server/capabilities.rs
  20. 2
      conduit/src/api/client_server/config.rs
  21. 2
      conduit/src/api/client_server/context.rs
  22. 10
      conduit/src/api/client_server/device.rs
  23. 8
      conduit/src/api/client_server/directory.rs
  24. 2
      conduit/src/api/client_server/filter.rs
  25. 12
      conduit/src/api/client_server/keys.rs
  26. 352
      conduit/src/api/client_server/media.rs
  27. 20
      conduit/src/api/client_server/membership.rs
  28. 3
      conduit/src/api/client_server/message.rs
  29. 0
      conduit/src/api/client_server/mod.rs
  30. 2
      conduit/src/api/client_server/openid.rs
  31. 2
      conduit/src/api/client_server/presence.rs
  32. 4
      conduit/src/api/client_server/profile.rs
  33. 4
      conduit/src/api/client_server/push.rs
  34. 10
      conduit/src/api/client_server/read_marker.rs
  35. 4
      conduit/src/api/client_server/redact.rs
  36. 2
      conduit/src/api/client_server/relations.rs
  37. 4
      conduit/src/api/client_server/report.rs
  38. 8
      conduit/src/api/client_server/room.rs
  39. 4
      conduit/src/api/client_server/search.rs
  40. 4
      conduit/src/api/client_server/session.rs
  41. 4
      conduit/src/api/client_server/space.rs
  42. 6
      conduit/src/api/client_server/state.rs
  43. 19
      conduit/src/api/client_server/sync.rs
  44. 4
      conduit/src/api/client_server/tag.rs
  45. 0
      conduit/src/api/client_server/thirdparty.rs
  46. 2
      conduit/src/api/client_server/threads.rs
  47. 2
      conduit/src/api/client_server/to_device.rs
  48. 2
      conduit/src/api/client_server/typing.rs
  49. 0
      conduit/src/api/client_server/unversioned.rs
  50. 4
      conduit/src/api/client_server/user_directory.rs
  51. 7
      conduit/src/api/client_server/voip.rs
  52. 2
      conduit/src/api/client_server/well_known.rs
  53. 0
      conduit/src/api/mod.rs
  54. 452
      conduit/src/api/ruma_wrapper/axum.rs
  55. 9
      conduit/src/api/ruma_wrapper/mod.rs
  56. 55
      conduit/src/api/server_server.rs
  57. 2
      conduit/src/clap.rs
  58. 3
      conduit/src/database/abstraction.rs
  59. 5
      conduit/src/database/abstraction/rocksdb.rs
  60. 5
      conduit/src/database/abstraction/sqlite.rs
  61. 2
      conduit/src/database/abstraction/watchers.rs
  62. 4
      conduit/src/database/key_value/account_data.rs
  63. 2
      conduit/src/database/key_value/appservice.rs
  64. 7
      conduit/src/database/key_value/globals.rs
  65. 4
      conduit/src/database/key_value/key_backups.rs
  66. 67
      conduit/src/database/key_value/media.rs
  67. 0
      conduit/src/database/key_value/mod.rs
  68. 4
      conduit/src/database/key_value/pusher.rs
  69. 6
      conduit/src/database/key_value/rooms/alias.rs
  70. 2
      conduit/src/database/key_value/rooms/auth_chain.rs
  71. 2
      conduit/src/database/key_value/rooms/directory.rs
  72. 0
      conduit/src/database/key_value/rooms/edus/mod.rs
  73. 4
      conduit/src/database/key_value/rooms/edus/presence.rs
  74. 4
      conduit/src/database/key_value/rooms/edus/read_receipt.rs
  75. 2
      conduit/src/database/key_value/rooms/lazy_load.rs
  76. 2
      conduit/src/database/key_value/rooms/metadata.rs
  77. 0
      conduit/src/database/key_value/rooms/mod.rs
  78. 2
      conduit/src/database/key_value/rooms/outlier.rs
  79. 3
      conduit/src/database/key_value/rooms/pdu_metadata.rs
  80. 2
      conduit/src/database/key_value/rooms/search.rs
  81. 4
      conduit/src/database/key_value/rooms/short.rs
  82. 2
      conduit/src/database/key_value/rooms/state.rs
  83. 4
      conduit/src/database/key_value/rooms/state_accessor.rs
  84. 7
      conduit/src/database/key_value/rooms/state_cache.rs
  85. 3
      conduit/src/database/key_value/rooms/state_compressor.rs
  86. 4
      conduit/src/database/key_value/rooms/threads.rs
  87. 10
      conduit/src/database/key_value/rooms/timeline.rs
  88. 4
      conduit/src/database/key_value/rooms/user.rs
  89. 3
      conduit/src/database/key_value/sending.rs
  90. 2
      conduit/src/database/key_value/transaction_ids.rs
  91. 4
      conduit/src/database/key_value/uiaa.rs
  92. 7
      conduit/src/database/key_value/users.rs
  93. 94
      conduit/src/database/mod.rs
  94. 5
      conduit/src/lib.rs
  95. 59
      conduit/src/main.rs
  96. 2
      conduit/src/service/account_data/data.rs
  97. 2
      conduit/src/service/account_data/mod.rs
  98. 37
      conduit/src/service/admin/mod.rs
  99. 0
      conduit/src/service/appservice/data.rs
  100. 4
      conduit/src/service/appservice/mod.rs
  101. Some files were not shown because too many files have changed in this diff Show More

3
.cargo/config.toml

@ -1,2 +1,5 @@
[env]
RUMA_UNSTABLE_EXHAUSTIVE_TYPES = "1"
[alias]
xtask = "run --package xtask --"

190
Cargo.lock generated

@ -47,12 +47,56 @@ dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
]
[[package]]
name = "anyhow"
version = "1.0.86"
@ -171,6 +215,7 @@ dependencies = [
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper 1.0.2",
"tokio",
"tower 0.5.2",
"tower-layer",
"tower-service",
@ -441,7 +486,7 @@ dependencies = [
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
"windows-link 0.1.1",
]
[[package]]
@ -471,6 +516,7 @@ version = "4.5.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
]
@ -508,6 +554,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "conduit"
version = "0.11.0-alpha"
@ -521,7 +573,7 @@ dependencies = [
"bytesize",
"chrono",
"clap",
"directories",
"conduit-config",
"figment",
"futures-util",
"hex",
@ -530,7 +582,6 @@ dependencies = [
"http",
"http-body-util",
"humantime",
"humantime-serde",
"hyper",
"hyper-util",
"image",
@ -572,9 +623,32 @@ dependencies = [
"tracing-flame",
"tracing-opentelemetry",
"tracing-subscriber",
]
[[package]]
name = "conduit-config"
version = "0.11.0-alpha"
dependencies = [
"bytesize",
"conduit-macros",
"humantime-serde",
"reqwest",
"ruma",
"rusty-s3",
"serde",
"thiserror 2.0.12",
"url",
]
[[package]]
name = "conduit-macros"
version = "0.11.0-alpha"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "const-oid"
version = "0.9.6"
@ -737,27 +811,6 @@ dependencies = [
"subtle",
]
[[package]]
name = "directories"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.59.0",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
@ -1577,6 +1630,12 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.12.1"
@ -1726,16 +1785,6 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.9.1",
"libc",
]
[[package]]
name = "libsqlite3-sys"
version = "0.33.0"
@ -2023,6 +2072,12 @@ dependencies = [
"portable-atomic",
]
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "openssl-probe"
version = "0.1.5"
@ -2119,12 +2174,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "overload"
version = "0.1.1"
@ -2320,9 +2369,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.95"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
@ -2387,9 +2436,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.40"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
dependencies = [
"proc-macro2",
]
@ -2468,17 +2517,6 @@ dependencies = [
"bitflags 2.9.1",
]
[[package]]
name = "redox_users"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [
"getrandom 0.2.16",
"libredox",
"thiserror 2.0.12",
]
[[package]]
name = "regex"
version = "1.11.1"
@ -3287,9 +3325,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.104"
version = "2.0.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12"
dependencies = [
"proc-macro2",
"quote",
@ -3653,6 +3691,7 @@ dependencies = [
"futures-util",
"pin-project-lite",
"sync_wrapper 1.0.2",
"tokio",
"tower-layer",
"tower-service",
]
@ -3848,6 +3887,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.8.0"
@ -4072,7 +4117,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [
"windows-implement 0.60.0",
"windows-interface 0.59.1",
"windows-link",
"windows-link 0.1.1",
"windows-result 0.3.2",
"windows-strings 0.4.0",
]
@ -4127,6 +4172,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.2.0"
@ -4142,7 +4193,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
dependencies = [
"windows-link",
"windows-link 0.1.1",
]
[[package]]
@ -4161,7 +4212,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
dependencies = [
"windows-link",
"windows-link 0.1.1",
]
[[package]]
@ -4191,6 +4242,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
@ -4365,6 +4425,16 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]]
name = "xtask"
version = "0.11.0-alpha"
dependencies = [
"clap",
"conduit-config",
"serde",
"serde_json",
]
[[package]]
name = "yansi"
version = "1.0.1"

246
Cargo.toml

@ -1,3 +1,8 @@
[workspace]
default-members = ["conduit"]
members = ["conduit", "conduit-config", "conduit-macros", "xtask"]
resolver = "2"
[workspace.lints.rust]
explicit_outlives_requirements = "warn"
unused_qualifications = "warn"
@ -7,127 +12,18 @@ cloned_instead_of_copied = "warn"
dbg_macro = "warn"
str_to_string = "warn"
[package]
authors = ["timokoesters <timo@koesters.xyz>"]
description = "A Matrix homeserver written in Rust"
edition = "2021"
[workspace.package]
# See also `rust-toolchain.toml`
edition = "2024"
homepage = "https://conduit.rs"
license = "Apache-2.0"
name = "conduit"
readme = "README.md"
repository = "https://gitlab.com/famedly/conduit"
version = "0.11.0-alpha"
# See also `rust-toolchain.toml`
rust-version = "1.85.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lints]
workspace = true
[dependencies]
# Web framework
axum = { version = "0.8", default-features = false, features = [
"form",
"http1",
"http2",
"json",
"matched-path",
], optional = true }
axum-extra = { version = "0.10", features = ["typed-header"] }
axum-server = { version = "0.7", features = ["tls-rustls"] }
tower = { version = "0.5", features = ["util"] }
tower-http = { version = "0.6", features = [
"add-extension",
"cors",
"sensitive-headers",
"trace",
"util",
] }
tower-service = "0.3"
# Async runtime and utilities
tokio = { version = "1", features = ["fs", "macros", "signal", "sync"] }
# Used for the http request / response body type for Ruma endpoints used with reqwest
bytes = "1"
http = "1"
# Used to find data directory for default db path
directories = "6"
# Used for ruma wrapper
serde_json = { version = "1", features = ["raw_value"] }
# Used for appservice registration files
serde_yaml = "0.9"
# Used for pdu definition
serde = { version = "1", features = ["rc"] }
# Used for secure identifiers
rand = "0.9"
# Used to hash passwords
rust-argon2 = "2"
# Used to send requests
hyper = "1"
hyper-util = { version = "0.1", features = [
"client",
"client-legacy",
"http1",
"http2",
] }
reqwest = { version = "0.12", default-features = false, features = [
"rustls-tls-native-roots",
"socks",
] }
# Used for conduit::Error type
thiserror = "2" #TODO: 2
# Used to generate thumbnails for images
image = { version = "0.25", default-features = false, features = [
"gif",
"jpeg",
"png",
"webp",
] }
# Used for creating media filenames
hex = "0.4"
sha2 = "0.10"
# Used for parsing media retention policies from the config
bytesize = { version = "2", features = ["serde"] }
humantime-serde = "1"
# Used to encode server public key
base64 = "0.22"
# Used when hashing the state
ring = "0.17"
# Used when querying the SRV record of other servers
hickory-resolver = "0.25"
# Used to find matching events for appservices
regex = "1"
# jwt jsonwebtokens
jsonwebtoken = "9"
# Performance measurements
opentelemetry = "0.29"
opentelemetry-jaeger-propagator = "0.29"
opentelemetry-otlp = { version = "0.29", features = ["grpc-tonic"] }
opentelemetry_sdk = { version = "0.29", features = ["rt-tokio"] }
tracing = "0.1"
tracing-flame = "0.2.0"
tracing-opentelemetry = "0.30"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
lru-cache = "0.1.2"
parking_lot = { version = "0.12", optional = true }
rusqlite = { version = "0.35", optional = true, features = ["bundled"] }
rust-version = "1.88.0"
# crossbeam = { version = "0.8.2", optional = true }
num_cpus = "1"
threadpool = "1"
# Used for ruma wrapper
serde_html_form = "0.2"
thread_local = "1"
# used for TURN server authentication
hmac = "0.12"
sha-1 = "0.10"
# used for conduit's CLI and admin room command parsing
chrono = "0.4"
[workspace.dependencies]
bytesize = "2"
# Removing the `color` feature as it is not used in the admin room commands.
# `string` feature is added for some reason, not sure.
# Added `derive` feature to allow derive macros
clap = { version = "4", default-features = false, features = [
"derive",
"error-context",
@ -136,112 +32,14 @@ clap = { version = "4", default-features = false, features = [
"string",
"usage",
] }
humantime = "2"
shell-words = "1.1.0"
futures-util = { version = "0.3", default-features = false }
# Used for reading the configuration from conduit.toml & environment variables
figment = { version = "0.10", features = ["env", "toml"] }
# Validating urls in config
url = { version = "2", features = ["serde"] }
async-trait = "0.1"
tikv-jemallocator = { version = "0.6", features = [
"unprefixed_malloc_on_supported_platforms",
], optional = true }
sd-notify = { version = "0.4", optional = true }
# Used for inspecting request errors
http-body-util = "0.1.3"
# Used for S3 media backend
rusty-s3 = "0.8.1"
# Used for matrix spec type definitions and helpers
[dependencies.ruma]
features = [
"appservice-api-c",
"canonical-json",
"client-api",
"compat-empty-string-null",
"compat-get-3pids",
"compat-null",
"compat-optional",
"compat-optional-txn-pdus",
"compat-server-signing-key-version",
"compat-tag-info",
"compat-unset-avatar",
"federation-api",
"push-gateway-api-c",
"rand",
"ring-compat",
"state-res",
"unstable-msc2448",
"unstable-msc4186",
"unstable-msc4311",
]
git = "https://github.com/ruma/ruma.git"
[dependencies.rocksdb]
features = ["lz4", "multi-threaded-cf", "zstd"]
optional = true
package = "rust-rocksdb"
version = "0.43"
[target.'cfg(unix)'.dependencies]
nix = { version = "0.30", features = ["resource"] }
[features]
backend_rocksdb = ["rocksdb"]
backend_sqlite = ["sqlite"]
conduit_bin = ["axum"]
default = ["backend_rocksdb", "backend_sqlite", "conduit_bin", "systemd"]
jemalloc = ["tikv-jemallocator"]
sqlite = ["parking_lot", "rusqlite", "tokio/signal"]
systemd = ["sd-notify"]
enforce_msc4311 = []
[[bin]]
name = "conduit"
path = "src/main.rs"
required-features = ["conduit_bin"]
[lib]
name = "conduit"
path = "src/lib.rs"
[package.metadata.deb]
assets = [
[
"README.md",
"usr/share/doc/matrix-conduit/",
"644",
],
[
"debian/README.md",
"usr/share/doc/matrix-conduit/README.Debian",
"644",
],
[
"target/release/conduit",
"usr/sbin/matrix-conduit",
"755",
],
]
conf-files = ["/etc/matrix-conduit/conduit.toml"]
copyright = "2020, Timo Kösters <timo@koesters.xyz>"
depends = "$auto, ca-certificates"
extended-description = """\
A fast Matrix homeserver that is optimized for smaller, personal servers, \
instead of a server that has high scalability."""
license-file = ["LICENSE", "3"]
maintainer = "Paul van Tilburg <paul@luon.net>"
maintainer-scripts = "debian/"
name = "matrix-conduit"
priority = "optional"
section = "net"
systemd-units = { unit-name = "matrix-conduit" }
conduit-config.path = "conduit-config"
conduit-macros.path = "conduit-macros"
reqwest = { version = "0.12", default-features = false }
ruma.git = "https://github.com/ruma/ruma.git"
rusty-s3 = "0.8"
serde = "1"
serde_json = "1"
thiserror = "2"
[profile.dev]
incremental = true

2
complement/Dockerfile

@ -1,4 +1,4 @@
FROM rust:1.85.0
FROM rust:1.88.0
WORKDIR /workdir

39
conduit-config/Cargo.toml

@ -0,0 +1,39 @@
[package]
edition.workspace = true
homepage.workspace = true
name = "conduit-config"
repository.workspace = true
rust-version.workspace = true
version = "0.11.0-alpha"
[dependencies]
# Self explanitory, used for deserializing the configuration
serde.workspace = true
# Parsing media retention policies
bytesize = { workspace = true, features = ["serde"] }
humantime-serde = "1"
# Validating urls
url = { version = "2", features = ["serde"] }
# Error type
thiserror.workspace = true
# Validating S3 config
rusty-s3.workspace = true
# Proxy config
reqwest.workspace = true
# Generating documentation
conduit-macros.workspace = true
# default room version, server name, ignored keys
[dependencies.ruma]
features = ["client-api", "federation-api"]
workspace = true
[features]
rocksdb = []
sqlite = []
# Used to generate docs, shouldn't be used outside of xtask
doc-generators = ["conduit-macros/doc-generators"]
[lints]
workspace = true

23
conduit-config/src/error.rs

@ -0,0 +1,23 @@
use thiserror::Error;
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Error, Debug)]
pub enum Error {
#[error(
"The media directory structure depth multiplied by the length is equal to or greater than a sha256 hex hash, please reduce at least one of the two so that their product is less than 64"
)]
DirectoryStructureLengthDepthTooLarge,
#[error("Invalid S3 config")]
S3,
#[error("Failed to construct proxy config: {source}")]
Proxy {
#[from]
source: reqwest::Error,
},
#[error("Registration token is empty")]
EmptyRegistrationToken,
}

121
src/config/mod.rs → conduit-config/src/lib.rs

@ -1,6 +1,5 @@
use std::{
collections::{BTreeMap, HashMap, HashSet},
fmt,
net::{IpAddr, Ipv4Addr},
num::NonZeroU8,
path::PathBuf,
@ -8,16 +7,19 @@ use std::{
};
use bytesize::ByteSize;
use ruma::{api::federation::discovery::VerifyKey, serde::Base64, OwnedServerName, RoomVersionId};
use serde::{de::IgnoredAny, Deserialize};
use tokio::time::{interval, Interval};
use tracing::warn;
pub use error::Error;
use ruma::{OwnedServerName, RoomVersionId, api::federation::discovery::VerifyKey, serde::Base64};
use serde::{
Deserialize,
de::{Error as _, IgnoredAny},
};
use url::Url;
use crate::Error;
pub mod error;
mod proxy;
use self::proxy::ProxyConfig;
pub mod rate_limiting;
use self::{proxy::ProxyConfig, rate_limiting::Config as RateLimitingConfig};
const SHA256_HEX_LENGTH: u8 = 64;
@ -30,7 +32,7 @@ pub struct IncompleteConfig {
pub tls: Option<TlsConfig>,
pub server_name: OwnedServerName,
pub database_backend: String,
pub database_backend: DatabaseBackend,
pub database_path: String,
#[serde(default = "default_db_cache_capacity_mb")]
pub db_cache_capacity_mb: f64,
@ -54,6 +56,7 @@ pub struct IncompleteConfig {
pub max_fetch_prev_events: u16,
#[serde(default = "false_fn")]
pub allow_registration: bool,
#[serde(default, deserialize_with = "forbid_empty_registration_token")]
pub registration_token: Option<String>,
#[serde(default = "default_openid_token_ttl")]
pub openid_token_ttl: u64,
@ -80,6 +83,8 @@ pub struct IncompleteConfig {
pub trusted_servers: Vec<OwnedServerName>,
#[serde(default = "default_log")]
pub log: String,
#[serde(default)]
pub ip_address_detection: IpAddrDetection,
pub turn_username: Option<String>,
pub turn_password: Option<String>,
pub turn_uris: Option<Vec<String>>,
@ -95,6 +100,9 @@ pub struct IncompleteConfig {
#[serde(default)]
pub media: IncompleteMediaConfig,
#[serde(default)]
pub rate_limiting: RateLimitingConfig,
pub emergency_password: Option<String>,
#[serde(flatten)]
@ -109,7 +117,7 @@ pub struct Config {
pub tls: Option<TlsConfig>,
pub server_name: OwnedServerName,
pub database_backend: String,
pub database_backend: DatabaseBackend,
pub database_path: String,
pub db_cache_capacity_mb: f64,
pub enable_lightning_bolt: bool,
@ -136,6 +144,7 @@ pub struct Config {
pub jwt_secret: Option<String>,
pub trusted_servers: Vec<OwnedServerName>,
pub log: String,
pub ip_address_detection: IpAddrDetection,
pub turn: Option<TurnConfig>,
@ -143,6 +152,8 @@ pub struct Config {
pub media: MediaConfig,
pub rate_limiting: RateLimitingConfig,
pub emergency_password: Option<String>,
pub catchall: BTreeMap<String, IgnoredAny>,
@ -182,6 +193,7 @@ impl From<IncompleteConfig> for Config {
jwt_secret,
trusted_servers,
log,
ip_address_detection,
turn_username,
turn_password,
turn_uris,
@ -189,6 +201,7 @@ impl From<IncompleteConfig> for Config {
turn_ttl,
turn,
media,
rate_limiting,
emergency_password,
catchall,
ignored_keys,
@ -287,8 +300,10 @@ impl From<IncompleteConfig> for Config {
jwt_secret,
trusted_servers,
log,
ip_address_detection,
turn,
media,
rate_limiting,
emergency_password,
catchall,
ignored_keys,
@ -296,6 +311,43 @@ impl From<IncompleteConfig> for Config {
}
}
fn forbid_empty_registration_token<'de, D>(de: D) -> Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
let opt = Option::<String>::deserialize(de)?;
if opt
.as_ref()
.map(|token| token.is_empty())
.unwrap_or_default()
{
return Err(D::Error::custom(Error::EmptyRegistrationToken));
}
Ok(opt)
}
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum DatabaseBackend {
#[cfg(feature = "sqlite")]
SQLite,
#[cfg(feature = "rocksdb")]
RocksDB,
}
#[cfg(any(feature = "sqlite", feature = "rocksdb"))]
impl std::fmt::Display for DatabaseBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let string = match self {
#[cfg(feature = "rocksdb")]
DatabaseBackend::RocksDB => "RocksDB",
#[cfg(feature = "sqlite")]
DatabaseBackend::SQLite => "SQLite",
};
write!(f, "{string}")
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct TlsConfig {
pub certs: String,
@ -356,7 +408,7 @@ pub struct MediaRetentionConfig {
impl MediaRetentionConfig {
/// Interval for the duration-based retention policies to be checked & enforced
pub fn cleanup_interval(&self) -> Option<Interval> {
pub fn cleanup_interval(&self) -> Option<Duration> {
self.scoped
.values()
.filter_map(|scoped| match (scoped.created, scoped.accessed) {
@ -369,7 +421,6 @@ impl MediaRetentionConfig {
.max(Duration::from_secs(60).min(Duration::from_secs(60 * 60 * 24)))
})
.min()
.map(interval)
}
}
@ -559,7 +610,7 @@ impl TryFrom<ShadowDirectoryStructure> for DirectoryStructure {
{
Ok(Self::Deep { length, depth })
} else {
Err(Error::bad_config("The media directory structure depth multiplied by the depth is equal to or greater than a sha256 hex hash, please reduce at least one of the two so that their product is less than 64"))
Err(Error::DirectoryStructureLengthDepthTooLarge)
}
}
}
@ -603,7 +654,7 @@ impl TryFrom<ShadowS3MediaBackend> for S3MediaBackend {
path: value.path,
directory_structure: value.directory_structure,
}),
Err(_) => Err(Error::bad_config("Invalid S3 config")),
Err(_) => Err(Error::S3),
}
}
}
@ -618,39 +669,27 @@ pub struct S3MediaBackend {
pub directory_structure: DirectoryStructure,
}
const DEPRECATED_KEYS: &[&str] = &[
"cache_capacity",
"turn_username",
"turn_password",
"turn_uris",
"turn_secret",
"turn_ttl",
];
impl Config {
pub fn warn_deprecated(&self) {
let mut was_deprecated = false;
for key in self
.catchall
.keys()
.filter(|key| DEPRECATED_KEYS.iter().any(|s| s == key))
{
warn!("Config parameter {} is deprecated", key);
was_deprecated = true;
}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub enum IpAddrDetection {
SocketAddress,
Header(String),
}
if was_deprecated {
warn!("Read conduit documentation and check your configuration if any new configuration parameters should be adjusted");
}
impl Default for IpAddrDetection {
fn default() -> Self {
Self::Header("X-Forwarded-For".to_owned())
}
}
impl fmt::Display for Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
#[cfg(any(feature = "sqlite", feature = "rocksdb"))]
impl std::fmt::Display for Config {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Prepare a list of config values to show
// TODO: Replace this with something more fit-for-purpose, especially with tables in mind.
let lines = [
("Server name", self.server_name.host()),
("Database backend", &self.database_backend),
("Database backend", &self.database_backend.to_string()),
("Database path", &self.database_path),
(
"Database cache capacity (MB)",
@ -716,7 +755,7 @@ impl fmt::Display for Config {
let mut msg: String = "Active config values:\n\n".to_owned();
for line in lines.into_iter().enumerate() {
msg += &format!("{}: {}\n", line.1 .0, line.1 .1);
msg += &format!("{}: {}\n", line.1.0, line.1.1);
}
write!(f, "{msg}")

35
src/config/proxy.rs → conduit-config/src/proxy.rs

@ -1,7 +1,9 @@
use std::{fmt, str::FromStr};
use reqwest::{Proxy, Url};
use serde::Deserialize;
use crate::Result;
use crate::error::Result;
/// ## Examples:
/// - No proxy (default):
@ -34,7 +36,6 @@ pub enum ProxyConfig {
#[default]
None,
Global {
#[serde(deserialize_with = "crate::utils::deserialize_from_str")]
url: Url,
},
ByDomain(Vec<PartialProxyConfig>),
@ -53,7 +54,6 @@ impl ProxyConfig {
#[derive(Clone, Debug, Deserialize)]
pub struct PartialProxyConfig {
#[serde(deserialize_with = "crate::utils::deserialize_from_str")]
url: Url,
#[serde(default)]
include: Vec<WildCardedDomain>,
@ -120,7 +120,8 @@ impl WildCardedDomain {
}
}
}
impl std::str::FromStr for WildCardedDomain {
impl FromStr for WildCardedDomain {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
// maybe do some domain validation?
@ -138,6 +139,30 @@ impl<'de> Deserialize<'de> for WildCardedDomain {
where
D: serde::de::Deserializer<'de>,
{
crate::utils::deserialize_from_str(deserializer)
deserialize_from_str(deserializer)
}
}
fn deserialize_from_str<
'de,
D: serde::de::Deserializer<'de>,
T: FromStr<Err = E>,
E: fmt::Display,
>(
deserializer: D,
) -> Result<T, D::Error> {
struct Visitor<T: FromStr<Err = E>, E>(std::marker::PhantomData<T>);
impl<T: FromStr<Err = Err>, Err: fmt::Display> serde::de::Visitor<'_> for Visitor<T, Err> {
type Value = T;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "a parsable string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
v.parse().map_err(serde::de::Error::custom)
}
}
deserializer.deserialize_str(Visitor(std::marker::PhantomData))
}

909
conduit-config/src/rate_limiting.rs

@ -0,0 +1,909 @@
use std::{collections::HashMap, num::NonZeroU64};
use bytesize::ByteSize;
use ruma::api::Metadata;
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct WrappedShadowConfig {
#[serde(default)]
pub inherits: ConfigPreset,
#[serde(flatten)]
pub config: ShadowConfig,
}
impl From<WrappedShadowConfig> for Config {
fn from(value: WrappedShadowConfig) -> Self {
Config::get_preset(value.inherits).apply_overrides(value.config)
}
}
#[derive(Debug, Clone, Deserialize, Default, Copy)]
#[cfg_attr(feature = "doc-generators", derive(serde::Serialize))]
#[serde(rename_all = "snake_case")]
pub enum ConfigPreset {
/// Default rate-limiting configuration, recommended for small private servers (i.e. single-user
/// or for family and/or friends)
#[default]
PrivateSmall,
PrivateMedium,
PublicMedium,
PublicLarge,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ShadowConfig {
pub client:
ShadowConfigFragment<ClientRestriction, ShadowClientMediaConfig, AuthenticationFailures>,
pub federation:
ShadowConfigFragment<FederationRestriction, ShadowFederationMediaConfig, Nothing>,
}
pub trait RestrictionGeneric: ConfigPart + std::hash::Hash + Eq {}
impl<T> RestrictionGeneric for T where T: ConfigPart + std::hash::Hash + Eq {}
pub trait ConfigPart: Clone + std::fmt::Debug + serde::de::DeserializeOwned {}
impl<T> ConfigPart for T where T: Clone + std::fmt::Debug + serde::de::DeserializeOwned {}
#[derive(Debug, Clone, Deserialize)]
pub struct ShadowConfigFragment<R, M, T>
where
R: RestrictionGeneric,
M: ConfigPart,
T: ConfigPart,
{
#[serde(bound(deserialize = "R: RestrictionGeneric, M: ConfigPart, T: ConfigPart"))]
pub target: Option<ShadowConfigFragmentFragment<R, M, T>>,
#[serde(bound(deserialize = "R: RestrictionGeneric, M: ConfigPart"))]
pub global: Option<ShadowConfigFragmentFragment<R, M, Nothing>>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ShadowConfigFragmentFragment<R, M, T>
where
R: RestrictionGeneric,
M: ConfigPart,
T: ConfigPart,
{
#[serde(
flatten,
// https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=fe75063b73c6d9860991c41572e00035
//
// For some reason specifying the default function fixes the issue in the playground link
// above. Makes no sense to me, but hey, it works.
default = "HashMap::new",
bound(deserialize = "R: RestrictionGeneric")
)]
pub map: HashMap<R, RequestLimitation>,
#[serde(bound(deserialize = "M: ConfigPart"))]
pub media: Option<M>,
#[serde(flatten)]
#[serde(bound(deserialize = "T: ConfigPart"))]
pub additional_fields: Option<T>,
}
#[derive(Clone, Copy, Debug, Deserialize)]
pub struct ShadowClientMediaConfig {
pub download: Option<MediaLimitation>,
pub upload: Option<MediaLimitation>,
pub fetch: Option<MediaLimitation>,
}
#[derive(Clone, Copy, Debug, Deserialize)]
pub struct ShadowFederationMediaConfig {
pub download: Option<MediaLimitation>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(from = "WrappedShadowConfig")]
pub struct Config {
pub target: ConfigFragment<AuthenticationFailures>,
pub global: ConfigFragment<Nothing>,
}
impl Default for Config {
fn default() -> Self {
Self::get_preset(ConfigPreset::default())
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ConfigFragment<T>
where
T: ConfigPart,
{
#[serde(bound(deserialize = "T: ConfigPart"))]
pub client: ConfigFragmentFragment<ClientRestriction, ClientMediaConfig, T>,
pub federation: ConfigFragmentFragment<FederationRestriction, FederationMediaConfig, Nothing>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ConfigFragmentFragment<R, M, T>
where
R: RestrictionGeneric,
M: ConfigPart,
T: ConfigPart,
{
#[serde(flatten)]
#[serde(bound(deserialize = "R: RestrictionGeneric"))]
pub map: HashMap<R, RequestLimitation>,
#[serde(bound(deserialize = "M: ConfigPart"))]
pub media: M,
#[serde(flatten)]
#[serde(bound(deserialize = "T: ConfigPart"))]
pub additional_fields: T,
}
impl<R, M, T> ConfigFragmentFragment<R, M, T>
where
R: RestrictionGeneric,
M: ConfigPart + MediaConfig,
T: ConfigPart,
{
pub fn apply_overrides(
self,
shadow: Option<ShadowConfigFragmentFragment<R, M::Shadow, T>>,
) -> Self {
let Some(shadow) = shadow else {
return self;
};
let ConfigFragmentFragment {
mut map,
media,
additional_fields,
} = self;
map.extend(shadow.map);
Self {
map,
media: if let Some(sm) = shadow.media {
media.apply_overrides(sm)
} else {
media
},
additional_fields: shadow.additional_fields.unwrap_or(additional_fields),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct AuthenticationFailures {
pub authentication_failures: RequestLimitation,
}
impl AuthenticationFailures {
fn new(timeframe: Timeframe, burst_capacity: NonZeroU64) -> Self {
Self {
authentication_failures: RequestLimitation::new(timeframe, burst_capacity),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct Nothing;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Restriction {
Client(ClientRestriction),
Federation(FederationRestriction),
}
impl From<ClientRestriction> for Restriction {
fn from(value: ClientRestriction) -> Self {
Self::Client(value)
}
}
impl From<FederationRestriction> for Restriction {
fn from(value: FederationRestriction) -> Self {
Self::Federation(value)
}
}
#[cfg(feature = "doc-generators")]
pub trait DocumentRestrictions: Sized {
fn variant_doc_comments() -> Vec<(Self, String)>;
fn container_doc_comment() -> String;
}
/// Applies for endpoints on the client-server API, which are used by clients, appservices, and
/// bots. Appservices can bypass rate-limiting though if `rate_limited` is set to `false` in their
/// registration file.
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[cfg_attr(
feature = "doc-generators",
derive(conduit_macros::DocumentRestrictions, serde::Serialize)
)]
#[serde(rename_all = "snake_case")]
pub enum ClientRestriction {
/// For registering a new user account. May be called multiples times for a single
/// registration if there are extra steps, e.g. providing a registration token.
Registration,
/// For logging into an existing account.
Login,
/// For checking whether a given registration token would allow the user to register an
/// account.
RegistrationTokenValidity,
/// For sending an event to a room.
///
/// Note that this is not used for state events, but for users who are unprivliged in a room,
/// the only state event they'll be able to send are ones to update their room profile.
SendEvent,
/// For joining a room.
Join,
/// For inviting a user to a room.
Invite,
/// For knocking on a room.
Knock,
/// For reporting a user, event, or room.
SendReport,
/// For adding an alias to a room.
CreateAlias,
/// For downloading a media file.
///
/// For rate-limiting based on the size of files downloaded, see the media rate-limiting
/// configuration.
MediaDownload,
/// For uploading a media file.
///
/// For rate-limiting based on the size of files uploaded, see the media rate-limiting
/// configuration.
MediaCreate,
}
/// Applies for endpoints on the federation API of this server, hence restricting how
/// many times other servers can use these endpoints on this server in a given timeframe.
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[cfg_attr(
feature = "doc-generators",
derive(conduit_macros::DocumentRestrictions, serde::Serialize)
)]
#[serde(rename_all = "snake_case")]
pub enum FederationRestriction {
/// For joining a room.
Join,
/// For knocking on a room.
Knock,
/// For inviting a local user to a room.
Invite,
// Transactions should be handled by a completely dedicated rate-limiter
/* /// For sending transactions of PDU/EDUs.
///
///
Transaction, */
/// For downloading media.
MediaDownload,
}
impl TryFrom<Metadata> for Restriction {
type Error = ();
fn try_from(value: Metadata) -> Result<Self, Self::Error> {
use Restriction::*;
use ruma::api::{
IncomingRequest,
client::{
account::{check_registration_token_validity, register},
alias::create_alias,
authenticated_media::{
get_content, get_content_as_filename, get_content_thumbnail, get_media_preview,
},
knock::knock_room,
media::{self, create_content, create_mxc_uri},
membership::{invite_user, join_room_by_id, join_room_by_id_or_alias},
message::send_message_event,
reporting::report_user,
room::{report_content, report_room},
session::login,
state::send_state_event,
},
federation::{
authenticated_media::{
get_content as federation_get_content,
get_content_thumbnail as federation_get_content_thumbnail,
},
membership::{create_invite, create_join_event, create_knock_event},
},
};
Ok(match value {
register::v3::Request::METADATA => Client(ClientRestriction::Registration),
check_registration_token_validity::v1::Request::METADATA => {
Client(ClientRestriction::RegistrationTokenValidity)
}
login::v3::Request::METADATA => Client(ClientRestriction::Login),
send_message_event::v3::Request::METADATA | send_state_event::v3::Request::METADATA => {
Client(ClientRestriction::SendEvent)
}
join_room_by_id::v3::Request::METADATA
| join_room_by_id_or_alias::v3::Request::METADATA => Client(ClientRestriction::Join),
invite_user::v3::Request::METADATA => Client(ClientRestriction::Invite),
knock_room::v3::Request::METADATA => Client(ClientRestriction::Knock),
report_user::v3::Request::METADATA
| report_content::v3::Request::METADATA
| report_room::v3::Request::METADATA => Client(ClientRestriction::SendReport),
create_alias::v3::Request::METADATA => Client(ClientRestriction::CreateAlias),
// NOTE: handle async media upload in a way that doesn't half the number of uploads you can do within a short timeframe, while not allowing pre-generation of MXC uris to allow uploading double the number of media at once
create_content::v3::Request::METADATA | create_mxc_uri::v1::Request::METADATA => {
Client(ClientRestriction::MediaCreate)
}
// Unauthenticate media is deprecated
#[allow(deprecated)]
media::get_content::v3::Request::METADATA
| media::get_content_as_filename::v3::Request::METADATA
| media::get_content_thumbnail::v3::Request::METADATA
| media::get_media_preview::v3::Request::METADATA
| get_content::v1::Request::METADATA
| get_content_as_filename::v1::Request::METADATA
| get_content_thumbnail::v1::Request::METADATA
| get_media_preview::v1::Request::METADATA => Client(ClientRestriction::MediaDownload),
federation_get_content::v1::Request::METADATA
| federation_get_content_thumbnail::v1::Request::METADATA => {
Federation(FederationRestriction::MediaDownload)
}
// v1 is deprecated
#[allow(deprecated)]
create_join_event::v1::Request::METADATA | create_join_event::v2::Request::METADATA => {
Federation(FederationRestriction::Join)
}
create_knock_event::v1::Request::METADATA => Federation(FederationRestriction::Knock),
create_invite::v1::Request::METADATA | create_invite::v2::Request::METADATA => {
Federation(FederationRestriction::Invite)
}
_ => return Err(()),
})
}
}
impl<T> ConfigFragment<T>
where
T: ConfigPart,
{
pub fn get(&self, restriction: &Restriction) -> &RequestLimitation {
// Maybe look into https://github.com/moriyoshi-kasuga/enum-table
match restriction {
Restriction::Client(client_restriction) => {
self.client.map.get(client_restriction).unwrap()
}
Restriction::Federation(federation_restriction) => {
self.federation.map.get(federation_restriction).unwrap()
}
}
}
}
#[derive(Clone, Copy, Debug, Deserialize)]
pub struct RequestLimitation {
#[serde(flatten)]
pub timeframe: Timeframe,
pub burst_capacity: NonZeroU64,
}
impl RequestLimitation {
pub fn new(timeframe: Timeframe, burst_capacity: NonZeroU64) -> Self {
Self {
timeframe,
burst_capacity,
}
}
}
#[derive(Deserialize, Clone, Copy, Debug)]
#[serde(rename_all = "snake_case")]
// When deserializing, we want this prefix
#[allow(clippy::enum_variant_names)]
pub enum Timeframe {
PerSecond(NonZeroU64),
PerMinute(NonZeroU64),
PerHour(NonZeroU64),
PerDay(NonZeroU64),
}
#[cfg(feature = "doc-generators")]
impl std::fmt::Display for Timeframe {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let value;
let string = match self {
Self::PerSecond(v) => {
value = v;
"second"
}
Self::PerMinute(v) => {
value = v;
"minute"
}
Self::PerHour(v) => {
value = v;
"hour"
}
Self::PerDay(v) => {
value = v;
"day"
}
};
write!(f, "{value} requests per {string}")
}
}
impl Timeframe {
pub fn nano_gap(&self) -> u64 {
match self {
Timeframe::PerSecond(t) => 1000 * 1000 * 1000 / t.get(),
Timeframe::PerMinute(t) => 1000 * 1000 * 1000 * 60 / t.get(),
Timeframe::PerHour(t) => 1000 * 1000 * 1000 * 60 * 60 / t.get(),
Timeframe::PerDay(t) => 1000 * 1000 * 1000 * 60 * 60 * 24 / t.get(),
}
}
}
pub trait MediaConfig {
type Shadow: ConfigPart;
fn apply_overrides(self, shadow: Self::Shadow) -> Self;
}
#[derive(Clone, Copy, Debug, Deserialize)]
pub struct ClientMediaConfig {
pub download: MediaLimitation,
pub upload: MediaLimitation,
pub fetch: MediaLimitation,
}
impl MediaConfig for ClientMediaConfig {
type Shadow = ShadowClientMediaConfig;
fn apply_overrides(self, shadow: Self::Shadow) -> Self {
let Self::Shadow {
download,
upload,
fetch,
} = shadow;
Self {
download: download.unwrap_or(self.download),
upload: upload.unwrap_or(self.upload),
fetch: fetch.unwrap_or(self.fetch),
}
}
}
#[derive(Clone, Copy, Debug, Deserialize)]
pub struct FederationMediaConfig {
pub download: MediaLimitation,
}
impl MediaConfig for FederationMediaConfig {
type Shadow = ShadowFederationMediaConfig;
fn apply_overrides(self, shadow: Self::Shadow) -> Self {
Self {
download: shadow.download.unwrap_or(self.download),
}
}
}
#[derive(Clone, Copy, Debug, Deserialize)]
pub struct MediaLimitation {
#[serde(flatten)]
pub timeframe: MediaTimeframe,
pub burst_capacity: ByteSize,
}
impl MediaLimitation {
pub fn new(timeframe: MediaTimeframe, burst_capacity: ByteSize) -> Self {
Self {
timeframe,
burst_capacity,
}
}
}
#[derive(Deserialize, Clone, Copy, Debug)]
#[serde(rename_all = "snake_case")]
// When deserializing, we want this prefix
#[allow(clippy::enum_variant_names)]
pub enum MediaTimeframe {
PerSecond(ByteSize),
PerMinute(ByteSize),
PerHour(ByteSize),
PerDay(ByteSize),
}
impl MediaTimeframe {
pub fn bytes_per_sec(&self) -> u64 {
match self {
MediaTimeframe::PerSecond(t) => t.as_u64(),
MediaTimeframe::PerMinute(t) => t.as_u64() / 60,
MediaTimeframe::PerHour(t) => t.as_u64() / (60 * 60),
MediaTimeframe::PerDay(t) => t.as_u64() / (60 * 60 * 24),
}
}
}
fn nz(int: u64) -> NonZeroU64 {
NonZeroU64::new(int).expect("Values are static")
}
macro_rules! default_restriction_map {
($restriction_type:ident; $($restriction:ident, $timeframe:ident, $timeframe_value:expr, $burst_capacity:expr;)*) => {
HashMap::from_iter([
$((
$restriction_type::$restriction,
RequestLimitation::new(Timeframe::$timeframe(nz($timeframe_value)), nz($burst_capacity)),
),)*
])
}
}
macro_rules! media_config {
($config_type:ident; $($key:ident: $timeframe:ident, $timeframe_value:expr, $burst_capacity:expr;)*) => {
$config_type {
$($key: MediaLimitation::new(MediaTimeframe::$timeframe($timeframe_value), $burst_capacity),)*
}
}
}
impl Config {
fn apply_overrides(self, shadow: ShadowConfig) -> Self {
let ShadowConfig {
client:
ShadowConfigFragment {
target: client_target,
global: client_global,
},
federation:
ShadowConfigFragment {
target: federation_target,
global: federation_global,
},
} = shadow;
Self {
target: ConfigFragment {
client: self.target.client.apply_overrides(client_target),
federation: self.target.federation.apply_overrides(federation_target),
},
global: ConfigFragment {
client: self.global.client.apply_overrides(client_global),
federation: self.global.federation.apply_overrides(federation_global),
},
}
}
pub fn get_preset(preset: ConfigPreset) -> Self {
// The client target map shouldn't really differ between presets, as individual user's
// behaviours shouldn't differ depending on the size of the server or whether it's private
// or public, but maybe I'm wrong.
let target_client_map = default_restriction_map!(
ClientRestriction;
Registration, PerDay, 3, 10;
Login, PerDay, 5, 20;
RegistrationTokenValidity, PerDay, 10, 20;
SendEvent, PerMinute, 15, 60;
Join, PerHour, 5, 30;
Knock, PerHour, 5, 30;
Invite, PerHour, 2, 20;
SendReport, PerDay, 5, 20;
CreateAlias, PerDay, 2, 20;
MediaDownload, PerHour, 30, 100;
MediaCreate, PerMinute, 4, 20;
);
// Same goes for media
let target_client_media = media_config! {
ClientMediaConfig;
download: PerMinute, ByteSize::mb(100), ByteSize::mb(50);
upload: PerMinute, ByteSize::mb(10), ByteSize::mb(100);
fetch: PerMinute, ByteSize::mb(100), ByteSize::mb(50);
};
// Currently, these values are completely arbitrary, not informed by any sort of
// knowledge. In the future, it would be good to have some sort of analytics to
// determine what some good defaults could be. Maybe getting some percentiles for
// burst_capacity & timeframes used. How we'd tell the difference between power users
// and malicilous attacks, I'm not sure.
match preset {
ConfigPreset::PrivateSmall => Self {
target: ConfigFragment {
client: ConfigFragmentFragment {
map: target_client_map,
media: target_client_media,
additional_fields: AuthenticationFailures::new(
Timeframe::PerHour(nz(1)),
nz(20),
),
},
federation: ConfigFragmentFragment {
map: default_restriction_map!(
FederationRestriction;
Join, PerHour, 10, 10;
Knock, PerHour, 10, 10;
Invite, PerHour, 10, 10;
MediaDownload, PerMinute, 10, 50;
),
media: media_config! {
FederationMediaConfig;
download: PerMinute, ByteSize::mb(100), ByteSize::mb(100);
},
additional_fields: Nothing,
},
},
global: ConfigFragment {
client: ConfigFragmentFragment {
map: default_restriction_map!(
ClientRestriction;
Registration, PerDay, 10, 20;
Login, PerHour, 10, 10;
RegistrationTokenValidity, PerDay, 10, 20;
SendEvent, PerSecond, 2, 100;
Join, PerMinute, 1, 30;
Knock, PerMinute, 1, 30;
Invite, PerHour, 10, 20;
SendReport, PerHour, 1, 25;
CreateAlias, PerHour, 5, 20;
MediaDownload, PerMinute, 5, 150;
MediaCreate, PerMinute, 20, 50;
),
media: media_config! {
ClientMediaConfig;
download: PerMinute, ByteSize::mb(250), ByteSize::mb(100);
upload: PerMinute, ByteSize::mb(50), ByteSize::mb(100);
fetch: PerMinute, ByteSize::mb(250), ByteSize::mb(100);
},
additional_fields: Nothing,
},
federation: ConfigFragmentFragment {
map: default_restriction_map!(
FederationRestriction;
Join, PerMinute, 10, 10;
Knock, PerMinute, 10, 10;
Invite, PerMinute, 10, 10;
MediaDownload, PerSecond, 10, 250;
),
media: media_config! {
FederationMediaConfig;
download: PerMinute, ByteSize::mb(250), ByteSize::mb(250);
},
additional_fields: Nothing,
},
},
},
ConfigPreset::PrivateMedium => Self {
target: ConfigFragment {
client: ConfigFragmentFragment {
map: target_client_map,
media: target_client_media,
additional_fields: AuthenticationFailures::new(
Timeframe::PerHour(nz(10)),
nz(20),
),
},
federation: ConfigFragmentFragment {
map: default_restriction_map!(
FederationRestriction;
Join, PerHour, 30, 10;
Knock, PerHour, 30, 10;
Invite, PerHour, 30, 10;
MediaDownload, PerMinute, 100, 50;
),
media: media_config! {
FederationMediaConfig;
download: PerMinute, ByteSize::mb(200), ByteSize::mb(200);
},
additional_fields: Nothing,
},
},
global: ConfigFragment {
client: ConfigFragmentFragment {
map: default_restriction_map!(
ClientRestriction;
Registration, PerDay, 20, 20;
Login, PerHour, 25, 15;
RegistrationTokenValidity, PerDay, 20, 20;
SendEvent, PerSecond, 10, 100;
Join, PerMinute, 5, 30;
Knock, PerMinute, 5, 30;
Invite, PerMinute, 1, 20;
SendReport, PerHour, 10, 25;
CreateAlias, PerMinute, 1, 50;
MediaDownload, PerSecond, 1, 200;
MediaCreate, PerSecond, 2, 20;
),
media: media_config! {
ClientMediaConfig;
download: PerMinute, ByteSize::mb(500), ByteSize::mb(200);
upload: PerMinute, ByteSize::mb(100), ByteSize::mb(200);
fetch: PerMinute, ByteSize::mb(500), ByteSize::mb(200);
},
additional_fields: Nothing,
},
federation: ConfigFragmentFragment {
map: default_restriction_map!(
FederationRestriction;
Join, PerMinute, 25, 25;
Knock, PerMinute, 25, 25;
Invite, PerMinute, 25, 25;
MediaDownload, PerSecond, 10, 100;
),
media: media_config! {
FederationMediaConfig;
download: PerMinute, ByteSize::mb(500), ByteSize::mb(500);
},
additional_fields: Nothing,
},
},
},
ConfigPreset::PublicMedium => Self {
target: ConfigFragment {
client: ConfigFragmentFragment {
map: target_client_map,
media: target_client_media,
additional_fields: AuthenticationFailures::new(
Timeframe::PerHour(nz(10)),
nz(20),
),
},
federation: ConfigFragmentFragment {
map: default_restriction_map!(
FederationRestriction;
Join, PerHour, 30, 10;
Knock, PerHour, 30, 10;
Invite, PerHour, 30, 10;
MediaDownload, PerMinute, 100, 50;
),
media: media_config! {
FederationMediaConfig;
download: PerMinute, ByteSize::mb(200), ByteSize::mb(200);
},
additional_fields: Nothing,
},
},
global: ConfigFragment {
client: ConfigFragmentFragment {
map: default_restriction_map!(
ClientRestriction;
Registration, PerHour, 5, 20;
Login, PerHour, 25, 15;
// Public servers don't have registration tokens, so let's rate limit
// heavily so that if they revert to a private server again, it's a
// reminder to change their preset.
RegistrationTokenValidity, PerDay, 1, 1;
SendEvent, PerSecond, 10, 100;
Join, PerMinute, 5, 30;
Knock, PerMinute, 5, 30;
Invite, PerMinute, 1, 20;
SendReport, PerHour, 10, 25;
CreateAlias, PerMinute, 1, 50;
MediaDownload, PerSecond, 1, 200;
MediaCreate, PerSecond, 2, 20;
),
media: media_config! {
ClientMediaConfig;
download: PerMinute, ByteSize::mb(500), ByteSize::mb(200);
upload: PerMinute, ByteSize::mb(100), ByteSize::mb(200);
fetch: PerMinute, ByteSize::mb(500), ByteSize::mb(200);
},
additional_fields: Nothing,
},
federation: ConfigFragmentFragment {
map: default_restriction_map!(
FederationRestriction;
Join, PerMinute, 25, 25;
Knock, PerMinute, 25, 25;
Invite, PerMinute, 25, 25;
MediaDownload, PerSecond, 10, 100;
),
media: media_config! {
FederationMediaConfig;
download: PerMinute, ByteSize::mb(500), ByteSize::mb(500);
},
additional_fields: Nothing,
},
},
},
ConfigPreset::PublicLarge => Self {
target: ConfigFragment {
client: ConfigFragmentFragment {
map: target_client_map,
media: target_client_media,
additional_fields: AuthenticationFailures::new(
Timeframe::PerMinute(nz(1)),
nz(20),
),
},
federation: ConfigFragmentFragment {
map: default_restriction_map!(
FederationRestriction;
Join, PerHour, 90, 30;
Knock, PerHour, 90, 30;
Invite, PerHour, 90, 30;
MediaDownload, PerMinute, 100, 50;
),
media: media_config! {
FederationMediaConfig;
download: PerMinute, ByteSize::mb(600), ByteSize::mb(300);
},
additional_fields: Nothing,
},
},
global: ConfigFragment {
client: ConfigFragmentFragment {
map: default_restriction_map!(
ClientRestriction;
Registration, PerMinute, 4, 25;
Login, PerMinute, 10, 25;
// Public servers don't have registration tokens, so let's rate limit
// heavily so that if they revert to a private server again, it's a
// reminder to change their preset.
RegistrationTokenValidity, PerDay, 1, 1;
SendEvent, PerSecond, 100, 50;
Join, PerSecond, 1, 20;
Knock, PerSecond, 1, 20;
Invite, PerMinute, 10, 40;
SendReport, PerMinute, 5, 25;
CreateAlias, PerMinute, 30, 20;
MediaDownload, PerSecond, 25, 200;
MediaCreate, PerSecond, 10, 30;
),
media: media_config! {
ClientMediaConfig;
download: PerMinute, ByteSize::gb(2), ByteSize::mb(500);
upload: PerMinute, ByteSize::mb(500), ByteSize::mb(500);
fetch: PerMinute, ByteSize::gb(2), ByteSize::mb(500);
},
additional_fields: Nothing,
},
federation: ConfigFragmentFragment {
map: default_restriction_map!(
FederationRestriction;
Join, PerSecond, 1, 50;
Knock, PerSecond, 1, 50;
Invite, PerSecond, 1, 50;
MediaDownload, PerSecond, 50, 100;
),
media: media_config! {
FederationMediaConfig;
download: PerMinute, ByteSize::gb(2), ByteSize::gb(1);
},
additional_fields: Nothing,
},
},
},
}
}
}

23
conduit-macros/Cargo.toml

@ -0,0 +1,23 @@
[package]
edition.workspace = true
homepage.workspace = true
name = "conduit-macros"
repository.workspace = true
rust-version.workspace = true
version = "0.11.0-alpha"
[lib]
proc-macro = true
[dependencies]
# Parsing and quoting tokens
proc-macro2 = "1"
quote = "1"
syn = { version = "2", features = ["full"] }
[features]
default = ["doc-generators"]
doc-generators = []
[lints]
workspace = true

95
conduit-macros/src/doc_generators.rs

@ -0,0 +1,95 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::{ToTokens, quote};
use syn::{Attribute, Expr, Ident, ItemEnum, Lit, MetaNameValue, Variant, parse::Parse};
pub(super) struct Restrictions {
ident: Ident,
doc_comment: String,
variants: Vec<Restriction>,
}
struct Restriction {
ident: Ident,
doc_comment: String,
}
impl Parse for Restrictions {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let ItemEnum {
ident,
variants,
attrs,
..
} = ItemEnum::parse(input)?;
let variants = variants
.into_iter()
.map(|Variant { attrs, ident, .. }| {
let doc_comment = attrs_to_doc_comment(attrs);
Ok(Restriction { ident, doc_comment })
})
.collect::<syn::Result<Vec<_>>>()?;
let doc_comment = attrs_to_doc_comment(attrs);
Ok(Self {
ident,
variants,
doc_comment,
})
}
}
fn attrs_to_doc_comment(attrs: Vec<Attribute>) -> String {
attrs
.into_iter()
.filter_map(|attr| {
if let syn::Meta::NameValue(MetaNameValue { path, value, .. }) = attr.meta
&& path.is_ident("doc")
&& let Expr::Lit(lit) = value
&& let Lit::Str(string) = lit.lit
{
Some(string.value().trim().to_owned())
} else {
None
}
})
.collect::<Vec<_>>()
.join("\n")
}
/// Produces the following function on said restriction:
/// - `variant_doc_comments`, returning each variant and it's doc comment.
/// - `container_doc_comment`, returning each variant and it's doc comment.
impl ToTokens for Restrictions {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let Self {
ident,
variants,
doc_comment, /* , doc_comments */
} = self;
let output = quote! {
impl DocumentRestrictions for #ident {
fn variant_doc_comments() -> Vec<(Self, String)> {
vec![#((#variants)),*]
}
fn container_doc_comment() -> String {
#doc_comment.to_owned()
}
}
};
tokens.extend(output);
}
}
impl ToTokens for Restriction {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let Self { ident, doc_comment } = self;
// `clone` because `to_tokens` takes a reference to self.
tokens.extend(quote!( (Self::#ident, #doc_comment.to_owned() ) ))
}
}

17
conduit-macros/src/lib.rs

@ -0,0 +1,17 @@
use proc_macro::TokenStream;
use quote::quote;
#[cfg(feature = "doc-generators")]
mod doc_generators;
/// Allows for the doc comments of restrictions to be accessed at runtime.
#[cfg(feature = "doc-generators")]
#[proc_macro_derive(DocumentRestrictions)]
pub fn document_restrictions(item: TokenStream) -> TokenStream {
use doc_generators::Restrictions;
use syn::parse_macro_input;
let restrictions = parse_macro_input!(item as Restrictions);
quote! { #restrictions }.into()
}

210
conduit/Cargo.toml

@ -0,0 +1,210 @@
[package]
authors = ["timokoesters <timo@koesters.xyz>"]
description = "A Matrix homeserver written in Rust"
edition.workspace = true
homepage.workspace = true
license = "Apache-2.0"
name = "conduit"
readme = "README.md"
repository.workspace = true
rust-version.workspace = true
version = "0.11.0-alpha"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lints]
workspace = true
[dependencies]
# For the configuration
conduit-config.workspace = true
# Web framework
axum = { version = "0.8", default-features = false, features = [
"form",
"http1",
"http2",
"json",
"matched-path",
"tokio",
], optional = true }
axum-extra = { version = "0.10", features = ["typed-header"] }
axum-server = { version = "0.7", features = ["tls-rustls"] }
tower = { version = "0.5", features = ["util"] }
tower-http = { version = "0.6", features = [
"add-extension",
"cors",
"sensitive-headers",
"trace",
"util",
] }
tower-service = "0.3"
# Async runtime and utilities
tokio = { version = "1", features = ["fs", "macros", "signal", "sync"] }
# Used for the http request / response body type for Ruma endpoints used with reqwest
bytes = "1"
http = "1"
# Used for ruma wrapper
serde_json = { workspace = true, features = ["raw_value"] }
# Used for appservice registration files
serde_yaml = "0.9"
# Used for pdu definition
serde = { version = "1", features = ["rc"] }
# Used for secure identifiers
rand = "0.9"
# Used to hash passwords
rust-argon2 = "2"
# Used to send requests
hyper = "1"
hyper-util = { version = "0.1", features = [
"client",
"client-legacy",
"http1",
"http2",
] }
reqwest = { workspace = true, features = ["rustls-tls-native-roots", "socks"] }
# Used for conduit::Error type
thiserror.workspace = true #TODO: 2
# Used to generate thumbnails for images
image = { version = "0.25", default-features = false, features = [
"gif",
"jpeg",
"png",
"webp",
] }
# Used for creating media filenames
hex = "0.4"
sha2 = "0.10"
# Used for parsing admin commands and purging media files for space limitations
bytesize.workspace = true
# Used to encode server public key
base64 = "0.22"
# Used when hashing the state
ring = "0.17"
# Used when querying the SRV record of other servers
hickory-resolver = "0.25"
# Used to find matching events for appservices
regex = "1"
# jwt jsonwebtokens
jsonwebtoken = "9"
# Performance measurements
opentelemetry = "0.29"
opentelemetry-jaeger-propagator = "0.29"
opentelemetry-otlp = { version = "0.29", features = ["grpc-tonic"] }
opentelemetry_sdk = { version = "0.29", features = ["rt-tokio"] }
tracing = "0.1"
tracing-flame = "0.2.0"
tracing-opentelemetry = "0.30"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
lru-cache = "0.1.2"
parking_lot = { version = "0.12", optional = true }
rusqlite = { version = "0.35", optional = true, features = ["bundled"] }
# crossbeam = { version = "0.8.2", optional = true }
num_cpus = "1"
threadpool = "1"
# Used for ruma wrapper
serde_html_form = "0.2"
thread_local = "1"
# used for TURN server authentication
hmac = "0.12"
sha-1 = "0.10"
# used for conduit's CLI and admin room command parsing
chrono = "0.4"
clap.workspace = true
humantime = "2"
shell-words = "1.1.0"
futures-util = { version = "0.3", default-features = false }
# Used for reading the configuration from conduit.toml & environment variables
figment = { version = "0.10", features = ["env", "toml"] }
async-trait = "0.1"
tikv-jemallocator = { version = "0.6", features = [
"unprefixed_malloc_on_supported_platforms",
], optional = true }
sd-notify = { version = "0.4", optional = true }
# Used for inspecting request errors
http-body-util = "0.1.3"
# Used for S3 media backend
rusty-s3.workspace = true
# Used for matrix spec type definitions and helpers
[dependencies.ruma]
features = [
"appservice-api-c",
"canonical-json",
"client-api",
"compat-empty-string-null",
"compat-get-3pids",
"compat-null",
"compat-optional",
"compat-optional-txn-pdus",
"compat-server-signing-key-version",
"compat-tag-info",
"compat-unset-avatar",
"federation-api",
"push-gateway-api-c",
"rand",
"ring-compat",
"state-res",
"unstable-msc2448",
"unstable-msc4186",
"unstable-msc4311",
]
workspace = true
[dependencies.rocksdb]
features = ["lz4", "multi-threaded-cf", "zstd"]
optional = true
package = "rust-rocksdb"
version = "0.43"
[target.'cfg(unix)'.dependencies]
nix = { version = "0.30", features = ["resource"] }
[features]
backend_rocksdb = ["conduit-config/rocksdb", "rocksdb"]
backend_sqlite = ["conduit-config/sqlite", "sqlite"]
conduit_bin = ["axum"]
default = ["backend_rocksdb", "backend_sqlite", "conduit_bin", "systemd"]
jemalloc = ["tikv-jemallocator"]
sqlite = ["parking_lot", "rusqlite", "tokio/signal"]
systemd = ["sd-notify"]
enforce_msc4311 = []
[[bin]]
name = "conduit"
path = "src/main.rs"
required-features = ["conduit_bin"]
[lib]
name = "conduit"
path = "src/lib.rs"
[package.metadata.deb]
assets = [
{ mode = "644", source = "../README.md", dest = "usr/share/doc/matrix-conduit" },
{ mode = "644", source = "../debian/README.md", dest = "usr/share/doc/matrix-conduit/README.Debian" },
{ mode = "755", source = "target/release/conduit", dest = "usr/sbin/matrix-conduit" },
]
conf-files = ["/etc/matrix-conduit/conduit.toml"]
copyright = "2020, Timo Kösters <timo@koesters.xyz>"
depends = "$auto, ca-certificates"
extended-description = """\
A fast Matrix homeserver that is optimized for smaller, personal servers, \
instead of a server that has high scalability."""
license-file = ["../LICENSE", "3"]
maintainer = "Paul van Tilburg <paul@luon.net>"
maintainer-scripts = "debian/"
name = "matrix-conduit"
priority = "optional"
section = "net"
systemd-units = { unit-name = "matrix-conduit" }

4
src/api/appservice_server.rs → conduit/src/api/appservice_server.rs

@ -1,6 +1,6 @@
use crate::{services, utils, Error, Result, SUPPORTED_VERSIONS};
use crate::{Error, Result, SUPPORTED_VERSIONS, services, utils};
use bytes::BytesMut;
use ruma::api::{appservice::Registration, IncomingResponse, OutgoingRequest, SendAccessToken};
use ruma::api::{IncomingResponse, OutgoingRequest, SendAccessToken, appservice::Registration};
use std::{fmt::Debug, mem, time::Duration};
use tracing::warn;

24
src/api/client_server/account.rs → conduit/src/api/client_server/account.rs

@ -1,18 +1,20 @@
use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH};
use crate::{api::client_server, services, utils, Error, Result, Ruma};
use crate::{Error, Result, Ruma, api::client_server, services, utils};
use ruma::{
UserId,
api::client::{
account::{
change_password, deactivate, get_3pids, get_username_availability,
ThirdPartyIdRemovalStatus, change_password, deactivate, get_3pids,
get_username_availability,
register::{self, LoginType},
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
whoami, ThirdPartyIdRemovalStatus,
whoami,
},
error::ErrorKind,
uiaa::{AuthFlow, AuthType, UiaaInfo},
},
events::{room::message::RoomMessageEventContent, GlobalAccountDataEventType},
push, UserId,
events::{GlobalAccountDataEventType, room::message::RoomMessageEventContent},
push,
};
use tracing::{info, warn};
@ -179,7 +181,7 @@ pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<registe
&uiaainfo,
)?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
return Err(Error::uiaa(uiaainfo));
}
// Success!
} else if let Some(json) = body.json_body {
@ -191,7 +193,7 @@ pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<registe
&uiaainfo,
&json,
)?;
return Err(Error::Uiaa(uiaainfo));
return Err(Error::uiaa(uiaainfo));
} else {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
}
@ -339,7 +341,7 @@ pub async fn change_password_route(
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
return Err(Error::uiaa(uiaainfo));
}
// Success!
} else if let Some(json) = body.json_body {
@ -347,7 +349,7 @@ pub async fn change_password_route(
services()
.uiaa
.create(sender_user, sender_device, &uiaainfo, &json)?;
return Err(Error::Uiaa(uiaainfo));
return Err(Error::uiaa(uiaainfo));
} else {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
}
@ -430,7 +432,7 @@ pub async fn deactivate_route(
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
return Err(Error::uiaa(uiaainfo));
}
// Success!
} else if let Some(json) = body.json_body {
@ -438,7 +440,7 @@ pub async fn deactivate_route(
services()
.uiaa
.create(sender_user, sender_device, &uiaainfo, &json)?;
return Err(Error::Uiaa(uiaainfo));
return Err(Error::uiaa(uiaainfo));
} else {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
}

2
src/api/client_server/alias.rs → conduit/src/api/client_server/alias.rs

@ -1,4 +1,4 @@
use crate::{services, Error, Result, Ruma};
use crate::{Error, Result, Ruma, services};
use ruma::api::client::{
alias::{create_alias, delete_alias, get_alias},
error::ErrorKind,

2
src/api/client_server/appservice.rs → conduit/src/api/client_server/appservice.rs

@ -5,7 +5,7 @@ use ruma::api::{
client::{appservice::request_ping, error::ErrorKind},
};
use crate::{api::appservice_server, Error, Result, Ruma};
use crate::{Error, Result, Ruma, api::appservice_server};
/// # `POST /_matrix/client/v1/appservice/{appserviceId}/ping`
///

2
src/api/client_server/backup.rs → conduit/src/api/client_server/backup.rs

@ -1,4 +1,4 @@
use crate::{services, Error, Result, Ruma};
use crate::{Error, Result, Ruma, services};
use ruma::api::client::{
backup::{
add_backup_keys, add_backup_keys_for_room, add_backup_keys_for_session,

2
src/api/client_server/capabilities.rs → conduit/src/api/client_server/capabilities.rs

@ -1,4 +1,4 @@
use crate::{services, Result, Ruma};
use crate::{Result, Ruma, services};
use ruma::api::client::discovery::get_capabilities::{
self,
v3::{Capabilities, RoomVersionStability, RoomVersionsCapability},

2
src/api/client_server/config.rs → conduit/src/api/client_server/config.rs

@ -1,4 +1,4 @@
use crate::{services, Error, Result, Ruma};
use crate::{Error, Result, Ruma, services};
use ruma::{
api::client::{
config::{

2
src/api/client_server/context.rs → conduit/src/api/client_server/context.rs

@ -1,4 +1,4 @@
use crate::{services, Error, Result, Ruma};
use crate::{Error, Result, Ruma, services};
use ruma::{
api::client::{context::get_context, error::ErrorKind, filter::LazyLoadOptions},
events::StateEventType,

10
src/api/client_server/device.rs → conduit/src/api/client_server/device.rs

@ -1,4 +1,4 @@
use crate::{services, utils, Error, Result, Ruma};
use crate::{Error, Result, Ruma, services, utils};
use ruma::api::client::{
device::{self, delete_device, delete_devices, get_device, get_devices, update_device},
error::ErrorKind,
@ -94,7 +94,7 @@ pub async fn delete_device_route(
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
return Err(Error::uiaa(uiaainfo));
}
// Success!
} else if let Some(json) = body.json_body {
@ -102,7 +102,7 @@ pub async fn delete_device_route(
services()
.uiaa
.create(sender_user, sender_device, &uiaainfo, &json)?;
return Err(Error::Uiaa(uiaainfo));
return Err(Error::uiaa(uiaainfo));
} else {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
}
@ -148,7 +148,7 @@ pub async fn delete_devices_route(
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
return Err(Error::uiaa(uiaainfo));
}
// Success!
} else if let Some(json) = body.json_body {
@ -156,7 +156,7 @@ pub async fn delete_devices_route(
services()
.uiaa
.create(sender_user, sender_device, &uiaainfo, &json)?;
return Err(Error::Uiaa(uiaainfo));
return Err(Error::uiaa(uiaainfo));
} else {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
}

8
src/api/client_server/directory.rs → conduit/src/api/client_server/directory.rs

@ -1,5 +1,6 @@
use crate::{services, Error, Result, Ruma};
use crate::{Error, Result, Ruma, services};
use ruma::{
ServerName, UInt,
api::{
client::{
directory::{
@ -13,6 +14,7 @@ use ruma::{
},
directory::{Filter, PublicRoomsChunk, RoomNetwork},
events::{
StateEventType,
room::{
avatar::RoomAvatarEventContent,
canonical_alias::RoomCanonicalAliasEventContent,
@ -22,9 +24,7 @@ use ruma::{
join_rules::RoomJoinRulesEventContent,
topic::RoomTopicEventContent,
},
StateEventType,
},
ServerName, UInt,
};
use tracing::{error, info, warn};
@ -169,7 +169,7 @@ pub(crate) async fn get_public_rooms_filtered_helper(
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Invalid `since` token",
))
));
}
};

2
src/api/client_server/filter.rs → conduit/src/api/client_server/filter.rs

@ -1,4 +1,4 @@
use crate::{services, Error, Result, Ruma};
use crate::{Error, Result, Ruma, services};
use ruma::api::client::{
error::ErrorKind,
filter::{create_filter, get_filter},

12
src/api/client_server/keys.rs → conduit/src/api/client_server/keys.rs

@ -1,7 +1,8 @@
use super::SESSION_ID_LENGTH;
use crate::{services, utils, Error, Result, Ruma};
use futures_util::{stream::FuturesUnordered, StreamExt};
use crate::{Error, Result, Ruma, services, utils};
use futures_util::{StreamExt, stream::FuturesUnordered};
use ruma::{
OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId, UserId,
api::{
client::{
error::ErrorKind,
@ -14,11 +15,10 @@ use ruma::{
federation,
},
serde::Raw,
OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId, UserId,
};
use serde_json::json;
use std::{
collections::{hash_map, BTreeMap, HashMap, HashSet},
collections::{BTreeMap, HashMap, HashSet, hash_map},
time::{Duration, Instant},
};
use tracing::{debug, error};
@ -125,7 +125,7 @@ pub async fn upload_signing_keys_route(
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
return Err(Error::uiaa(uiaainfo));
}
// Success!
} else if let Some(json) = &body.json_body {
@ -221,7 +221,7 @@ pub async fn upload_signing_keys_route(
services()
.uiaa
.create(sender_user, sender_device, &uiaainfo, json)?;
return Err(Error::Uiaa(uiaainfo));
return Err(Error::uiaa(uiaainfo));
}
} else {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));

352
src/api/client_server/media.rs → conduit/src/api/client_server/media.rs

@ -3,9 +3,17 @@
use std::time::Duration;
use crate::{service::media::FileMeta, services, utils, Error, Result, Ruma};
use crate::{
Error, Result, Ruma,
service::{
media::{FileMeta, size},
rate_limiting::Target,
},
services, utils,
};
use http::header::{CONTENT_DISPOSITION, CONTENT_TYPE};
use ruma::{
ServerName, UInt,
api::{
client::{
authenticated_media::{
@ -18,7 +26,6 @@ use ruma::{
},
http_headers::{ContentDisposition, ContentDispositionType},
media::Method,
ServerName, UInt,
};
const MXC_LENGTH: usize = 32;
@ -54,6 +61,8 @@ pub async fn get_media_config_auth_route(
pub async fn create_content_route(
body: Ruma<create_content::v3::Request>,
) -> Result<create_content::v3::Response> {
let sender_user = body.sender_user.expect("user is authenticated");
let create_content::v3::Request {
filename,
content_type,
@ -61,6 +70,13 @@ pub async fn create_content_route(
..
} = body.body;
let target = Target::from_client_request(body.appservice_info, &sender_user);
services()
.rate_limiting
.check_media_upload(target, size(&file)?)
.await?;
let media_id = utils::random_string(MXC_LENGTH);
services()
@ -71,7 +87,7 @@ pub async fn create_content_route(
filename.as_deref(),
content_type.as_deref(),
&file,
body.sender_user.as_deref(),
Some(&sender_user),
)
.await?;
@ -84,7 +100,13 @@ pub async fn create_content_route(
pub async fn get_remote_content(
server_name: &ServerName,
media_id: String,
target: Target,
) -> Result<get_content::v1::Response, Error> {
services()
.rate_limiting
.check_media_pre_fetch(&target)
.await?;
let content_response = match services()
.sending
.send_federation_request(
@ -153,6 +175,11 @@ pub async fn get_remote_content(
)
.await?;
services()
.rate_limiting
.update_media_post_fetch(target, size(&content_response.file)?)
.await;
Ok(content_response)
}
@ -171,11 +198,21 @@ pub async fn get_content_route(
} = get_content(
&body.server_name,
body.media_id.clone(),
body.allow_remote,
false,
body.sender_ip_address.map(Target::Ip),
)
.await?;
if let Some(target) = Target::from_client_request_optional_auth(
body.appservice_info,
&body.sender_user,
body.sender_ip_address,
) {
services()
.rate_limiting
.update_media_post_fetch(target, size(&file)?)
.await;
}
Ok(media::get_content::v3::Response {
file,
content_type,
@ -190,14 +227,24 @@ pub async fn get_content_route(
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, true).await
let Ruma::<get_content::v1::Request> {
body,
sender_user,
appservice_info,
..
} = body;
let sender_user = sender_user.as_ref().expect("user is authenticated");
let target = Target::from_client_request(appservice_info, sender_user);
get_content(&body.server_name, body.media_id.clone(), Some(target)).await
}
pub async fn get_content(
server_name: &ServerName,
media_id: String,
allow_remote: bool,
authenticated: bool,
target: Option<Target>,
) -> Result<get_content::v1::Response, Error> {
services().media.check_blocked(server_name, &media_id)?;
@ -207,7 +254,7 @@ pub async fn get_content(
file,
})) = services()
.media
.get(server_name, &media_id, authenticated)
.get(server_name, &media_id, target.clone())
.await
{
Ok(get_content::v1::Response {
@ -215,16 +262,25 @@ pub async fn get_content(
content_type,
content_disposition: Some(content_disposition),
})
} else if server_name != services().globals.server_name() && allow_remote && authenticated {
let remote_content_response = get_remote_content(server_name, media_id.clone()).await?;
Ok(get_content::v1::Response {
content_disposition: remote_content_response.content_disposition,
content_type: remote_content_response.content_type,
file: remote_content_response.file,
})
} else {
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
let error = Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."));
if let Some(target) = target {
if server_name != services().globals.server_name() && target.is_authenticated() {
let remote_content_response =
get_remote_content(server_name, media_id.clone(), target).await?;
Ok(get_content::v1::Response {
content_disposition: remote_content_response.content_disposition,
content_type: remote_content_response.content_type,
file: remote_content_response.file,
})
} else {
error
}
} else {
error
}
}
}
@ -244,8 +300,7 @@ pub async fn get_content_as_filename_route(
&body.server_name,
body.media_id.clone(),
body.filename.clone(),
body.allow_remote,
false,
body.sender_ip_address.map(Target::Ip),
)
.await?;
@ -263,12 +318,22 @@ pub async fn get_content_as_filename_route(
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> {
let Ruma::<get_content_as_filename::v1::Request> {
body,
sender_user,
appservice_info,
..
} = body;
let sender_user = sender_user.as_ref().expect("user is authenticated");
let target = Target::from_client_request(appservice_info, sender_user);
get_content_as_filename(
&body.server_name,
body.media_id.clone(),
body.filename.clone(),
true,
true,
Some(target),
)
.await
}
@ -277,8 +342,7 @@ async fn get_content_as_filename(
server_name: &ServerName,
media_id: String,
filename: String,
allow_remote: bool,
authenticated: bool,
target: Option<Target>,
) -> Result<get_content_as_filename::v1::Response, Error> {
services().media.check_blocked(server_name, &media_id)?;
@ -286,7 +350,7 @@ async fn get_content_as_filename(
file, content_type, ..
})) = services()
.media
.get(server_name, &media_id, authenticated)
.get(server_name, &media_id, target.clone())
.await
{
Ok(get_content_as_filename::v1::Response {
@ -297,19 +361,28 @@ async fn get_content_as_filename(
.with_filename(Some(filename.clone())),
),
})
} else if server_name != services().globals.server_name() && allow_remote && authenticated {
let remote_content_response = get_remote_content(server_name, media_id.clone()).await?;
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,
})
} else {
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
let error = Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."));
if let Some(target) = target {
if server_name != services().globals.server_name() && target.is_authenticated() {
let remote_content_response =
get_remote_content(server_name, media_id.clone(), target).await?;
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,
})
} else {
error
}
} else {
error
}
}
}
@ -321,6 +394,17 @@ async fn get_content_as_filename(
pub async fn get_content_thumbnail_route(
body: Ruma<media::get_content_thumbnail::v3::Request>,
) -> Result<media::get_content_thumbnail::v3::Response> {
let Ruma::<media::get_content_thumbnail::v3::Request> {
body,
sender_user,
sender_ip_address,
appservice_info,
..
} = body;
let target =
Target::from_client_request_optional_auth(appservice_info, &sender_user, sender_ip_address);
let get_content_thumbnail::v1::Response {
file,
content_type,
@ -332,8 +416,7 @@ pub async fn get_content_thumbnail_route(
body.width,
body.method.clone(),
body.animated,
body.allow_remote,
false,
target,
)
.await?;
@ -351,6 +434,15 @@ pub async fn get_content_thumbnail_route(
pub async fn get_content_thumbnail_auth_route(
body: Ruma<get_content_thumbnail::v1::Request>,
) -> Result<get_content_thumbnail::v1::Response> {
let Ruma::<get_content_thumbnail::v1::Request> {
body,
sender_user,
appservice_info,
..
} = body;
let sender_user = sender_user.as_ref().expect("user is authenticated");
let target = Target::from_client_request(appservice_info, sender_user);
get_content_thumbnail(
&body.server_name,
body.media_id.clone(),
@ -358,8 +450,7 @@ pub async fn get_content_thumbnail_auth_route(
body.width,
body.method.clone(),
body.animated,
true,
true,
Some(target),
)
.await
}
@ -372,8 +463,7 @@ async fn get_content_thumbnail(
width: UInt,
method: Option<Method>,
animated: Option<bool>,
allow_remote: bool,
authenticated: bool,
target: Option<Target>,
) -> Result<get_content_thumbnail::v1::Response, Error> {
services().media.check_blocked(server_name, &media_id)?;
@ -392,7 +482,7 @@ async fn get_content_thumbnail(
height
.try_into()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Height is invalid."))?,
authenticated,
target.clone(),
)
.await?
{
@ -401,99 +491,117 @@ async fn get_content_thumbnail(
content_type,
content_disposition: Some(content_disposition),
})
} else if server_name != services().globals.server_name() && allow_remote && authenticated {
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,
content_disposition: content.content_disposition,
},
} else {
let error = Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."));
Ok(federation_media::get_content_thumbnail::v1::Response {
metadata: _,
content: FileOrLocation::Location(url),
}) => {
let get_content::v1::Response {
file,
content_type,
content_disposition,
} = get_location_content(url).await?;
get_content_thumbnail::v1::Response {
file,
content_type,
content_disposition,
}
}
Err(Error::BadRequest(ErrorKind::Unrecognized, _)) => {
let media::get_content_thumbnail::v3::Response {
file,
content_type,
content_disposition,
..
} = services()
if let Some(target) = target {
if server_name != services().globals.server_name() {
services()
.rate_limiting
.check_media_pre_fetch(&target)
.await?;
let thumbnail_response = match services()
.sending
.send_federation_request(
server_name,
media::get_content_thumbnail::v3::Request {
federation_media::get_content_thumbnail::v1::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
{
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,
content_disposition: content.content_disposition,
},
Ok(federation_media::get_content_thumbnail::v1::Response {
metadata: _,
content: FileOrLocation::Location(url),
}) => {
let get_content::v1::Response {
file,
content_type,
content_disposition,
} = get_location_content(url).await?;
get_content_thumbnail::v1::Response {
file,
content_type,
content_disposition,
}
}
Err(Error::BadRequest(ErrorKind::Unrecognized, _)) => {
let media::get_content_thumbnail::v3::Response {
file,
content_type,
content_disposition,
..
} = 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,
content_disposition,
}
}
Err(e) => return Err(e),
};
services()
.rate_limiting
.update_media_post_fetch(target, size(&thumbnail_response.file)?)
.await;
services()
.media
.upload_thumbnail(
server_name,
&media_id,
thumbnail_response
.content_disposition
.as_ref()
.and_then(|cd| cd.filename.as_deref()),
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?;
get_content_thumbnail::v1::Response {
file,
content_type,
content_disposition,
}
Ok(thumbnail_response)
} else {
error
}
Err(e) => return Err(e),
};
services()
.media
.upload_thumbnail(
server_name,
&media_id,
thumbnail_response
.content_disposition
.as_ref()
.and_then(|cd| cd.filename.as_deref()),
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(thumbnail_response)
} else {
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
} else {
error
}
}
}

20
src/api/client_server/membership.rs → conduit/src/api/client_server/membership.rs

@ -1,4 +1,5 @@
use ruma::{
OwnedServerName, RoomId, UserId,
api::{
client::{
error::ErrorKind,
@ -11,17 +12,16 @@ use ruma::{
},
federation::{
self,
membership::{create_invite, RawStrippedState},
membership::{RawStrippedState, create_invite},
},
},
events::{
StateEventType, TimelineEventType,
room::{
join_rules::JoinRule,
member::{MembershipState, RoomMemberEventContent},
},
StateEventType, TimelineEventType,
},
OwnedServerName, RoomId, UserId,
};
use serde_json::value::to_raw_value;
use std::{
@ -32,8 +32,9 @@ use tokio::sync::RwLock;
use tracing::{error, info, warn};
use crate::{
service::pdu::{gen_event_id_canonical_json, PduBuilder},
services, utils, Error, PduEvent, Result, Ruma,
Error, PduEvent, Result, Ruma,
service::pdu::{PduBuilder, gen_event_id_canonical_json},
services, utils,
};
/// # `POST /_matrix/client/r0/rooms/{roomId}/join`
@ -253,7 +254,7 @@ pub async fn knock_room_route(
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"You are not allowed to knock on this room.",
))
));
}
};
@ -774,7 +775,12 @@ pub(crate) async fn invite_helper(
};
if *pdu.event_id != *event_id {
warn!("Server {} changed invite event, that's not allowed in the spec: ours: {:?}, theirs: {:?}", user_id.server_name(), pdu_json, value);
warn!(
"Server {} changed invite event, that's not allowed in the spec: ours: {:?}, theirs: {:?}",
user_id.server_name(),
pdu_json,
value
);
}
let origin: OwnedServerName = serde_json::from_value(

3
src/api/client_server/message.rs → conduit/src/api/client_server/message.rs

@ -1,6 +1,7 @@
use crate::{
Error, Result, Ruma,
service::{pdu::PduBuilder, rooms::timeline::PduCount},
services, utils, Error, Result, Ruma,
services, utils,
};
use ruma::{
api::client::{

0
src/api/client_server/mod.rs → conduit/src/api/client_server/mod.rs

2
src/api/client_server/openid.rs → conduit/src/api/client_server/openid.rs

@ -2,7 +2,7 @@ use std::time::Duration;
use ruma::{api::client::account, authentication::TokenType};
use crate::{services, Result, Ruma};
use crate::{Result, Ruma, services};
/// # `POST /_matrix/client/r0/user/{userId}/openid/request_token`
///

2
src/api/client_server/presence.rs → conduit/src/api/client_server/presence.rs

@ -1,4 +1,4 @@
use crate::{services, utils, Error, Result, Ruma};
use crate::{Error, Result, Ruma, services, utils};
use ruma::api::client::{
error::ErrorKind,
presence::{get_presence, set_presence},

4
src/api/client_server/profile.rs → conduit/src/api/client_server/profile.rs

@ -1,4 +1,4 @@
use crate::{service::pdu::PduBuilder, services, utils, Error, Result, Ruma};
use crate::{Error, Result, Ruma, service::pdu::PduBuilder, services, utils};
use ruma::{
api::{
client::{
@ -9,7 +9,7 @@ use ruma::{
},
federation::{self, query::get_profile_information::v1::ProfileField},
},
events::{room::member::RoomMemberEventContent, StateEventType, TimelineEventType},
events::{StateEventType, TimelineEventType, room::member::RoomMemberEventContent},
};
use serde_json::value::to_raw_value;
use std::sync::Arc;

4
src/api/client_server/push.rs → conduit/src/api/client_server/push.rs

@ -1,4 +1,4 @@
use crate::{services, Error, Result, Ruma};
use crate::{Error, Result, Ruma, services};
use ruma::{
api::client::{
error::ErrorKind,
@ -8,7 +8,7 @@ use ruma::{
set_pushrule_enabled,
},
},
events::{push_rules::PushRulesEvent, GlobalAccountDataEventType},
events::{GlobalAccountDataEventType, push_rules::PushRulesEvent},
push::{InsertPushRuleError, RemovePushRuleError},
};

10
src/api/client_server/read_marker.rs → conduit/src/api/client_server/read_marker.rs

@ -1,11 +1,11 @@
use crate::{service::rooms::timeline::PduCount, services, Error, Result, Ruma};
use crate::{Error, Result, Ruma, service::rooms::timeline::PduCount, services};
use ruma::{
MilliSecondsSinceUnixEpoch,
api::client::{error::ErrorKind, read_marker::set_read_marker, receipt::create_receipt},
events::{
receipt::{ReceiptThread, ReceiptType},
RoomAccountDataEventType,
receipt::{ReceiptThread, ReceiptType},
},
MilliSecondsSinceUnixEpoch,
};
use std::collections::BTreeMap;
@ -55,7 +55,7 @@ pub async fn set_read_marker_route(
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Read receipt is in backfilled timeline",
))
));
}
PduCount::Normal(c) => c,
};
@ -165,7 +165,7 @@ pub async fn create_receipt_route(
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Read receipt is in backfilled timeline",
))
));
}
PduCount::Normal(c) => c,
};

4
src/api/client_server/redact.rs → conduit/src/api/client_server/redact.rs

@ -1,9 +1,9 @@
use std::sync::Arc;
use crate::{service::pdu::PduBuilder, services, Result, Ruma};
use crate::{Result, Ruma, service::pdu::PduBuilder, services};
use ruma::{
api::client::redact::redact_event,
events::{room::redaction::RoomRedactionEventContent, TimelineEventType},
events::{TimelineEventType, room::redaction::RoomRedactionEventContent},
};
use serde_json::value::to_raw_value;

2
src/api/client_server/relations.rs → conduit/src/api/client_server/relations.rs

@ -3,7 +3,7 @@ use ruma::api::client::relations::{
get_relating_events_with_rel_type_and_event_type,
};
use crate::{services, Result, Ruma};
use crate::{Result, Ruma, services};
/// # `GET /_matrix/client/r0/rooms/{roomId}/relations/{eventId}/{relType}/{eventType}`
pub async fn get_relating_events_with_rel_type_and_event_type_route(

4
src/api/client_server/report.rs → conduit/src/api/client_server/report.rs

@ -1,4 +1,4 @@
use crate::{services, utils::HtmlEscape, Error, Result, Ruma};
use crate::{Error, Result, Ruma, services, utils::HtmlEscape};
use ruma::{
api::client::{error::ErrorKind, room::report_content},
events::room::message,
@ -20,7 +20,7 @@ pub async fn report_event_route(
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Invalid Event ID",
))
));
}
};

8
src/api/client_server/room.rs → conduit/src/api/client_server/room.rs

@ -1,12 +1,15 @@
use crate::{
api::client_server::invite_helper, service::pdu::PduBuilder, services, Error, Result, Ruma,
Error, Result, Ruma, api::client_server::invite_helper, service::pdu::PduBuilder, services,
};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, Int, OwnedRoomAliasId, OwnedUserId, RoomAliasId,
RoomVersionId,
api::client::{
error::ErrorKind,
room::{self, aliases, create_room, get_room_event, upgrade_room},
},
events::{
StateEventType, TimelineEventType,
room::{
canonical_alias::RoomCanonicalAliasEventContent,
create::RoomCreateEventContent,
@ -19,12 +22,9 @@ use ruma::{
tombstone::RoomTombstoneEventContent,
topic::RoomTopicEventContent,
},
StateEventType, TimelineEventType,
},
int,
serde::JsonObject,
CanonicalJsonObject, CanonicalJsonValue, Int, OwnedRoomAliasId, OwnedUserId, RoomAliasId,
RoomVersionId,
};
use serde::Deserialize;
use serde_json::{json, value::to_raw_value};

4
src/api/client_server/search.rs → conduit/src/api/client_server/search.rs

@ -1,4 +1,4 @@
use crate::{services, Error, Result, Ruma};
use crate::{Error, Result, Ruma, services};
use ruma::api::client::{
error::ErrorKind,
search::search_events::{
@ -63,7 +63,7 @@ pub async fn search_events_route(
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Invalid next_batch token.",
))
));
}
None => 0, // Default to the start
};

4
src/api/client_server/session.rs → conduit/src/api/client_server/session.rs

@ -1,12 +1,12 @@
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use crate::{services, utils, Error, Result, Ruma};
use crate::{Error, Result, Ruma, services, utils};
use ruma::{
UserId,
api::client::{
error::ErrorKind,
session::{get_login_types, login, logout, logout_all},
uiaa::UserIdentifier,
},
UserId,
};
use serde::Deserialize;
use tracing::{info, warn};

4
src/api/client_server/space.rs → conduit/src/api/client_server/space.rs

@ -1,9 +1,9 @@
use std::str::FromStr;
use crate::{service::rooms::spaces::PagnationToken, services, Error, Result, Ruma};
use crate::{Error, Result, Ruma, service::rooms::spaces::PagnationToken, services};
use ruma::{
api::client::{error::ErrorKind, space::get_hierarchy},
UInt,
api::client::{error::ErrorKind, space::get_hierarchy},
};
/// # `GET /_matrix/client/v1/rooms/{room_id}/hierarchy``

6
src/api/client_server/state.rs → conduit/src/api/client_server/state.rs

@ -1,7 +1,8 @@
use std::sync::Arc;
use crate::{service::pdu::PduBuilder, services, Error, Result, Ruma, RumaResponse};
use crate::{Error, Result, Ruma, RumaResponse, service::pdu::PduBuilder, services};
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, RoomId, UserId,
api::client::{
error::ErrorKind,
state::{
@ -10,10 +11,9 @@ use ruma::{
},
},
events::{
room::canonical_alias::RoomCanonicalAliasEventContent, AnyStateEventContent, StateEventType,
AnyStateEventContent, StateEventType, room::canonical_alias::RoomCanonicalAliasEventContent,
},
serde::Raw,
EventId, MilliSecondsSinceUnixEpoch, RoomId, UserId,
};
use tracing::warn;

19
src/api/client_server/sync.rs → conduit/src/api/client_server/sync.rs

@ -1,31 +1,32 @@
use crate::{
Error, PduEvent, Result, Ruma, RumaResponse,
service::{pdu::EventHash, rooms::timeline::PduCount},
services, utils, Error, PduEvent, Result, Ruma, RumaResponse,
services, utils,
};
use ruma::{
DeviceId, EventId, JsOption, OwnedDeviceId, OwnedUserId, RoomId, UInt, UserId,
api::client::{
filter::{FilterDefinition, LazyLoadOptions},
sync::sync_events::{
self,
self, DeviceLists, UnreadNotificationsCount,
v3::{
Ephemeral, Filter, GlobalAccountData, InviteState, InvitedRoom, JoinedRoom,
KnockState, KnockedRoom, LeftRoom, Presence, RoomAccountData, RoomSummary, Rooms,
State, StateEvents, Timeline, ToDevice,
},
DeviceLists, UnreadNotificationsCount,
},
uiaa::UiaaResponse,
},
events::{
room::member::{MembershipState, RoomMemberEventContent},
StateEventType, TimelineEventType,
room::member::{MembershipState, RoomMemberEventContent},
},
serde::Raw,
uint, DeviceId, EventId, JsOption, OwnedDeviceId, OwnedUserId, RoomId, UInt, UserId,
uint,
};
use std::{
collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap, HashSet},
collections::{BTreeMap, BTreeSet, HashMap, HashSet, hash_map::Entry},
sync::Arc,
time::Duration,
};
@ -134,16 +135,14 @@ pub async fn sync_events_route(
}
}
let result = match rx
match rx
.borrow()
.as_ref()
.expect("When sync channel changes it's always set to some")
{
Ok(response) => Ok(response.clone()),
Err(error) => Err(error.to_response()),
};
result
}
}
async fn sync_helper_wrapper(

4
src/api/client_server/tag.rs → conduit/src/api/client_server/tag.rs

@ -1,9 +1,9 @@
use crate::{services, Error, Result, Ruma};
use crate::{Error, Result, Ruma, services};
use ruma::{
api::client::tag::{create_tag, delete_tag, get_tags},
events::{
tag::{TagEvent, TagEventContent},
RoomAccountDataEventType,
tag::{TagEvent, TagEventContent},
},
};
use std::collections::BTreeMap;

0
src/api/client_server/thirdparty.rs → conduit/src/api/client_server/thirdparty.rs

2
src/api/client_server/threads.rs → conduit/src/api/client_server/threads.rs

@ -1,6 +1,6 @@
use ruma::api::client::{error::ErrorKind, threads::get_threads};
use crate::{services, Error, Result, Ruma};
use crate::{Error, Result, Ruma, services};
/// # `GET /_matrix/client/r0/rooms/{roomId}/threads`
pub async fn get_threads_route(

2
src/api/client_server/to_device.rs → conduit/src/api/client_server/to_device.rs

@ -1,6 +1,6 @@
use std::collections::BTreeMap;
use crate::{services, Error, Result, Ruma};
use crate::{Error, Result, Ruma, services};
use ruma::{
api::{
client::{error::ErrorKind, to_device::send_event_to_device},

2
src/api/client_server/typing.rs → conduit/src/api/client_server/typing.rs

@ -1,4 +1,4 @@
use crate::{services, utils, Error, Result, Ruma};
use crate::{Error, Result, Ruma, services, utils};
use ruma::api::client::{error::ErrorKind, typing::create_typing_event};
/// # `PUT /_matrix/client/r0/rooms/{roomId}/typing/{userId}`

0
src/api/client_server/unversioned.rs → conduit/src/api/client_server/unversioned.rs

4
src/api/client_server/user_directory.rs → conduit/src/api/client_server/user_directory.rs

@ -1,9 +1,9 @@
use crate::{services, Result, Ruma};
use crate::{Result, Ruma, services};
use ruma::{
api::client::user_directory::search_users,
events::{
room::join_rules::{JoinRule, RoomJoinRulesEventContent},
StateEventType,
room::join_rules::{JoinRule, RoomJoinRulesEventContent},
},
};

7
src/api/client_server/voip.rs → conduit/src/api/client_server/voip.rs

@ -1,9 +1,10 @@
use crate::{config::TurnAuth, services, Error, Result, Ruma};
use base64::{engine::general_purpose, Engine as _};
use crate::{Error, Result, Ruma, services};
use base64::{Engine as _, engine::general_purpose};
use conduit_config::TurnAuth;
use hmac::{Hmac, Mac};
use ruma::{
api::client::{error::ErrorKind, voip::get_turn_server_info},
SecondsSinceUnixEpoch,
api::client::{error::ErrorKind, voip::get_turn_server_info},
};
use sha1::Sha1;
use std::time::{Duration, SystemTime};

2
src/api/client_server/well_known.rs → conduit/src/api/client_server/well_known.rs

@ -1,6 +1,6 @@
use ruma::api::client::discovery::discover_homeserver::{self, HomeserverInfo};
use crate::{services, Result, Ruma};
use crate::{Result, Ruma, services};
/// # `GET /.well-known/matrix/client`
///

0
src/api/mod.rs → conduit/src/api/mod.rs

452
conduit/src/api/ruma_wrapper/axum.rs

@ -0,0 +1,452 @@
use std::{
collections::BTreeMap,
error::Error as _,
iter::FromIterator,
net::{IpAddr, SocketAddr},
str::{self, FromStr},
};
use axum::{
RequestPartsExt,
body::Body,
extract::{ConnectInfo, FromRequest, Path},
response::{IntoResponse, Response},
};
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
typed_header::TypedHeaderRejectionReason,
};
use bytes::{BufMut, BytesMut};
use conduit_config::IpAddrDetection;
use http::{Request, StatusCode};
use ruma::{
CanonicalJsonValue, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, UserId,
api::{
AuthScheme, IncomingRequest, OutgoingResponse, client::error::ErrorKind,
federation::authentication::XMatrix,
},
};
use serde::Deserialize;
use tracing::{debug, error, warn};
use super::{Ruma, RumaResponse};
use crate::{
Error, Result,
service::{appservice::RegistrationInfo, rate_limiting::Target},
services,
};
enum Token {
Appservice(Box<RegistrationInfo>),
User((OwnedUserId, OwnedDeviceId)),
AuthRateLimited(Error),
Invalid,
None,
}
impl<T, S> FromRequest<S> for Ruma<T>
where
T: IncomingRequest,
S: Sync,
{
type Rejection = Error;
async fn from_request(req: Request<Body>, _state: &S) -> Result<Self, Self::Rejection> {
#[derive(Deserialize)]
struct QueryParams {
access_token: Option<String>,
user_id: Option<String>,
}
let (mut parts, mut body) = {
let (parts, body) = req.into_parts();
let body = axum::body::to_bytes(
body,
services()
.globals
.max_request_size()
.try_into()
.unwrap_or(usize::MAX),
)
.await
.map_err(|err| {
if err
.source()
.is_some_and(|err| err.is::<http_body_util::LengthLimitError>())
{
Error::BadRequest(ErrorKind::TooLarge, "Reached maximum request size")
} else {
error!("An unknown error has occurred: {err}");
Error::BadRequest(ErrorKind::Unknown, "An unknown error has occurred")
}
})?;
(parts, body)
};
let metadata = T::METADATA;
let auth_header: Option<TypedHeader<Authorization<Bearer>>> =
// If X-Matrix signatures are used, it causes this extraction to fail with an error
if metadata.authentication != AuthScheme::ServerSignatures {
parts.extract().await?
} else {
None
};
let path_params: Path<Vec<String>> = parts.extract().await?;
let query = parts.uri.query().unwrap_or_default();
let query_params: QueryParams = match serde_html_form::from_str(query) {
Ok(params) => params,
Err(e) => {
error!(%query, "Failed to deserialize query parameters: {}", e);
return Err(Error::BadRequest(
ErrorKind::Unknown,
"Failed to read query parameters",
));
}
};
let token = match &auth_header {
Some(TypedHeader(Authorization(bearer))) => Some(bearer.token()),
None => query_params.access_token.as_deref(),
};
let sender_ip_address: Option<IpAddr> =
match &services().globals.config.ip_address_detection {
IpAddrDetection::SocketAddress => {
let addr: ConnectInfo<SocketAddr> = parts.extract().await?;
Some(addr.ip())
}
IpAddrDetection::Header(name) => parts
.headers
.get(name)
.and_then(|header| header.to_str().ok())
.map(|header| header.split_once(',').map(|(ip, _)| ip).unwrap_or(header))
.and_then(|ip| IpAddr::from_str(ip).ok()),
};
let token = if let Some(token) = token {
let mut rate_limited = None;
if let Some(ip_addr) = sender_ip_address {
if let Err(instant) = services().rate_limiting.pre_auth_check(ip_addr).await {
rate_limited = Some(instant);
}
}
if let Some(instant) = rate_limited {
Token::AuthRateLimited(instant)
} else if let Some(reg_info) = services().appservice.find_from_token(token).await {
Token::Appservice(Box::new(reg_info.clone()))
} else if let Some((user_id, device_id)) = services().users.find_from_token(token)? {
Token::User((user_id, device_id))
} else {
Token::Invalid
}
} else {
Token::None
};
let mut json_body = serde_json::from_slice::<CanonicalJsonValue>(&body).ok();
let (sender_user, sender_device, sender_servername, appservice_info) = match (
metadata.authentication,
token,
) {
(_, Token::AuthRateLimited(instant)) => {
return Err(instant);
}
(_, Token::Invalid) => {
// OpenID endpoint uses a query param with the same name, drop this once query params for user auth are removed from the spec
if query_params.access_token.is_some() {
(None, None, None, None)
} else {
if let Some(addr) = sender_ip_address {
services()
.rate_limiting
.update_post_auth_failure(addr)
.await;
} else {
error!(
"Auth failure occurred, but IP address was not extracted. Please check your Conduit & reverse proxy configuration, as if nothing is done, an attacker can brute-force access tokens and login to user's accounts"
);
}
return Err(Error::BadRequest(
ErrorKind::UnknownToken { soft_logout: false },
"Unknown access token.",
));
}
}
(AuthScheme::AccessToken, Token::Appservice(info)) => {
let user_id = query_params
.user_id
.map_or_else(
|| {
UserId::parse_with_server_name(
info.registration.sender_localpart.as_str(),
services().globals.server_name(),
)
},
UserId::parse,
)
.map_err(|_| {
Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.")
})?;
if !info.is_user_match(&user_id) {
return Err(Error::BadRequest(
ErrorKind::Exclusive,
"User is not in namespace.",
));
}
if !services().users.exists(&user_id)? {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"User does not exist.",
));
}
(Some(user_id), None, None, Some(*info))
}
(
AuthScheme::None
| AuthScheme::AppserviceToken
| AuthScheme::AppserviceTokenOptional
| AuthScheme::AccessTokenOptional,
Token::Appservice(info),
) => (None, None, None, Some(*info)),
(AuthScheme::AppserviceToken | AuthScheme::AccessToken, Token::None) => {
return Err(Error::BadRequest(
ErrorKind::MissingToken,
"Missing access token.",
));
}
(
AuthScheme::AccessToken | AuthScheme::AccessTokenOptional | AuthScheme::None,
Token::User((user_id, device_id)),
) => (Some(user_id), Some(device_id), None, None),
(AuthScheme::ServerSignatures, Token::None) => {
let TypedHeader(Authorization(x_matrix)) = parts
.extract::<TypedHeader<Authorization<XMatrix>>>()
.await
.map_err(|e| {
warn!("Missing or invalid Authorization header: {}", e);
let msg = match e.reason() {
TypedHeaderRejectionReason::Missing => "Missing Authorization header.",
TypedHeaderRejectionReason::Error(_) => "Invalid X-Matrix signatures.",
_ => "Unknown header-related error",
};
Error::BadRequest(ErrorKind::forbidden(), msg)
})?;
if let Some(dest) = x_matrix.destination {
if dest != services().globals.server_name() {
return Err(Error::BadRequest(
ErrorKind::Unauthorized,
"X-Matrix destination field does not match server name.",
));
}
};
let origin_signatures = BTreeMap::from_iter([(
x_matrix.key.clone(),
CanonicalJsonValue::String(x_matrix.sig.to_string()),
)]);
let signatures = BTreeMap::from_iter([(
x_matrix.origin.as_str().to_owned(),
CanonicalJsonValue::Object(
origin_signatures
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect(),
),
)]);
let mut request_map = BTreeMap::from_iter([
(
"method".to_owned(),
CanonicalJsonValue::String(parts.method.to_string()),
),
(
"uri".to_owned(),
CanonicalJsonValue::String(parts.uri.to_string()),
),
(
"origin".to_owned(),
CanonicalJsonValue::String(x_matrix.origin.as_str().to_owned()),
),
(
"destination".to_owned(),
CanonicalJsonValue::String(
services().globals.server_name().as_str().to_owned(),
),
),
(
"signatures".to_owned(),
CanonicalJsonValue::Object(signatures),
),
]);
if let Some(json_body) = &json_body {
request_map.insert("content".to_owned(), json_body.clone());
};
let keys_result = services()
.rooms
.event_handler
.fetch_signing_keys(&x_matrix.origin, vec![x_matrix.key.to_string()], false)
.await;
let keys = match keys_result {
Ok(b) => b,
Err(e) => {
warn!("Failed to fetch signing keys: {}", e);
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Failed to fetch signing keys.",
));
}
};
// Only verify_keys that are currently valid should be used for validating requests
// as per MSC4029
let pub_key_map = BTreeMap::from_iter([(
x_matrix.origin.as_str().to_owned(),
if keys.valid_until_ts > MilliSecondsSinceUnixEpoch::now() {
keys.verify_keys
.into_iter()
.map(|(id, key)| (id, key.key))
.collect()
} else {
BTreeMap::new()
},
)]);
match ruma::signatures::verify_json(&pub_key_map, &request_map) {
Ok(()) => (None, None, Some(x_matrix.origin), None),
Err(e) => {
warn!(
"Failed to verify json request from {}: {}\n{:?}",
x_matrix.origin, e, request_map
);
if parts.uri.to_string().contains('@') {
warn!(
"Request uri contained '@' character. Make sure your \
reverse proxy gives Conduit the raw uri (apache: use \
nocanon)"
);
}
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Failed to verify X-Matrix signatures.",
));
}
}
}
(
AuthScheme::None
| AuthScheme::AppserviceTokenOptional
| AuthScheme::AccessTokenOptional,
Token::None,
) => (None, None, None, None),
(AuthScheme::ServerSignatures, Token::Appservice(_) | Token::User(_)) => {
return Err(Error::BadRequest(
ErrorKind::Unauthorized,
"Only server signatures should be used on this endpoint.",
));
}
(AuthScheme::AppserviceToken | AuthScheme::AppserviceTokenOptional, Token::User(_)) => {
return Err(Error::BadRequest(
ErrorKind::Unauthorized,
"Only appservice access tokens should be used on this endpoint.",
));
}
};
let sender_ip_address = parts
.headers
.get("X-Forwarded-For")
.and_then(|header| header.to_str().ok())
.map(|header| header.split_once(',').map(|(ip, _)| ip).unwrap_or(header))
.and_then(|ip| IpAddr::from_str(ip).ok());
let target = if let Some(server_name) = sender_servername.clone() {
Some(Target::Server(server_name))
} else if let Some(user) = &sender_user {
Some(Target::from_client_request(appservice_info.clone(), user))
} else {
sender_ip_address.map(Target::Ip)
};
services().rate_limiting.check(target, metadata).await?;
let mut http_request = Request::builder().uri(parts.uri).method(parts.method);
*http_request.headers_mut().unwrap() = parts.headers;
if let Some(CanonicalJsonValue::Object(json_body)) = &mut json_body {
let user_id = sender_user.clone().unwrap_or_else(|| {
UserId::parse_with_server_name("", services().globals.server_name())
.expect("we know this is valid")
});
let uiaa_request = json_body
.get("auth")
.and_then(|auth| auth.as_object())
.and_then(|auth| auth.get("session"))
.and_then(|session| session.as_str())
.and_then(|session| {
services().uiaa.get_uiaa_request(
&user_id,
&sender_device.clone().unwrap_or_else(|| "".into()),
session,
)
});
if let Some(CanonicalJsonValue::Object(initial_request)) = uiaa_request {
for (key, value) in initial_request {
json_body.entry(key).or_insert(value);
}
}
let mut buf = BytesMut::new().writer();
serde_json::to_writer(&mut buf, json_body).expect("value serialization can't fail");
body = buf.into_inner().freeze();
}
let http_request = http_request.body(&*body).unwrap();
debug!("{:?}", http_request);
let body = T::try_from_http_request(http_request, &path_params).map_err(|e| {
warn!("try_from_http_request failed: {:?}", e);
debug!("JSON body: {:?}", json_body);
Error::BadRequest(ErrorKind::BadJson, "Failed to deserialize request.")
})?;
Ok(Ruma {
body,
sender_user,
sender_device,
sender_servername,
appservice_info,
json_body,
sender_ip_address,
})
}
}
impl<T: OutgoingResponse> IntoResponse for RumaResponse<T> {
fn into_response(self) -> Response {
match self.0.try_into_http_response::<BytesMut>() {
Ok(res) => res.map(BytesMut::freeze).map(Body::from).into_response(),
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}
}

9
src/api/ruma_wrapper/mod.rs → conduit/src/api/ruma_wrapper/mod.rs

@ -1,9 +1,9 @@
use crate::{service::appservice::RegistrationInfo, Error};
use crate::{Error, service::appservice::RegistrationInfo};
use ruma::{
api::client::uiaa::UiaaResponse, CanonicalJsonValue, OwnedDeviceId, OwnedServerName,
OwnedUserId,
CanonicalJsonValue, OwnedDeviceId, OwnedServerName, OwnedUserId,
api::client::uiaa::UiaaResponse,
};
use std::ops::Deref;
use std::{net::IpAddr, ops::Deref};
#[cfg(feature = "conduit_bin")]
mod axum;
@ -14,6 +14,7 @@ pub struct Ruma<T> {
pub sender_user: Option<OwnedUserId>,
pub sender_device: Option<OwnedDeviceId>,
pub sender_servername: Option<OwnedServerName>,
pub sender_ip_address: Option<IpAddr>,
// This is None when body is not a valid string
pub json_body: Option<CanonicalJsonValue>,
pub appservice_info: Option<RegistrationInfo>,

55
src/api/server_server.rs → conduit/src/api/server_server.rs

@ -1,33 +1,39 @@
#![allow(deprecated)]
use crate::{
Error, PduEvent, Result, Ruma, SUPPORTED_VERSIONS,
api::client_server::{self, claim_keys_helper, get_keys_helper},
service::{
globals::SigningKeys,
media::FileMeta,
pdu::{gen_event_id_canonical_json, PduBuilder},
pdu::{PduBuilder, gen_event_id_canonical_json},
rate_limiting::Target,
},
services, utils, Error, PduEvent, Result, Ruma, SUPPORTED_VERSIONS,
services, utils,
};
use axum::{response::IntoResponse, Json};
use axum::{Json, response::IntoResponse};
use axum_extra::headers::{CacheControl, Header};
use get_profile_information::v1::ProfileField;
use http::header::AUTHORIZATION;
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId,
OwnedRoomId, OwnedServerName, OwnedServerSigningKeyId, OwnedUserId, RoomId, RoomVersionId,
ServerName, Signatures, UserId,
api::{
EndpointError, IncomingResponse, OutgoingRequest, OutgoingResponse, SendAccessToken,
client::error::{Error as RumaError, ErrorKind},
federation::{
authenticated_media::{
get_content, get_content_thumbnail, Content, ContentMetadata, FileOrLocation,
Content, ContentMetadata, FileOrLocation, get_content, get_content_thumbnail,
},
authorization::get_event_authorization,
backfill::get_backfill,
device::get_devices::{self, v1::UserDevice},
directory::{get_public_rooms, get_public_rooms_filtered},
discovery::{
discover_homeserver, get_server_keys, get_server_version, ServerSigningKeys,
VerifyKey,
ServerSigningKeys, VerifyKey, discover_homeserver, get_server_keys,
get_server_version,
},
event::{get_event, get_missing_events, get_room_state, get_room_state_ids},
keys::{claim_keys, get_keys},
@ -43,25 +49,22 @@ use ruma::{
send_transaction_message,
},
},
EndpointError, IncomingResponse, OutgoingRequest, OutgoingResponse, SendAccessToken,
},
directory::{Filter, RoomNetwork},
events::{
StateEventType, TimelineEventType,
receipt::{ReceiptEvent, ReceiptEventContent, ReceiptType},
room::{
join_rules::{AllowRule, JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
},
StateEventType, TimelineEventType,
},
room_version_rules::{AuthorizationRules, RoomVersionRules},
serde::{Base64, JsonObject, Raw},
to_device::DeviceIdOrAllDevices,
uint, user_id, CanonicalJsonObject, CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch,
OwnedEventId, OwnedRoomId, OwnedServerName, OwnedServerSigningKeyId, OwnedUserId, RoomId,
RoomVersionId, ServerName, Signatures, UserId,
uint, user_id,
};
use serde_json::value::{to_raw_value, RawValue as RawJsonValue};
use serde_json::value::{RawValue as RawJsonValue, to_raw_value};
use std::{
collections::BTreeMap,
fmt::Debug,
@ -130,11 +133,11 @@ where
T: OutgoingRequest + Debug,
{
if !services().globals.allow_federation() {
return Err(Error::bad_config("Federation is disabled."));
return Err(Error::BadServerResponse("Federation is disabled."));
}
if destination == services().globals.server_name() {
return Err(Error::bad_config(
return Err(Error::BadServerResponse(
"Won't send federation request to ourselves",
));
}
@ -2242,6 +2245,13 @@ pub async fn create_invite_route(
pub async fn get_content_route(
body: Ruma<get_content::v1::Request>,
) -> Result<get_content::v1::Response> {
let sender_servername = body
.sender_servername
.as_ref()
.expect("server is authenticated");
let target = Some(Target::Server(sender_servername.to_owned()));
services()
.media
.check_blocked(services().globals.server_name(), &body.media_id)?;
@ -2252,7 +2262,11 @@ pub async fn get_content_route(
file,
}) = services()
.media
.get(services().globals.server_name(), &body.media_id, true)
.get(
services().globals.server_name(),
&body.media_id,
target.clone(),
)
.await?
{
Ok(get_content::v1::Response::new(
@ -2274,6 +2288,13 @@ pub async fn get_content_route(
pub async fn get_content_thumbnail_route(
body: Ruma<get_content_thumbnail::v1::Request>,
) -> Result<get_content_thumbnail::v1::Response> {
let Ruma::<get_content_thumbnail::v1::Request> {
body,
sender_servername,
..
} = body;
let sender_servername = sender_servername.expect("server is authenticated");
services()
.media
.check_blocked(services().globals.server_name(), &body.media_id)?;
@ -2293,7 +2314,7 @@ pub async fn get_content_thumbnail_route(
body.height
.try_into()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid."))?,
true,
Some(Target::Server(sender_servername)),
)
.await?
else {
@ -2545,7 +2566,7 @@ pub async fn well_known_server(
#[cfg(test)]
mod tests {
use super::{add_port_to_hostname, get_ip_with_port, FedDest};
use super::{FedDest, add_port_to_hostname, get_ip_with_port};
#[test]
fn ips_get_default_ports() {

2
src/clap.rs → conduit/src/clap.rs

@ -11,7 +11,7 @@ fn version() -> String {
let cargo_pkg_version = env!("CARGO_PKG_VERSION");
match option_env!("CONDUIT_VERSION_EXTRA") {
Some(x) => format!("{} ({})", cargo_pkg_version, x),
Some(x) => format!("{cargo_pkg_version} ({x})"),
None => cargo_pkg_version.to_owned(),
}
}

3
src/database/abstraction.rs → conduit/src/database/abstraction.rs

@ -1,6 +1,7 @@
use super::Config;
use crate::Result;
use conduit_config::Config;
use std::{future::Future, pin::Pin, sync::Arc};
#[cfg(feature = "sqlite")]

5
src/database/abstraction/rocksdb.rs → conduit/src/database/abstraction/rocksdb.rs

@ -1,5 +1,6 @@
use super::{super::Config, watchers::Watchers, KeyValueDatabaseEngine, KvTree};
use crate::{utils, Result};
use super::{KeyValueDatabaseEngine, KvTree, watchers::Watchers};
use crate::{Result, utils};
use conduit_config::Config;
use std::{
future::Future,
pin::Pin,

5
src/database/abstraction/sqlite.rs → conduit/src/database/abstraction/sqlite.rs

@ -1,5 +1,6 @@
use super::{watchers::Watchers, KeyValueDatabaseEngine, KvTree};
use crate::{database::Config, Result};
use super::{KeyValueDatabaseEngine, KvTree, watchers::Watchers};
use crate::Result;
use conduit_config::Config;
use parking_lot::{Mutex, MutexGuard};
use rusqlite::{Connection, DatabaseName::Main, OptionalExtension};
use std::{

2
src/database/abstraction/watchers.rs → conduit/src/database/abstraction/watchers.rs

@ -1,5 +1,5 @@
use std::{
collections::{hash_map, HashMap},
collections::{HashMap, hash_map},
future::Future,
pin::Pin,
sync::RwLock,

4
src/database/key_value/account_data.rs → conduit/src/database/key_value/account_data.rs

@ -1,13 +1,13 @@
use std::collections::HashMap;
use ruma::{
RoomId, UserId,
api::client::error::ErrorKind,
events::{AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, RoomAccountDataEventType},
serde::Raw,
RoomId, UserId,
};
use crate::{database::KeyValueDatabase, service, services, utils, Error, Result};
use crate::{Error, Result, database::KeyValueDatabase, service, services, utils};
impl service::account_data::Data for KeyValueDatabase {
/// Places one event in the account data of the user and removes the previous entry.

2
src/database/key_value/appservice.rs → conduit/src/database/key_value/appservice.rs

@ -1,6 +1,6 @@
use ruma::api::appservice::Registration;
use crate::{database::KeyValueDatabase, service, utils, Error, Result};
use crate::{Error, Result, database::KeyValueDatabase, service, utils};
impl service::appservice::Data for KeyValueDatabase {
/// Registers an appservice and returns the ID to the caller

7
src/database/key_value/globals.rs → conduit/src/database/key_value/globals.rs

@ -1,18 +1,19 @@
use std::collections::HashMap;
use async_trait::async_trait;
use futures_util::{stream::FuturesUnordered, StreamExt};
use futures_util::{StreamExt, stream::FuturesUnordered};
use lru_cache::LruCache;
use ruma::{
DeviceId, ServerName, UserId,
api::federation::discovery::{OldVerifyKey, ServerSigningKeys},
signatures::Ed25519KeyPair,
DeviceId, ServerName, UserId,
};
use crate::{
Error, Result,
database::KeyValueDatabase,
service::{self, globals::SigningKeys},
services, utils, Error, Result,
services, utils,
};
pub const COUNTER: &[u8] = b"c";

4
src/database/key_value/key_backups.rs → conduit/src/database/key_value/key_backups.rs

@ -1,15 +1,15 @@
use std::collections::BTreeMap;
use ruma::{
OwnedRoomId, RoomId, UserId,
api::client::{
backup::{BackupAlgorithm, KeyBackupData, RoomKeyBackup},
error::ErrorKind,
},
serde::Raw,
OwnedRoomId, RoomId, UserId,
};
use crate::{database::KeyValueDatabase, service, services, utils, Error, Result};
use crate::{Error, Result, database::KeyValueDatabase, service, services, utils};
impl service::key_backups::Data for KeyValueDatabase {
fn create_backup(

67
src/database/key_value/media.rs → conduit/src/database/key_value/media.rs

@ -1,12 +1,13 @@
use std::{collections::BTreeMap, ops::Range, slice::Split};
use bytesize::ByteSize;
use ruma::{api::client::error::ErrorKind, OwnedServerName, ServerName, UserId};
use sha2::{digest::Output, Sha256};
use conduit_config::{MediaRetentionConfig, MediaRetentionScope};
use ruma::{OwnedServerName, ServerName, UserId, api::client::error::ErrorKind};
use sha2::{Sha256, digest::Output};
use tracing::error;
use crate::{
config::{MediaRetentionConfig, MediaRetentionScope},
Error, Result,
database::KeyValueDatabase,
service::{
self,
@ -15,7 +16,7 @@ use crate::{
MediaQueryFileInfo, MediaQueryThumbInfo, MediaType, ServerNameOrUserId,
},
},
services, utils, Error, Result,
services, utils,
};
impl service::media::Data for KeyValueDatabase {
@ -203,19 +204,7 @@ impl service::media::Data for KeyValueDatabase {
let is_blocked_via_filehash = self.is_blocked_filehash(&sha256_digest)?;
let time_info = if let Some(filehash_meta) = self
.filehash_metadata
.get(&sha256_digest)?
.map(FilehashMetadata::from_vec)
{
Some(FileInfo {
creation: filehash_meta.creation(&sha256_digest)?,
last_access: filehash_meta.last_access(&sha256_digest)?,
size: filehash_meta.size(&sha256_digest)?,
})
} else {
None
};
let file_info = self.file_info(&sha256_digest)?;
Some(MediaQueryFileInfo {
uploader_localpart,
@ -224,7 +213,7 @@ impl service::media::Data for KeyValueDatabase {
content_type,
unauthenticated_access_permitted,
is_blocked_via_filehash,
file_info: time_info,
file_info,
})
} else {
None
@ -612,9 +601,9 @@ impl service::media::Data for KeyValueDatabase {
self.servername_userlocalpart_mediaid
.scan_prefix(prefix)
.map(|(k, _)| {
let parts = k.split(|&b| b == 0xff);
let mut parts = k.split(|&b| b == 0xff);
let media_id = parts.last().ok_or_else(|| {
let media_id = parts.next_back().ok_or_else(|| {
Error::bad_database("Invalid format of key in blocked_servername_mediaid")
})?;
@ -664,9 +653,9 @@ impl service::media::Data for KeyValueDatabase {
}
} else {
error!(
"Invalid format of key in filehash_servername_mediaid for media with sha256 content hash of {}",
hex::encode(&metadata.sha256_digest)
);
"Invalid format of key in filehash_servername_mediaid for media with sha256 content hash of {}",
hex::encode(&metadata.sha256_digest)
);
errors.push(Error::BadDatabase(
"Invalid format of key in filehash_servername_mediaid",
));
@ -675,9 +664,9 @@ impl service::media::Data for KeyValueDatabase {
let thumbnail_id_error = || {
error!(
"Invalid format of key in filehash_thumbnail_id for media with sha256 content hash of {}",
hex::encode(&metadata.sha256_digest)
);
"Invalid format of key in filehash_thumbnail_id for media with sha256 content hash of {}",
hex::encode(&metadata.sha256_digest)
);
Error::BadDatabase("Invalid format of value in filehash_thumbnailid")
};
@ -732,7 +721,9 @@ impl service::media::Data for KeyValueDatabase {
}
Ok(None) => (),
Ok(Some(Err(e))) => {
error!("Error parsing metadata for \"mxc://{server_name}/{media_id}\" from servernamemediaid_metadata: {e}");
error!(
"Error parsing metadata for \"mxc://{server_name}/{media_id}\" from servernamemediaid_metadata: {e}"
);
errors.push(e);
continue;
}
@ -748,7 +739,9 @@ impl service::media::Data for KeyValueDatabase {
maybe_remove_remaining_metadata(&metadata, &mut errors);
}
Err(e) => {
error!("Error parsing metadata for thumbnail of \"mxc://{server_name}/{media_id}\" from thumbnailid_metadata: {e}");
error!(
"Error parsing metadata for thumbnail of \"mxc://{server_name}/{media_id}\" from thumbnailid_metadata: {e}"
);
errors.push(e);
}
}
@ -1353,6 +1346,24 @@ impl service::media::Data for KeyValueDatabase {
Ok(())
}
}
fn file_info(&self, sha256_digest: &[u8]) -> Result<Option<FileInfo>, Error> {
Ok(
if let Some(filehash_meta) = self
.filehash_metadata
.get(sha256_digest)?
.map(FilehashMetadata::from_vec)
{
Some(FileInfo {
creation: filehash_meta.creation(sha256_digest)?,
last_access: filehash_meta.last_access(sha256_digest)?,
size: filehash_meta.size(sha256_digest)?,
})
} else {
None
},
)
}
}
impl KeyValueDatabase {

0
src/database/key_value/mod.rs → conduit/src/database/key_value/mod.rs

4
src/database/key_value/pusher.rs → conduit/src/database/key_value/pusher.rs

@ -1,9 +1,9 @@
use ruma::{
api::client::push::{set_pusher, Pusher},
UserId,
api::client::push::{Pusher, set_pusher},
};
use crate::{database::KeyValueDatabase, service, utils, Error, Result};
use crate::{Error, Result, database::KeyValueDatabase, service, utils};
impl service::pusher::Data for KeyValueDatabase {
fn set_pusher(&self, sender: &UserId, pusher: set_pusher::v3::PusherAction) -> Result<()> {

6
src/database/key_value/rooms/alias.rs → conduit/src/database/key_value/rooms/alias.rs

@ -1,9 +1,9 @@
use ruma::{
api::client::error::ErrorKind, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId,
UserId,
OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, UserId,
api::client::error::ErrorKind,
};
use crate::{database::KeyValueDatabase, service, services, utils, Error, Result};
use crate::{Error, Result, database::KeyValueDatabase, service, services, utils};
impl service::rooms::alias::Data for KeyValueDatabase {
fn set_alias(&self, alias: &RoomAliasId, room_id: &RoomId, user_id: &UserId) -> Result<()> {

2
src/database/key_value/rooms/auth_chain.rs → conduit/src/database/key_value/rooms/auth_chain.rs

@ -1,6 +1,6 @@
use std::{collections::HashSet, mem::size_of, sync::Arc};
use crate::{database::KeyValueDatabase, service, utils, Result};
use crate::{Result, database::KeyValueDatabase, service, utils};
impl service::rooms::auth_chain::Data for KeyValueDatabase {
fn get_cached_eventid_authchain(&self, key: &[u64]) -> Result<Option<Arc<HashSet<u64>>>> {

2
src/database/key_value/rooms/directory.rs → conduit/src/database/key_value/rooms/directory.rs

@ -1,6 +1,6 @@
use ruma::{OwnedRoomId, RoomId};
use crate::{database::KeyValueDatabase, service, utils, Error, Result};
use crate::{Error, Result, database::KeyValueDatabase, service, utils};
impl service::rooms::directory::Data for KeyValueDatabase {
fn set_public(&self, room_id: &RoomId) -> Result<()> {

0
src/database/key_value/rooms/edus/mod.rs → conduit/src/database/key_value/rooms/edus/mod.rs

4
src/database/key_value/rooms/edus/presence.rs → conduit/src/database/key_value/rooms/edus/presence.rs

@ -1,10 +1,10 @@
use std::collections::HashMap;
use ruma::{
events::presence::PresenceEvent, presence::PresenceState, OwnedUserId, RoomId, UInt, UserId,
OwnedUserId, RoomId, UInt, UserId, events::presence::PresenceEvent, presence::PresenceState,
};
use crate::{database::KeyValueDatabase, service, services, utils, Error, Result};
use crate::{Error, Result, database::KeyValueDatabase, service, services, utils};
impl service::rooms::edus::presence::Data for KeyValueDatabase {
fn update_presence(

4
src/database/key_value/rooms/edus/read_receipt.rs → conduit/src/database/key_value/rooms/edus/read_receipt.rs

@ -1,8 +1,8 @@
use ruma::{
events::receipt::ReceiptEvent, serde::Raw, CanonicalJsonObject, OwnedUserId, RoomId, UserId,
CanonicalJsonObject, OwnedUserId, RoomId, UserId, events::receipt::ReceiptEvent, serde::Raw,
};
use crate::{database::KeyValueDatabase, service, services, utils, Error, Result};
use crate::{Error, Result, database::KeyValueDatabase, service, services, utils};
impl service::rooms::edus::read_receipt::Data for KeyValueDatabase {
fn readreceipt_update(

2
src/database/key_value/rooms/lazy_load.rs → conduit/src/database/key_value/rooms/lazy_load.rs

@ -1,6 +1,6 @@
use ruma::{DeviceId, RoomId, UserId};
use crate::{database::KeyValueDatabase, service, Result};
use crate::{Result, database::KeyValueDatabase, service};
impl service::rooms::lazy_loading::Data for KeyValueDatabase {
fn lazy_load_was_sent_before(

2
src/database/key_value/rooms/metadata.rs → conduit/src/database/key_value/rooms/metadata.rs

@ -1,6 +1,6 @@
use ruma::{OwnedRoomId, RoomId};
use crate::{database::KeyValueDatabase, service, services, utils, Error, Result};
use crate::{Error, Result, database::KeyValueDatabase, service, services, utils};
impl service::rooms::metadata::Data for KeyValueDatabase {
fn exists(&self, room_id: &RoomId) -> Result<bool> {

0
src/database/key_value/rooms/mod.rs → conduit/src/database/key_value/rooms/mod.rs

2
src/database/key_value/rooms/outlier.rs → conduit/src/database/key_value/rooms/outlier.rs

@ -1,6 +1,6 @@
use ruma::{CanonicalJsonObject, EventId};
use crate::{database::KeyValueDatabase, service, Error, PduEvent, Result};
use crate::{Error, PduEvent, Result, database::KeyValueDatabase, service};
impl service::rooms::outlier::Data for KeyValueDatabase {
fn get_outlier_pdu_json(&self, event_id: &EventId) -> Result<Option<CanonicalJsonObject>> {

3
src/database/key_value/rooms/pdu_metadata.rs → conduit/src/database/key_value/rooms/pdu_metadata.rs

@ -3,9 +3,10 @@ use std::sync::Arc;
use ruma::{EventId, RoomId, UserId};
use crate::{
Error, PduEvent, Result,
database::KeyValueDatabase,
service::{self, rooms::timeline::PduCount},
services, utils, Error, PduEvent, Result,
services, utils,
};
impl service::rooms::pdu_metadata::Data for KeyValueDatabase {

2
src/database/key_value/rooms/search.rs → conduit/src/database/key_value/rooms/search.rs

@ -1,6 +1,6 @@
use ruma::RoomId;
use crate::{database::KeyValueDatabase, service, services, utils, Result};
use crate::{Result, database::KeyValueDatabase, service, services, utils};
/// Splits a string into tokens used as keys in the search inverted index
///

4
src/database/key_value/rooms/short.rs → conduit/src/database/key_value/rooms/short.rs

@ -1,8 +1,8 @@
use std::sync::Arc;
use ruma::{events::StateEventType, EventId, RoomId};
use ruma::{EventId, RoomId, events::StateEventType};
use crate::{database::KeyValueDatabase, service, services, utils, Error, Result};
use crate::{Error, Result, database::KeyValueDatabase, service, services, utils};
impl service::rooms::short::Data for KeyValueDatabase {
fn get_or_create_shorteventid(&self, event_id: &EventId) -> Result<u64> {

2
src/database/key_value/rooms/state.rs → conduit/src/database/key_value/rooms/state.rs

@ -4,7 +4,7 @@ use std::collections::HashSet;
use std::sync::Arc;
use tokio::sync::MutexGuard;
use crate::{database::KeyValueDatabase, service, utils, Error, Result};
use crate::{Error, Result, database::KeyValueDatabase, service, utils};
impl service::rooms::state::Data for KeyValueDatabase {
fn get_room_shortstatehash(&self, room_id: &RoomId) -> Result<Option<u64>> {

4
src/database/key_value/rooms/state_accessor.rs → conduit/src/database/key_value/rooms/state_accessor.rs

@ -1,8 +1,8 @@
use std::{collections::HashMap, sync::Arc};
use crate::{database::KeyValueDatabase, service, services, utils, Error, PduEvent, Result};
use crate::{Error, PduEvent, Result, database::KeyValueDatabase, service, services, utils};
use async_trait::async_trait;
use ruma::{events::StateEventType, EventId, RoomId};
use ruma::{EventId, RoomId, events::StateEventType};
#[async_trait]
impl service::rooms::state_accessor::Data for KeyValueDatabase {

7
src/database/key_value/rooms/state_cache.rs → conduit/src/database/key_value/rooms/state_cache.rs

@ -1,15 +1,16 @@
use std::{collections::HashSet, sync::Arc};
use ruma::{
OwnedRoomId, OwnedServerName, OwnedUserId, RoomId, ServerName, UserId,
events::{AnyStrippedStateEvent, AnySyncStateEvent},
serde::Raw,
OwnedRoomId, OwnedServerName, OwnedUserId, RoomId, ServerName, UserId,
};
use crate::{
database::{abstraction::KvTree, KeyValueDatabase},
Error, Result,
database::{KeyValueDatabase, abstraction::KvTree},
service::{self, appservice::RegistrationInfo},
services, utils, Error, Result,
services, utils,
};
use super::{get_room_and_user_byte_ids, get_userroom_id_bytes};

3
src/database/key_value/rooms/state_compressor.rs → conduit/src/database/key_value/rooms/state_compressor.rs

@ -1,9 +1,10 @@
use std::{collections::HashSet, mem::size_of, sync::Arc};
use crate::{
Error, Result,
database::KeyValueDatabase,
service::{self, rooms::state_compressor::data::StateDiff},
utils, Error, Result,
utils,
};
impl service::rooms::state_compressor::Data for KeyValueDatabase {

4
src/database/key_value/rooms/threads.rs → conduit/src/database/key_value/rooms/threads.rs

@ -1,6 +1,6 @@
use ruma::{api::client::threads::get_threads::v1::IncludeThreads, OwnedUserId, RoomId, UserId};
use ruma::{OwnedUserId, RoomId, UserId, api::client::threads::get_threads::v1::IncludeThreads};
use crate::{database::KeyValueDatabase, service, services, utils, Error, PduEvent, Result};
use crate::{Error, PduEvent, Result, database::KeyValueDatabase, service, services, utils};
impl service::rooms::threads::Data for KeyValueDatabase {
fn threads_until<'a>(

10
src/database/key_value/rooms/timeline.rs → conduit/src/database/key_value/rooms/timeline.rs

@ -1,11 +1,11 @@
use std::{collections::hash_map, mem::size_of, sync::Arc};
use ruma::{
api::client::error::ErrorKind, CanonicalJsonObject, EventId, OwnedUserId, RoomId, UserId,
CanonicalJsonObject, EventId, OwnedUserId, RoomId, UserId, api::client::error::ErrorKind,
};
use tracing::error;
use crate::{database::KeyValueDatabase, service, services, utils, Error, PduEvent, Result};
use crate::{Error, PduEvent, Result, database::KeyValueDatabase, service, services, utils};
use service::rooms::timeline::PduCount;
@ -346,11 +346,7 @@ fn count_to_id(
pdu_id.extend_from_slice(&0_u64.to_be_bytes());
let num = u64::MAX - x;
if subtract {
if num > 0 {
num - offset
} else {
num
}
if num > 0 { num - offset } else { num }
} else {
num + offset
}

4
src/database/key_value/rooms/user.rs → conduit/src/database/key_value/rooms/user.rs

@ -1,6 +1,6 @@
use ruma::{OwnedRoomId, OwnedUserId, RoomId, UserId};
use crate::{database::KeyValueDatabase, service, services, utils, Error, Result};
use crate::{Error, Result, database::KeyValueDatabase, service, services, utils};
use super::{get_room_and_user_byte_ids, get_userroom_id_bytes};
@ -115,7 +115,7 @@ impl service::rooms::user::Data for KeyValueDatabase {
let roomid_index = key
.iter()
.enumerate()
.find(|(_, &b)| b == 0xff)
.find(|&(_, &b)| b == 0xff)
.ok_or_else(|| Error::bad_database("Invalid userroomid_joined in db."))?
.0
+ 1; // +1 because the room id starts AFTER the separator

3
src/database/key_value/sending.rs → conduit/src/database/key_value/sending.rs

@ -1,12 +1,13 @@
use ruma::{ServerName, UserId};
use crate::{
Error, Result,
database::KeyValueDatabase,
service::{
self,
sending::{OutgoingKind, SendingEventType},
},
services, utils, Error, Result,
services, utils,
};
impl service::sending::Data for KeyValueDatabase {

2
src/database/key_value/transaction_ids.rs → conduit/src/database/key_value/transaction_ids.rs

@ -1,6 +1,6 @@
use ruma::{DeviceId, TransactionId, UserId};
use crate::{database::KeyValueDatabase, service, Result};
use crate::{Result, database::KeyValueDatabase, service};
impl service::transaction_ids::Data for KeyValueDatabase {
fn add_txnid(

4
src/database/key_value/uiaa.rs → conduit/src/database/key_value/uiaa.rs

@ -1,9 +1,9 @@
use ruma::{
api::client::{error::ErrorKind, uiaa::UiaaInfo},
CanonicalJsonValue, DeviceId, UserId,
api::client::{error::ErrorKind, uiaa::UiaaInfo},
};
use crate::{database::KeyValueDatabase, service, Error, Result};
use crate::{Error, Result, database::KeyValueDatabase, service};
impl service::uiaa::Data for KeyValueDatabase {
fn set_uiaa_request(

7
src/database/key_value/users.rs → conduit/src/database/key_value/users.rs

@ -1,20 +1,21 @@
use std::{collections::BTreeMap, mem::size_of};
use ruma::{
DeviceId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OwnedDeviceId, OwnedMxcUri,
OwnedOneTimeKeyId, OwnedUserId, UInt, UserId,
api::client::{device::Device, error::ErrorKind, filter::FilterDefinition},
encryption::{CrossSigningKey, DeviceKeys, OneTimeKey},
events::{AnyToDeviceEvent, StateEventType},
serde::Raw,
DeviceId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OwnedDeviceId, OwnedMxcUri,
OwnedOneTimeKeyId, OwnedUserId, UInt, UserId,
};
use tracing::warn;
use crate::{
Error, Result,
api::client_server::TOKEN_LENGTH,
database::KeyValueDatabase,
service::{self, users::clean_signatures},
services, utils, Error, Result,
services, utils,
};
impl service::users::Data for KeyValueDatabase {

94
src/database/mod.rs → conduit/src/database/mod.rs

@ -2,30 +2,31 @@ pub mod abstraction;
pub mod key_value;
use crate::{
Config, Error, PduEvent, Result, SERVICES, Services,
service::{globals, rooms::timeline::PduCount},
services, utils, Config, Error, PduEvent, Result, Services, SERVICES,
services, utils,
};
use abstraction::{KeyValueDatabaseEngine, KvTree};
use base64::{engine::general_purpose, Engine};
use directories::ProjectDirs;
use base64::{Engine, engine::general_purpose};
use conduit_config::DatabaseBackend;
use key_value::media::FilehashMetadata;
use lru_cache::LruCache;
use ruma::{
CanonicalJsonValue, EventId, OwnedDeviceId, OwnedEventId, OwnedMxcUri, OwnedRoomId,
OwnedUserId, RoomId, UserId,
events::{
GlobalAccountDataEvent, GlobalAccountDataEventType, StateEventType,
push_rules::{PushRulesEvent, PushRulesEventContent},
room::message::RoomMessageEventContent,
GlobalAccountDataEvent, GlobalAccountDataEventType, StateEventType,
},
push::Ruleset,
CanonicalJsonValue, EventId, OwnedDeviceId, OwnedEventId, OwnedMxcUri, OwnedRoomId,
OwnedUserId, RoomId, UserId,
};
use serde::Deserialize;
use sha2::{Digest, Sha256};
use std::{
collections::{BTreeMap, HashMap, HashSet},
fs::{self, remove_dir_all},
fs,
io::Write,
mem::size_of,
path::{Path, PathBuf},
@ -214,18 +215,6 @@ pub struct KeyValueDatabase {
}
impl KeyValueDatabase {
/// Tries to remove the old database but ignores all errors.
pub fn try_remove(server_name: &str) -> Result<()> {
let mut path = ProjectDirs::from("xyz", "koesters", "conduit")
.ok_or_else(|| Error::bad_config("The OS didn't return a valid home directory path."))?
.data_dir()
.to_path_buf();
path.push(server_name);
let _ = remove_dir_all(path);
Ok(())
}
fn check_db_setup(config: &Config) -> Result<()> {
let path = Path::new(&config.database_path);
@ -252,22 +241,32 @@ impl KeyValueDatabase {
return Ok(());
}
if sled_exists && config.database_backend != "sled" {
return Err(Error::bad_config(
if sled_exists {
return Err(Error::Initialization(
"Found sled at database_path, but is not specified in config.",
));
}
if sqlite_exists && config.database_backend != "sqlite" {
return Err(Error::bad_config(
"Found sqlite at database_path, but is not specified in config.",
));
}
if rocksdb_exists && config.database_backend != "rocksdb" {
return Err(Error::bad_config(
"Found rocksdb at database_path, but is not specified in config.",
));
// Only works as these are the only two active backends currently. If there ever were
// to be more than 2, this could get complicated due to there not being attributes
// available on expressions yet: https://github.com/rust-lang/rust/issues/15701
match config.database_backend {
#[cfg(feature = "rocksdb")]
DatabaseBackend::RocksDB => {
if sqlite_exists {
return Err(Error::Initialization(
"Found sqlite at database_path, but is not specified in config.",
));
}
}
#[cfg(feature = "sqlite")]
DatabaseBackend::SQLite => {
if rocksdb_exists {
return Err(Error::Initialization(
"Found rocksdb at database_path, but is not specified in config.",
));
}
}
}
Ok(())
@ -279,23 +278,18 @@ impl KeyValueDatabase {
if !Path::new(&config.database_path).exists() {
fs::create_dir_all(&config.database_path)
.map_err(|_| Error::BadConfig("Database folder doesn't exists and couldn't be created (e.g. due to missing permissions). Please create the database folder yourself."))?;
.map_err(|_| Error::Initialization("Database folder doesn't exists and couldn't be created (e.g. due to missing permissions). Please create the database folder yourself."))?;
}
let builder: Arc<dyn KeyValueDatabaseEngine> = match &*config.database_backend {
let builder: Arc<dyn KeyValueDatabaseEngine> = match &config.database_backend {
#[cfg(feature = "sqlite")]
"sqlite" => Arc::new(Arc::<abstraction::sqlite::Engine>::open(&config)?),
DatabaseBackend::SQLite => Arc::new(Arc::<abstraction::sqlite::Engine>::open(&config)?),
#[cfg(feature = "rocksdb")]
"rocksdb" => Arc::new(Arc::<abstraction::rocksdb::Engine>::open(&config)?),
_ => {
return Err(Error::BadConfig("Database backend not found."));
DatabaseBackend::RocksDB => {
Arc::new(Arc::<abstraction::rocksdb::Engine>::open(&config)?)
}
};
if config.registration_token == Some(String::new()) {
return Err(Error::bad_config("Registration token is empty"));
}
if config.max_request_size < 1024 {
error!(?config.max_request_size, "Max request size is less than 1KB. Please increase it.");
}
@ -453,7 +447,7 @@ impl KeyValueDatabase {
conduit_user
);
return Err(Error::bad_database(
"Cannot reuse an existing database after changing the server name, please delete the old one first."
"Cannot reuse an existing database after changing the server name, please delete the old one first.",
));
}
}
@ -1007,7 +1001,9 @@ impl KeyValueDatabase {
}
if services().globals.database_version()? < 17 {
warn!("Migrating media repository to new format. If you have a lot of media stored, this may take a while, so please be patiant!");
warn!(
"Migrating media repository to new format. If you have a lot of media stored, this may take a while, so please be patiant!"
);
let tree = db._db.open_tree("mediaid_file")?;
tree.clear().unwrap();
@ -1056,9 +1052,9 @@ impl KeyValueDatabase {
}
if services().globals.database_version()? < 18 {
if let crate::config::MediaBackendConfig::FileSystem {
if let conduit_config::MediaBackendConfig::FileSystem {
path,
directory_structure: crate::config::DirectoryStructure::Deep { length, depth },
directory_structure: conduit_config::DirectoryStructure::Deep { length, depth },
} = &services().globals.config.media.backend
{
for file in fs::read_dir(path)
@ -1074,7 +1070,7 @@ impl KeyValueDatabase {
file.path(),
services().globals.get_media_path(
path.as_str(),
&crate::config::DirectoryStructure::Deep {
&conduit_config::DirectoryStructure::Deep {
length: *length,
depth: *depth,
},
@ -1124,7 +1120,9 @@ impl KeyValueDatabase {
match set_emergency_access() {
Ok(pwd_set) => {
if pwd_set {
warn!("The Conduit account emergency password is set! Please unset it as soon as you finish admin account recovery!");
warn!(
"The Conduit account emergency password is set! Please unset it as soon as you finish admin account recovery!"
);
services().admin.send_message(RoomMessageEventContent::text_plain("The Conduit account emergency password is set! Please unset it as soon as you finish admin account recovery!"));
}
}
@ -1217,7 +1215,7 @@ impl KeyValueDatabase {
#[tracing::instrument]
pub async fn start_cleanup_task() {
#[cfg(unix)]
use tokio::signal::unix::{signal, SignalKind};
use tokio::signal::unix::{SignalKind, signal};
use std::time::{Duration, Instant};

5
src/lib.rs → conduit/src/lib.rs

@ -1,6 +1,5 @@
pub mod api;
pub mod clap;
mod config;
mod database;
mod service;
mod utils;
@ -14,10 +13,10 @@ use std::{
};
pub use api::ruma_wrapper::{Ruma, RumaResponse};
pub use config::Config;
pub use conduit_config::Config;
pub use database::KeyValueDatabase;
use ruma::api::{MatrixVersion, SupportedVersions};
pub use service::{pdu::PduEvent, Services};
pub use service::{Services, pdu::PduEvent};
pub use utils::error::{Error, Result};
pub static SERVICES: RwLock<Option<&'static Services>> = RwLock::new(None);

59
src/main.rs → conduit/src/main.rs

@ -1,41 +1,41 @@
use std::{future::Future, io, net::SocketAddr, sync::atomic, time::Duration};
use axum::{
Router,
body::Body,
extract::{FromRequestParts, MatchedPath},
middleware::map_response,
response::{IntoResponse, Response},
routing::{any, get, on, MethodFilter},
Router,
routing::{MethodFilter, any, get, on},
};
use axum_server::{bind, bind_rustls, tls_rustls::RustlsConfig, Handle as ServerHandle};
use axum_server::{Handle as ServerHandle, bind, bind_rustls, tls_rustls::RustlsConfig};
use conduit::api::{client_server, server_server};
use figment::{
Figment,
providers::{Env, Format, Toml},
value::Uncased,
Figment,
};
use http::{
header::{self, HeaderName, CONTENT_SECURITY_POLICY},
Method, StatusCode, Uri,
header::{self, CONTENT_SECURITY_POLICY, HeaderName},
};
use opentelemetry::trace::TracerProvider;
use ruma::api::{
IncomingRequest,
client::{
error::{Error as RumaError, ErrorBody, ErrorKind},
uiaa::UiaaResponse,
},
IncomingRequest,
};
use tokio::signal;
use tower::ServiceBuilder;
use tower_http::{
ServiceBuilderExt as _,
cors::{self, CorsLayer},
trace::TraceLayer,
ServiceBuilderExt as _,
};
use tracing::{debug, error, info, warn};
use tracing_subscriber::{prelude::*, EnvFilter};
use tracing_subscriber::{EnvFilter, prelude::*};
pub use conduit::*; // Re-export everything from the library crate
@ -52,6 +52,15 @@ static SUB_TABLES: [&str; 3] = ["well_known", "tls", "media"]; // Not doing `pro
// this is what we have to deal with. Also see: https://github.com/SergioBenitez/Figment/issues/12#issuecomment-801449465
static SUB_SUB_TABLES: [&str; 2] = ["directory_structure", "retention"];
const DEPRECATED_KEYS: &[&str] = &[
"cache_capacity",
"turn_username",
"turn_password",
"turn_uris",
"turn_secret",
"turn_ttl",
];
#[tokio::main]
async fn main() {
clap::parse();
@ -104,7 +113,21 @@ async fn main() {
}
};
config.warn_deprecated();
let mut was_deprecated = false;
for key in config
.catchall
.keys()
.filter(|key| DEPRECATED_KEYS.iter().any(|s| s == key))
{
warn!("Config parameter {} is deprecated", key);
was_deprecated = true;
}
if was_deprecated {
warn!(
"Read conduit documentation and check your configuration if any new configuration parameters should be adjusted"
);
}
let jaeger = if config.allow_jaeger {
opentelemetry::global::set_text_map_propagator(
@ -157,7 +180,9 @@ async fn main() {
let filter_layer = match EnvFilter::try_new(&config.log) {
Ok(s) => s,
Err(e) => {
eprintln!("It looks like your config is invalid. The following error occurred while parsing it: {e}");
eprintln!(
"It looks like your config is invalid. The following error occurred while parsing it: {e}"
);
EnvFilter::try_new("warn").unwrap()
}
};
@ -196,7 +221,11 @@ async fn main() {
/// Adds additional headers to prevent any potential XSS attacks via the media repo
async fn set_csp_header(response: Response) -> impl IntoResponse {
(
[(CONTENT_SECURITY_POLICY, "sandbox; default-src 'none'; script-src 'none'; plugin-types application/pdf; style-src 'unsafe-inline'; object-src 'self';")], response
[(
CONTENT_SECURITY_POLICY,
"sandbox; default-src 'none'; script-src 'none'; plugin-types application/pdf; style-src 'unsafe-inline'; object-src 'self';",
)],
response,
)
}
@ -242,7 +271,9 @@ async fn run_server() -> io::Result<()> {
)
.layer(map_response(set_csp_header));
let app = routes(config).layer(middlewares).into_make_service();
let app = routes(config)
.layer(middlewares)
.into_make_service_with_connect_info::<SocketAddr>();
let handle = ServerHandle::new();
tokio::spawn(shutdown_signal(handle.clone()));
@ -544,7 +575,7 @@ async fn shutdown_signal(handle: ServerHandle) {
}
async fn federation_disabled(_: Uri) -> impl IntoResponse {
Error::bad_config("Federation is disabled.")
Error::BadServerResponse("Federation is disabled.")
}
async fn not_found(uri: Uri) -> impl IntoResponse {
@ -644,7 +675,7 @@ fn method_to_filter(method: Method) -> MethodFilter {
#[cfg(unix)]
#[tracing::instrument(err)]
fn maximize_fd_limit() -> Result<(), nix::errno::Errno> {
use nix::sys::resource::{getrlimit, setrlimit, Resource};
use nix::sys::resource::{Resource, getrlimit, setrlimit};
let res = Resource::RLIMIT_NOFILE;

2
src/service/account_data/data.rs → conduit/src/service/account_data/data.rs

@ -2,9 +2,9 @@ use std::collections::HashMap;
use crate::Result;
use ruma::{
RoomId, UserId,
events::{AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, RoomAccountDataEventType},
serde::Raw,
RoomId, UserId,
};
pub trait Data: Send + Sync {

2
src/service/account_data/mod.rs → conduit/src/service/account_data/mod.rs

@ -3,9 +3,9 @@ mod data;
pub use data::Data;
use ruma::{
RoomId, UserId,
events::{AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, RoomAccountDataEventType},
serde::Raw,
RoomId, UserId,
};
use std::collections::HashMap;

37
src/service/admin/mod.rs → conduit/src/service/admin/mod.rs

@ -12,9 +12,13 @@ use clap::{Args, Parser};
use image::GenericImageView;
use regex::Regex;
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId,
OwnedServerName, RoomAliasId, RoomId, RoomVersionId, ServerName, UserId,
api::appservice::Registration,
events::{
TimelineEventType,
room::{
MediaSource,
canonical_alias::RoomCanonicalAliasEventContent,
create::RoomCreateEventContent,
guest_access::{GuestAccess, RoomGuestAccessEventContent},
@ -28,28 +32,25 @@ use ruma::{
name::RoomNameEventContent,
power_levels::RoomPowerLevelsEventContent,
topic::RoomTopicEventContent,
MediaSource,
},
TimelineEventType,
},
room_version_rules::RoomVersionRules,
EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId,
OwnedServerName, RoomAliasId, RoomId, RoomVersionId, ServerName, UserId,
};
use serde_json::value::to_raw_value;
use tokio::sync::{mpsc, Mutex, RwLock};
use tokio::sync::{Mutex, RwLock, mpsc};
use crate::{
api::client_server::{self, leave_all_rooms, AUTO_GEN_PASSWORD_LENGTH},
Error, PduEvent, Result,
api::client_server::{self, AUTO_GEN_PASSWORD_LENGTH, leave_all_rooms},
service::rate_limiting::Target,
services,
utils::{self, HtmlEscape},
Error, PduEvent, Result,
};
use super::{
media::{
size, BlockedMediaInfo, FileInfo, MediaListItem, MediaQuery, MediaQueryFileInfo,
MediaQueryThumbInfo, ServerNameOrUserId,
BlockedMediaInfo, FileInfo, MediaListItem, MediaQuery, MediaQueryFileInfo,
MediaQueryThumbInfo, ServerNameOrUserId, size,
},
pdu::PduBuilder,
};
@ -415,7 +416,7 @@ pub struct ListMediaArgs {
#[derive(Debug)]
pub enum AdminRoomEvent {
ProcessMessage(String),
SendMessage(RoomMessageEventContent),
SendMessage(Box<RoomMessageEventContent>),
}
pub struct Service {
@ -451,7 +452,7 @@ impl Service {
tokio::select! {
Some(event) = receiver.recv() => {
let message_content = match event {
AdminRoomEvent::SendMessage(content) => content.into(),
AdminRoomEvent::SendMessage(content) => (*content).into(),
AdminRoomEvent::ProcessMessage(room_message) => self.process_admin_message(room_message).await,
};
@ -498,7 +499,7 @@ impl Service {
pub fn send_message(&self, message_content: RoomMessageEventContent) {
self.sender
.send(AdminRoomEvent::SendMessage(message_content))
.send(AdminRoomEvent::SendMessage(Box::new(message_content)))
.unwrap();
}
@ -794,7 +795,7 @@ impl Service {
return Ok(RoomMessageEventContent::text_plain(format!(
"The supplied username is not a valid username: {e}"
))
.into())
.into());
}
};
@ -849,7 +850,7 @@ impl Service {
return Ok(RoomMessageEventContent::text_plain(format!(
"The supplied username is not a valid username: {e}"
))
.into())
.into());
}
};
@ -1174,8 +1175,12 @@ impl Service {
file,
content_type,
content_disposition,
} = client_server::media::get_content(server_name, media_id.to_owned(), true, true)
.await?;
} = client_server::media::get_content(
server_name,
media_id.to_owned(),
Some(Target::User(services().globals.server_user().to_owned())),
)
.await?;
if let Ok(image) = image::load_from_memory(&file) {
let filename = content_disposition.and_then(|cd| cd.filename);

0
src/service/appservice/data.rs → conduit/src/service/appservice/data.rs

4
src/service/appservice/mod.rs → conduit/src/service/appservice/mod.rs

@ -7,12 +7,12 @@ pub use data::Data;
use futures_util::Future;
use regex::RegexSet;
use ruma::{
api::appservice::{Namespace, Registration},
RoomAliasId, RoomId, UserId,
api::appservice::{Namespace, Registration},
};
use tokio::sync::RwLock;
use crate::{services, Result};
use crate::{Result, services};
/// Compiled regular expressions for a namespace.
#[derive(Clone, Debug)]

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save