Compare commits

..

15 Commits

Author SHA1 Message Date
strawberry af77615e8a implement MSC4166, returning M_NOT_FOUND if no TURN URIs are set
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-07-30 00:11:16 -04:00
strawberry f91e40bc44 rust: bump console-subscriber to 0.4
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-07-29 19:18:56 -04:00
strawberry 111f2223bd nix: use cortex-a73 / ARMv8-a, strip and remove unnecessary bloat
for some reason gcc is included in OCI images

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-07-29 19:11:02 -04:00
strawberry 1d97858aa6 add support for MSC4168: migrate m.room.* on room upgrades
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-07-29 01:04:26 -04:00
strawberry 2834d50ddf display success/failure count in get-remote-pdu-list
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-07-28 21:38:14 -04:00
strawberry 3c9b702527 docs: mention FreeBSD specific build commandline for deployment
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-07-28 21:34:08 -04:00
strawberry a8c707d8a5 docs: update configuration docs for the new -O option
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-07-28 16:46:20 -04:00
strawberry 85c5b11e73 bump tokio, use default-features = false for more crates
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-07-28 15:15:39 -04:00
strawberry e0d3180303 docs: mention potential DNS issues when using docker
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-07-28 12:53:45 -04:00
strawberry 1e059d209c dont consider presence status changes apart of presence spam
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-07-28 12:53:45 -04:00
strawberry 8e690f73d6 debian: remove --verbose from postinst
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-07-28 12:53:45 -04:00
strawberry 547bc2171c reduce unnecessary logging on URL preview and event, use sensible error code for URL previews
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-07-28 12:53:45 -04:00
strawberry 275075a2f5 apply forbidden_remote_server_names to outbound sending and inbound federation handling
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-07-28 12:53:45 -04:00
strawberry 335be12f79 set last_seen_ip on new/initial device creation
this is not automatically updating, but at least have something
useful there instead of nothing

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-07-28 12:53:45 -04:00
strawberry fd74a0ac3d sync: always include_redundant_members when using element_hacks feature
see:
- https://github.com/matrix-org/matrix-js-sdk/pull/4329
- https://github.com/element-hq/synapse/blob/568051c0f07393b786b9d813a1db53dd332c9fc2/synapse/handlers/pagination.py#L639
- https://matrix.to/#/!n8DKU1BeeJilOJXDPr:seirdy.one/$gvGF7GuPrLPR8nHg4UiHvP-ZVwuiMkYS8O6VlsDCW9w?via=awawawawawawawawawawawawawawawawawawawawawawawawawawawawawawaw.gay&via=transfem.dev&via=matrix.org

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-07-28 12:53:45 -04:00
63 changed files with 1164 additions and 1158 deletions
Generated
+43 -74
View File
@@ -71,12 +71,6 @@ dependencies = [
"password-hash", "password-hash",
] ]
[[package]]
name = "arrayvec"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]] [[package]]
name = "as_variant" name = "as_variant"
version = "1.2.0" version = "1.2.0"
@@ -400,9 +394,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.16.3" version = "1.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "102087e286b4677862ea56cf8fc58bb2cdfa8725c40ffb80fe3a008eb7f2fc83" checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
@@ -418,9 +412,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.7.0" version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fca2be1d5c43812bae364ee3f30b3afcb7877cf59f4aeb94c66f313a41d2fac9" checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952"
[[package]] [[package]]
name = "bzip2-sys" name = "bzip2-sys"
@@ -445,9 +439,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.1.7" version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
@@ -505,9 +499,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.13" version = "4.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc" checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -515,9 +509,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.13" version = "4.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99" checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"clap_lex", "clap_lex",
@@ -525,9 +519,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.13" version = "4.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@@ -614,6 +608,7 @@ dependencies = [
"http", "http",
"http-body-util", "http-body-util",
"hyper", "hyper",
"image",
"ipaddress", "ipaddress",
"jsonwebtoken", "jsonwebtoken",
"log", "log",
@@ -626,6 +621,7 @@ dependencies = [
"sha-1", "sha-1",
"tokio", "tokio",
"tracing", "tracing",
"webpage",
] ]
[[package]] [[package]]
@@ -633,13 +629,11 @@ name = "conduit_core"
version = "0.4.6" version = "0.4.6"
dependencies = [ dependencies = [
"argon2", "argon2",
"arrayvec",
"axum", "axum",
"bytes", "bytes",
"cargo_toml", "cargo_toml",
"checked_ops", "checked_ops",
"chrono", "chrono",
"clap",
"conduit_macros", "conduit_macros",
"const-str", "const-str",
"ctor", "ctor",
@@ -763,7 +757,6 @@ dependencies = [
"tokio", "tokio",
"tracing", "tracing",
"url", "url",
"webpage",
] ]
[[package]] [[package]]
@@ -1347,7 +1340,7 @@ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"http", "http",
"indexmap 2.3.0", "indexmap 2.2.6",
"slab", "slab",
"tokio", "tokio",
"tokio-util", "tokio-util",
@@ -1716,9 +1709,9 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.3.0" version = "2.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.14.5", "hashbrown 0.14.5",
@@ -2255,7 +2248,7 @@ checksum = "1e32339a5dc40459130b3bd269e9892439f55b33e772d2a9d402a789baaf4e8a"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"indexmap 2.3.0", "indexmap 2.2.6",
"js-sys", "js-sys",
"once_cell", "once_cell",
"pin-project-lite", "pin-project-lite",
@@ -2302,7 +2295,7 @@ dependencies = [
"glob", "glob",
"once_cell", "once_cell",
"opentelemetry", "opentelemetry",
"ordered-float 4.2.2", "ordered-float 4.2.1",
"percent-encoding", "percent-encoding",
"rand", "rand",
"thiserror", "thiserror",
@@ -2321,9 +2314,9 @@ dependencies = [
[[package]] [[package]]
name = "ordered-float" name = "ordered-float"
version = "4.2.2" version = "4.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a91171844676f8c7990ce64959210cd2eaef32c2612c50f9fae9f8aaa6065a6" checksum = "19ff2cf528c6c03d9ed653d6c4ce1dc0582dc4af309790ad92f07c1cd551b0be"
dependencies = [ dependencies = [
"num-traits", "num-traits",
] ]
@@ -2550,12 +2543,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.18" version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dee4364d9f3b902ef14fab8a1ddffb783a1cb6b4bba3bfc1fa3922732c7de97f" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
dependencies = [
"zerocopy",
]
[[package]] [[package]]
name = "precomputed-hash" name = "precomputed-hash"
@@ -2935,7 +2925,7 @@ dependencies = [
"bytes", "bytes",
"form_urlencoded", "form_urlencoded",
"http", "http",
"indexmap 2.3.0", "indexmap 2.2.6",
"js_int", "js_int",
"konst", "konst",
"percent-encoding", "percent-encoding",
@@ -2961,7 +2951,7 @@ version = "0.28.1"
source = "git+https://github.com/girlbossceo/ruwuma?rev=c76e2873c1593a3308d4ba3e0e4a1db65acf8536#c76e2873c1593a3308d4ba3e0e4a1db65acf8536" source = "git+https://github.com/girlbossceo/ruwuma?rev=c76e2873c1593a3308d4ba3e0e4a1db65acf8536#c76e2873c1593a3308d4ba3e0e4a1db65acf8536"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"indexmap 2.3.0", "indexmap 2.2.6",
"js_int", "js_int",
"js_option", "js_option",
"percent-encoding", "percent-encoding",
@@ -3485,7 +3475,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de514ef58196f1fc96dcaef80fe6170a1ce6215df9687a93fe8300e773fefc5" checksum = "8de514ef58196f1fc96dcaef80fe6170a1ce6215df9687a93fe8300e773fefc5"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"indexmap 2.3.0", "indexmap 2.2.6",
"itoa", "itoa",
"ryu", "ryu",
"serde", "serde",
@@ -3550,7 +3540,7 @@ version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [ dependencies = [
"indexmap 2.3.0", "indexmap 2.2.6",
"itoa", "itoa",
"ryu", "ryu",
"serde", "serde",
@@ -3617,9 +3607,9 @@ dependencies = [
[[package]] [[package]]
name = "signal-hook-mio" name = "signal-hook-mio"
version = "0.2.4" version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
dependencies = [ dependencies = [
"libc", "libc",
"mio 0.8.11", "mio 0.8.11",
@@ -4020,9 +4010,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-socks" name = "tokio-socks"
version = "0.5.2" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0"
dependencies = [ dependencies = [
"either", "either",
"futures-util", "futures-util",
@@ -4056,21 +4046,21 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.19" version = "0.8.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" checksum = "81967dd0dd2c1ab0bc3468bd7caecc32b8a4aa47d0c8c695d8c2b2108168d62c"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
"toml_edit 0.22.20", "toml_edit 0.22.17",
] ]
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.8" version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@@ -4081,22 +4071,22 @@ version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
dependencies = [ dependencies = [
"indexmap 2.3.0", "indexmap 2.2.6",
"toml_datetime", "toml_datetime",
"winnow 0.5.40", "winnow 0.5.40",
] ]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.20" version = "0.22.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" checksum = "8d9f8729f5aea9562aac1cc0441f5d6de3cff1ee0c5d67293eeca5eb36ee7c16"
dependencies = [ dependencies = [
"indexmap 2.3.0", "indexmap 2.2.6",
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
"winnow 0.6.18", "winnow 0.6.16",
] ]
[[package]] [[package]]
@@ -4777,9 +4767,9 @@ dependencies = [
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.6.18" version = "0.6.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" checksum = "b480ae9340fc261e6be3e95a1ba86d54ae3f9171132a73ce8d4bbaf68339507c"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -4821,27 +4811,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "zerocopy"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6"
dependencies = [
"byteorder",
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.72",
]
[[package]] [[package]]
name = "zeroize" name = "zeroize"
version = "1.8.1" version = "1.8.1"
-10
View File
@@ -569,17 +569,7 @@ lto = "fat"
[profile.release-max-perf.build-override] [profile.release-max-perf.build-override]
inherits = "release-max-perf" inherits = "release-max-perf"
opt-level = 0 opt-level = 0
codegen-units = 32
#rustflags = [ #rustflags = [
# '-Crelocation-model=pic',
# '-Ctarget-feature=-crt-static',
# '-Clink-arg=-Wl,--no-gc-sections',
#]
[profile.release-max-perf.package.conduit_macros]
inherits = "release-max-perf.build-override"
#rustflags = [
# '-Crelocation-model=pic',
# '-Ctarget-feature=-crt-static', # '-Ctarget-feature=-crt-static',
#] #]
+5 -28
View File
@@ -224,9 +224,11 @@ registration_token = "change this token for something specific to your server"
# No default. # No default.
# forbidden_alias_names = [] # forbidden_alias_names = []
# List of forbidden server names that we will block all client room joins, incoming federated room directory requests, incoming federated invites for, and incoming federated joins. This check is applied on the room ID, room alias, sender server name, and sender user's server name. # List of forbidden server names that we will block incoming AND outgoing federation with, and block client room joins / remote user invites.
# Basically "global" ACLs. For our user (client) checks, admin users are allowed. #
# No default. # This check is applied on the room ID, room alias, sender server name, sender user's server name, inbound federation X-Matrix origin, and outbound federation handler.
#
# Basically "global" ACLs. No default.
# forbidden_remote_server_names = [] # forbidden_remote_server_names = []
# List of forbidden server names that we will block all outgoing federated room directory requests for. Useful for preventing our users from wandering into bad servers or spaces. # List of forbidden server names that we will block all outgoing federated room directory requests for. Useful for preventing our users from wandering into bad servers or spaces.
@@ -514,31 +516,6 @@ allow_profile_lookup_federation_requests = true
# Defaults to false as this uses more CPU when compressing. # Defaults to false as this uses more CPU when compressing.
#rocksdb_bottommost_compression = false #rocksdb_bottommost_compression = false
# Level of statistics collection. Some admin commands to display database statistics may require
# this option to be set. Database performance may be impacted by higher settings.
#
# Option is a number ranging from 0 to 6:
# 0 = No statistics.
# 1 = No statistics in release mode (default).
# 2 to 3 = Statistics with no performance impact.
# 3 to 5 = Statistics with possible performance impact.
# 6 = All statistics.
#
# Defaults to 1 (No statistics, except in debug-mode)
#rocksdb_stats_level = 1
# Database repair mode (for RocksDB SST corruption)
#
# Use this option when the server reports corruption while running or panics. If the server refuses
# to start use the recovery mode options first. Corruption errors containing the acronym 'SST' which
# occur after startup will likely require this option.
#
# - Backing up your database directory is recommended prior to running the repair.
# - Disabling repair mode and restarting the server is recommended after running the repair.
#
# Defaults to false
#rocksdb_repair = false
# Database recovery mode (for RocksDB WAL corruption) # Database recovery mode (for RocksDB WAL corruption)
# #
# Use this option when the server reports corruption and refuses to start. Set mode 2 (PointInTime) # Use this option when the server reports corruption and refuses to start. Set mode 2 (PointInTime)
+2 -2
View File
@@ -17,9 +17,9 @@ OCI images for conduwuit are available in the registries listed below.
| GitLab Registry | [registry.gitlab.com/conduwuit/conduwuit:main][gl] | ![Image Size][shield-main] | Stable main branch. | | GitLab Registry | [registry.gitlab.com/conduwuit/conduwuit:main][gl] | ![Image Size][shield-main] | Stable main branch. |
| Docker Hub | [docker.io/girlbossceo/conduwuit:main][dh] | ![Image Size][shield-main] | Stable main branch. | | Docker Hub | [docker.io/girlbossceo/conduwuit:main][dh] | ![Image Size][shield-main] | Stable main branch. |
[dh]: https://hub.docker.com/r/girlbossceo/conduwuit [dh]: https://hub.docker.com/repository/docker/girlbossceo/conduwuit
[gh]: https://github.com/girlbossceo/conduwuit/pkgs/container/conduwuit [gh]: https://github.com/girlbossceo/conduwuit/pkgs/container/conduwuit
[gl]: https://gitlab.com/conduwuit/conduwuit/container_registry/6369729 [gl]: https://gitlab.com/conduwuit/conduwuit/container_registry/6351657
[shield-latest]: https://img.shields.io/docker/image-size/girlbossceo/conduwuit/latest [shield-latest]: https://img.shields.io/docker/image-size/girlbossceo/conduwuit/latest
[shield-main]: https://img.shields.io/docker/image-size/girlbossceo/conduwuit/main [shield-main]: https://img.shields.io/docker/image-size/girlbossceo/conduwuit/main
Generated
+3 -3
View File
@@ -81,11 +81,11 @@
"complement": { "complement": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1722323564, "lastModified": 1720637557,
"narHash": "sha256-6w6/N8walz4Ayc9zu7iySqJRmGFukhkaICLn4dweAcA=", "narHash": "sha256-oZz6nCmFmdJZpC+K1iOG2KkzTI6rlAmndxANPDVU7X0=",
"owner": "matrix-org", "owner": "matrix-org",
"repo": "complement", "repo": "complement",
"rev": "6e4426a9e63233f9821a4d2382bfed145244183f", "rev": "0d14432e010482ea9e13a6f7c47c1533c0c9d62f",
"type": "github" "type": "github"
}, },
"original": { "original": {
+1 -4
View File
@@ -1,9 +1,6 @@
use std::time::SystemTime; use service::Services;
use conduit_service::Services;
pub(crate) struct Command<'a> { pub(crate) struct Command<'a> {
pub(crate) services: &'a Services, pub(crate) services: &'a Services,
pub(crate) body: &'a [&'a str], pub(crate) body: &'a [&'a str],
pub(crate) timer: SystemTime,
} }
+35 -32
View File
@@ -1,12 +1,16 @@
use std::{ use std::{
collections::{BTreeMap, HashMap}, collections::{BTreeMap, HashMap},
fmt::Write, fmt::Write,
sync::Arc, sync::{Arc, Mutex},
time::{Instant, SystemTime}, time::{Instant, SystemTime},
}; };
use api::client::validate_and_add_event_id; use api::client::validate_and_add_event_id;
use conduit::{debug, debug_error, err, info, trace, utils, warn, Error, PduEvent, Result}; use conduit::{
debug, info, log,
log::{capture, Capture},
utils, warn, Error, PduEvent, Result,
};
use ruma::{ use ruma::{
api::{client::error::ErrorKind, federation::event::get_room_state}, api::{client::error::ErrorKind, federation::event::get_room_state},
events::room::message::RoomMessageEventContent, events::room::message::RoomMessageEventContent,
@@ -209,7 +213,7 @@ pub(super) async fn get_remote_pdu(
Error::BadRequest(ErrorKind::Unknown, "Received response from server but failed to parse PDU") Error::BadRequest(ErrorKind::Unknown, "Received response from server but failed to parse PDU")
})?; })?;
trace!("Attempting to parse PDU: {:?}", &response.pdu); debug!("Attempting to parse PDU: {:?}", &response.pdu);
let parsed_pdu = { let parsed_pdu = {
let parsed_result = self let parsed_result = self
.services .services
@@ -234,7 +238,8 @@ pub(super) async fn get_remote_pdu(
debug!("Attempting to fetch homeserver signing keys for {server}"); debug!("Attempting to fetch homeserver signing keys for {server}");
self.services self.services
.server_keys .rooms
.event_handler
.fetch_required_signing_keys(parsed_pdu.iter().map(|(_event_id, event, _room_id)| event), &pub_key_map) .fetch_required_signing_keys(parsed_pdu.iter().map(|(_event_id, event, _room_id)| event), &pub_key_map)
.await .await
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
@@ -447,7 +452,8 @@ pub(super) async fn verify_json(&self) -> Result<RoomMessageEventContent> {
let pub_key_map = RwLock::new(BTreeMap::new()); let pub_key_map = RwLock::new(BTreeMap::new());
self.services self.services
.server_keys .rooms
.event_handler
.fetch_required_signing_keys([&value], &pub_key_map) .fetch_required_signing_keys([&value], &pub_key_map)
.await?; .await?;
@@ -565,7 +571,8 @@ pub(super) async fn force_set_room_state_from_server(
info!("Fetching required signing keys for all the state events we got"); info!("Fetching required signing keys for all the state events we got");
self.services self.services
.server_keys .rooms
.event_handler
.fetch_required_signing_keys(events.iter().map(|(_event_id, event, _room_id)| event), &pub_key_map) .fetch_required_signing_keys(events.iter().map(|(_event_id, event, _room_id)| event), &pub_key_map)
.await?; .await?;
@@ -580,8 +587,8 @@ pub(super) async fn force_set_room_state_from_server(
}; };
let pdu = PduEvent::from_id_val(&event_id, value.clone()).map_err(|e| { let pdu = PduEvent::from_id_val(&event_id, value.clone()).map_err(|e| {
debug_error!("Invalid PDU in fetching remote room state PDUs response: {value:#?}"); warn!("Invalid PDU in fetching remote room state PDUs response: {} {:?}", e, value);
err!(BadServerResponse(debug_error!("Invalid PDU in send_join response: {e:?}"))) Error::BadServerResponse("Invalid PDU in send_join response.")
})?; })?;
self.services self.services
@@ -684,7 +691,8 @@ pub(super) async fn get_verify_keys(
let signature_ids: Vec<String> = Vec::new(); let signature_ids: Vec<String> = Vec::new();
let keys = self let keys = self
.services .services
.server_keys .rooms
.event_handler
.fetch_signing_keys_for_server(&server_name, signature_ids) .fetch_signing_keys_for_server(&server_name, signature_ids)
.await?; .await?;
@@ -713,14 +721,30 @@ pub(super) async fn resolve_true_destination(
)); ));
} }
let filter: &capture::Filter = &|data| {
data.level() <= log::Level::DEBUG
&& data.mod_name().starts_with("conduit")
&& matches!(data.span_name(), "actual" | "well-known" | "srv")
};
let state = &self.services.server.log.capture;
let logs = Arc::new(Mutex::new(String::new()));
let capture = Capture::new(state, Some(filter), capture::fmt_markdown(logs.clone()));
let capture_scope = capture.start();
let actual = self let actual = self
.services .services
.resolver .resolver
.resolve_actual_dest(&server_name, !no_cache) .resolve_actual_dest(&server_name, !no_cache)
.await?; .await?;
drop(capture_scope);
let msg = format!("Destination: {}\nHostname URI: {}", actual.dest, actual.host,); let msg = format!(
"{}\nDestination: {}\nHostname URI: {}",
logs.lock().expect("locked"),
actual.dest,
actual.host,
);
Ok(RoomMessageEventContent::text_markdown(msg)) Ok(RoomMessageEventContent::text_markdown(msg))
} }
@@ -808,24 +832,3 @@ pub(super) async fn list_dependencies(&self, names: bool) -> Result<RoomMessageE
Ok(RoomMessageEventContent::notice_markdown(out)) Ok(RoomMessageEventContent::notice_markdown(out))
} }
#[admin_command]
pub(super) async fn database_stats(
&self, property: Option<String>, map: Option<String>,
) -> Result<RoomMessageEventContent> {
let property = property.unwrap_or_else(|| "rocksdb.stats".to_owned());
let map_name = map.as_ref().map_or(utils::string::EMPTY, String::as_str);
let mut out = String::new();
for (name, map) in self.services.db.iter_maps() {
if !map_name.is_empty() && *map_name != *name {
continue;
}
let res = map.property(&property)?;
let res = res.trim();
writeln!(out, "##### {name}:\n```\n{res}\n```")?;
}
Ok(RoomMessageEventContent::notice_markdown(out))
}
-8
View File
@@ -184,14 +184,6 @@ pub(super) enum DebugCommand {
names: bool, names: bool,
}, },
/// - Get database statistics
DatabaseStats {
property: Option<String>,
#[arg(short, long, alias("column"))]
map: Option<String>,
},
/// - Developer test stubs /// - Developer test stubs
#[command(subcommand)] #[command(subcommand)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
+67 -85
View File
@@ -1,21 +1,7 @@
use std::{ use std::{panic::AssertUnwindSafe, sync::Arc, time::Instant};
panic::AssertUnwindSafe,
sync::{Arc, Mutex},
time::SystemTime,
};
use clap::{CommandFactory, Parser}; use clap::{CommandFactory, Parser};
use conduit::{ use conduit::{checked, error, trace, utils::string::common_prefix, Error, Result};
debug, error,
log::{
capture,
capture::Capture,
fmt::{markdown_table, markdown_table_head},
},
trace,
utils::string::{collect_stream, common_prefix},
Error, Result,
};
use futures_util::future::FutureExt; use futures_util::future::FutureExt;
use ruma::{ use ruma::{
events::{ events::{
@@ -25,10 +11,9 @@ use ruma::{
OwnedEventId, OwnedEventId,
}; };
use service::{ use service::{
admin::{CommandInput, CommandOutput, HandlerFuture, HandlerResult}, admin::{CommandInput, CommandOutput, CommandResult, HandlerResult},
Services, Services,
}; };
use tracing::Level;
use crate::{admin, admin::AdminCommand, Command}; use crate::{admin, admin::AdminCommand, Command};
@@ -36,12 +21,12 @@ use crate::{admin, admin::AdminCommand, Command};
pub(super) fn complete(line: &str) -> String { complete_command(AdminCommand::command(), line) } pub(super) fn complete(line: &str) -> String { complete_command(AdminCommand::command(), line) }
#[must_use] #[must_use]
pub(super) fn handle(services: Arc<Services>, command: CommandInput) -> HandlerFuture { pub(super) fn handle(services: Arc<Services>, command: CommandInput) -> HandlerResult {
Box::pin(handle_command(services, command)) Box::pin(handle_command(services, command))
} }
#[tracing::instrument(skip_all, name = "admin")] #[tracing::instrument(skip_all, name = "admin")]
async fn handle_command(services: Arc<Services>, command: CommandInput) -> HandlerResult { async fn handle_command(services: Arc<Services>, command: CommandInput) -> CommandResult {
AssertUnwindSafe(Box::pin(process_command(services, &command))) AssertUnwindSafe(Box::pin(process_command(services, &command)))
.catch_unwind() .catch_unwind()
.await .await
@@ -49,24 +34,13 @@ async fn handle_command(services: Arc<Services>, command: CommandInput) -> Handl
.or_else(|error| handle_panic(&error, command)) .or_else(|error| handle_panic(&error, command))
} }
async fn process_command(services: Arc<Services>, input: &CommandInput) -> CommandOutput { async fn process_command(services: Arc<Services>, command: &CommandInput) -> CommandOutput {
let (command, args, body) = match parse(&services, input) { process(services, &command.command)
Err(error) => return error,
Ok(parsed) => parsed,
};
let context = Command {
services: &services,
body: &body,
timer: SystemTime::now(),
};
process(&context, command, &args)
.await .await
.and_then(|content| reply(content, input.reply_id.clone())) .and_then(|content| reply(content, command.reply_id.clone()))
} }
fn handle_panic(error: &Error, command: CommandInput) -> HandlerResult { fn handle_panic(error: &Error, command: CommandInput) -> CommandResult {
let link = "Please submit a [bug report](https://github.com/girlbossceo/conduwuit/issues/new). 🥺"; let link = "Please submit a [bug report](https://github.com/girlbossceo/conduwuit/issues/new). 🥺";
let msg = format!("Panic occurred while processing command:\n```\n{error:#?}\n```\n{link}"); let msg = format!("Panic occurred while processing command:\n```\n{error:#?}\n```\n{link}");
let content = RoomMessageEventContent::notice_markdown(msg); let content = RoomMessageEventContent::notice_markdown(msg);
@@ -85,61 +59,69 @@ fn reply(mut content: RoomMessageEventContent, reply_id: Option<OwnedEventId>) -
} }
// Parse and process a message from the admin room // Parse and process a message from the admin room
async fn process(context: &Command<'_>, command: AdminCommand, args: &[String]) -> CommandOutput { async fn process(services: Arc<Services>, msg: &str) -> CommandOutput {
let filter: &capture::Filter = let lines = msg.lines().filter(|l| !l.trim().is_empty());
&|data| data.level() <= Level::DEBUG && data.our_modules() && data.scope.contains(&"admin"); let command = lines
let logs = Arc::new(Mutex::new( .clone()
collect_stream(|s| markdown_table_head(s)).expect("markdown table header"), .next()
)); .expect("each string has at least one line");
let (parsed, body) = match parse_command(command) {
let capture = Capture::new( Ok(parsed) => parsed,
&context.services.server.log.capture, Err(error) => {
Some(filter), let server_name = services.globals.server_name();
capture::fmt(markdown_table, logs.clone()), let message = error.replace("server.name", server_name.as_str());
); return Some(RoomMessageEventContent::notice_markdown(message));
},
let capture_scope = capture.start();
let result = Box::pin(admin::process(command, context)).await;
drop(capture_scope);
debug!(
ok = result.is_ok(),
elapsed = ?context.timer.elapsed(),
command = ?args,
"command processed"
);
let logs = logs.lock().expect("locked");
let output = match result {
Err(error) => format!("{logs}\nEncountered an error while handling the command:\n```\n{error:#?}\n```"),
Ok(reply) => format!("{logs}\n{}", reply.body()), //TODO: content is recreated to add logs
}; };
Some(RoomMessageEventContent::notice_markdown(output)) let body = parse_body(AdminCommand::command(), &body, lines.skip(1).collect()).expect("trailing body parsed");
} let context = Command {
services: &services,
// Parse chat messages from the admin room into an AdminCommand object body: &body,
fn parse<'a>( };
services: &Arc<Services>, input: &'a CommandInput, let timer = Instant::now();
) -> Result<(AdminCommand, Vec<String>, Vec<&'a str>), CommandOutput> { let result = Box::pin(admin::process(parsed, &context)).await;
let lines = input.command.lines().filter(|line| !line.trim().is_empty()); let elapsed = timer.elapsed();
let command_line = lines.clone().next().expect("command missing first line"); conduit::debug!(?command, ok = result.is_ok(), "command processed in {elapsed:?}");
let body = lines.skip(1).collect(); match result {
match parse_command(command_line) { Ok(reply) => Some(reply),
Ok((command, args)) => Ok((command, args, body)), Err(error) => Some(RoomMessageEventContent::notice_markdown(format!(
Err(error) => { "Encountered an error while handling the command:\n```\n{error:#?}\n```"
let message = error ))),
.to_string()
.replace("server.name", services.globals.server_name().as_str());
Err(Some(RoomMessageEventContent::notice_markdown(message)))
},
} }
} }
fn parse_command(line: &str) -> Result<(AdminCommand, Vec<String>)> { // Parse chat messages from the admin room into an AdminCommand object
let argv = parse_line(line); fn parse_command(command_line: &str) -> Result<(AdminCommand, Vec<String>), String> {
let command = AdminCommand::try_parse_from(&argv)?; let argv = parse_line(command_line);
Ok((command, argv)) let com = AdminCommand::try_parse_from(&argv).map_err(|error| error.to_string())?;
Ok((com, argv))
}
fn parse_body<'a>(mut cmd: clap::Command, body: &'a [String], lines: Vec<&'a str>) -> Result<Vec<&'a str>> {
let mut start = 1;
'token: for token in body.iter().skip(1) {
let cmd_ = cmd.clone();
for sub in cmd_.get_subcommands() {
if sub.get_name() == *token {
start = checked!(start + 1)?;
cmd = sub.clone();
continue 'token;
}
}
// positional arguments have to be skipped too
let num_posargs = cmd_.get_positionals().count();
start = checked!(start + num_posargs)?;
break;
}
Ok(body
.iter()
.skip(start)
.map(String::as_str)
.chain(lines)
.collect::<Vec<&'a str>>())
} }
fn complete_command(mut cmd: clap::Command, line: &str) -> String { fn complete_command(mut cmd: clap::Command, line: &str) -> String {
+1 -1
View File
@@ -547,7 +547,7 @@ async fn list_banned_rooms(&self) -> Result<RoomMessageEventContent> {
rooms.reverse(); rooms.reverse();
let output_plain = format!( let output_plain = format!(
"Rooms Banned ({}):\n```\n{}\n```", "Rooms Banned ({}):\n```\n{}```",
rooms.len(), rooms.len(),
rooms rooms
.iter() .iter()
+2
View File
@@ -47,6 +47,7 @@ hmac.workspace = true
http.workspace = true http.workspace = true
http-body-util.workspace = true http-body-util.workspace = true
hyper.workspace = true hyper.workspace = true
image.workspace = true
ipaddress.workspace = true ipaddress.workspace = true
jsonwebtoken.workspace = true jsonwebtoken.workspace = true
log.workspace = true log.workspace = true
@@ -59,6 +60,7 @@ serde.workspace = true
sha-1.workspace = true sha-1.workspace = true
tokio.workspace = true tokio.workspace = true
tracing.workspace = true tracing.workspace = true
webpage.workspace = true
[lints] [lints]
workspace = true workspace = true
+3 -2
View File
@@ -4,7 +4,7 @@ use std::{
}; };
use axum::extract::State; use axum::extract::State;
use conduit::{utils, utils::math::continue_exponential_backoff_secs, Err, Error, Result}; use conduit::{debug, utils, utils::math::continue_exponential_backoff_secs, Error, Result};
use futures_util::{stream::FuturesUnordered, StreamExt}; use futures_util::{stream::FuturesUnordered, StreamExt};
use ruma::{ use ruma::{
api::{ api::{
@@ -362,7 +362,8 @@ pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool + Send>(
const MIN: u64 = 5 * 60; const MIN: u64 = 5 * 60;
const MAX: u64 = 60 * 60 * 24; const MAX: u64 = 60 * 60 * 24;
if continue_exponential_backoff_secs(MIN, MAX, time.elapsed(), *tries) { if continue_exponential_backoff_secs(MIN, MAX, time.elapsed(), *tries) {
return (server, Err!(BadServerResponse("bad query from {server:?}, still backing off"))); debug!("Backing off query from {server:?}");
return (server, Err(Error::BadServerResponse("bad query, still backing off")));
} }
} }
+257 -35
View File
@@ -1,11 +1,11 @@
#![allow(deprecated)] #![allow(deprecated)]
use std::time::Duration; use std::{io::Cursor, time::Duration};
use axum::extract::State; use axum::extract::State;
use axum_client_ip::InsecureClientIp; use axum_client_ip::InsecureClientIp;
use conduit::{ use conduit::{
debug_warn, err, error, debug, debug_info, debug_warn, err, info,
utils::{ utils::{
self, self,
content_disposition::{content_disposition_type, make_content_disposition, sanitise_filename}, content_disposition::{content_disposition_type, make_content_disposition, sanitise_filename},
@@ -13,20 +13,27 @@ use conduit::{
}, },
warn, Err, Error, Result, warn, Err, Error, Result,
}; };
use image::io::Reader as ImgReader;
use ipaddress::IPAddress;
use reqwest::Url;
use ruma::api::client::{ use ruma::api::client::{
error::{ErrorKind, RetryAfter}, error::ErrorKind,
media::{ media::{
create_content, get_content, get_content_as_filename, get_content_thumbnail, get_media_config, create_content, get_content, get_content_as_filename, get_content_thumbnail, get_media_config,
get_media_preview, get_media_preview,
}, },
}; };
use service::{ use service::{
media::{FileMeta, MXC_LENGTH}, media::{FileMeta, UrlPreviewData},
Services, Services,
}; };
use webpage::HTML;
use crate::{Ruma, RumaResponse}; use crate::{Ruma, RumaResponse};
/// generated MXC ID (`media-id`) length
const MXC_LENGTH: usize = 32;
/// Cache control for immutable objects /// Cache control for immutable objects
const CACHE_CONTROL_IMMUTABLE: &str = "public,max-age=31536000,immutable"; const CACHE_CONTROL_IMMUTABLE: &str = "public,max-age=31536000,immutable";
@@ -69,36 +76,23 @@ pub(crate) async fn get_media_preview_route(
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let url = &body.url; let url = &body.url;
if !services.media.url_preview_allowed(url) { if !url_preview_allowed(&services, url) {
return Err!(Request(Forbidden( debug_info!(%sender_user, "URL is not allowed to be previewed: {url}");
warn!(%sender_user, %url, "URL is not allowed to be previewed") return Err(Error::BadRequest(ErrorKind::forbidden(), "URL is not allowed to be previewed"));
)));
} }
match services.media.get_url_preview(url).await { match get_url_preview(&services, url).await {
Ok(preview) => { Ok(preview) => {
let res = serde_json::value::to_raw_value(&preview).map_err(|e| { let res = serde_json::value::to_raw_value(&preview).map_err(|e| {
error!(%sender_user, "Failed to convert UrlPreviewData into a serde json value: {e}"); warn!(%sender_user, "Failed to convert UrlPreviewData into a serde json value: {e}");
Error::BadRequest( err!(Request(Unknown("Failed to generate a URL preview")))
ErrorKind::LimitExceeded {
retry_after: Some(RetryAfter::Delay(Duration::from_secs(5))),
},
"Failed to generate a URL preview, try again later.",
)
})?; })?;
Ok(get_media_preview::v3::Response::from_raw_value(res)) Ok(get_media_preview::v3::Response::from_raw_value(res))
}, },
Err(e) => { Err(e) => {
warn!(%sender_user, "Failed to generate a URL preview: {e}"); info!(%sender_user, "Failed to generate a URL preview: {e}");
// there doesn't seem to be an agreed-upon error code in the spec. Err!(Request(Unknown("Failed to generate a URL preview")))
// the only response codes in the preview_url spec page are 200 and 429.
Err(Error::BadRequest(
ErrorKind::LimitExceeded {
retry_after: Some(RetryAfter::Delay(Duration::from_secs(5))),
},
"Failed to generate a URL preview, try again later.",
))
}, },
} }
} }
@@ -222,7 +216,10 @@ pub(crate) async fn get_content_route(
body.timeout_ms, body.timeout_ms,
) )
.await .await
.map_err(|e| err!(Request(NotFound(debug_warn!("Fetching media `{mxc}` failed: {e:?}")))))?; .map_err(|e| {
debug_warn!("Fetching media `{}` failed: {:?}", mxc, e);
Error::BadRequest(ErrorKind::NotFound, "Remote media error.")
})?;
let content_disposition = Some(make_content_disposition( let content_disposition = Some(make_content_disposition(
&response.content_type, &response.content_type,
@@ -238,7 +235,7 @@ pub(crate) async fn get_content_route(
cache_control: Some(CACHE_CONTROL_IMMUTABLE.to_owned()), cache_control: Some(CACHE_CONTROL_IMMUTABLE.to_owned()),
}) })
} else { } else {
Err!(Request(NotFound("Media not found."))) Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
} }
} }
@@ -325,10 +322,13 @@ pub(crate) async fn get_content_as_filename_route(
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()), cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
}) })
}, },
Err(e) => Err!(Request(NotFound(debug_warn!("Fetching media `{mxc}` failed: {e:?}")))), Err(e) => {
debug_warn!("Fetching media `{}` failed: {:?}", mxc, e);
Err(Error::BadRequest(ErrorKind::NotFound, "Remote media error."))
},
} }
} else { } else {
Err!(Request(NotFound("Media not found."))) Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
} }
} }
@@ -379,10 +379,10 @@ pub(crate) async fn get_content_thumbnail_route(
&mxc, &mxc,
body.width body.width
.try_into() .try_into()
.map_err(|e| err!(Request(InvalidParam("Width is invalid: {e:?}"))))?, .map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid."))?,
body.height body.height
.try_into() .try_into()
.map_err(|e| err!(Request(InvalidParam("Height is invalid: {e:?}"))))?, .map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Height is invalid."))?,
) )
.await? .await?
{ {
@@ -405,7 +405,7 @@ pub(crate) async fn get_content_thumbnail_route(
// we'll lie to the client and say the blocked server's media was not found and // we'll lie to the client and say the blocked server's media was not found and
// log. the client has no way of telling anyways so this is a security bonus. // log. the client has no way of telling anyways so this is a security bonus.
debug_warn!("Received request for media `{}` on blocklisted server", mxc); debug_warn!("Received request for media `{}` on blocklisted server", mxc);
return Err!(Request(NotFound("Media not found."))); return Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."));
} }
match services match services
@@ -454,10 +454,13 @@ pub(crate) async fn get_content_thumbnail_route(
content_disposition, content_disposition,
}) })
}, },
Err(e) => Err!(Request(NotFound(debug_warn!("Fetching media `{mxc}` failed: {e:?}")))), Err(e) => {
debug_warn!("Fetching media `{}` failed: {:?}", mxc, e);
Err(Error::BadRequest(ErrorKind::NotFound, "Remote media error."))
},
} }
} else { } else {
Err!(Request(NotFound("Media not found."))) Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
} }
} }
@@ -495,7 +498,7 @@ async fn get_remote_content(
// we'll lie to the client and say the blocked server's media was not found and // we'll lie to the client and say the blocked server's media was not found and
// log. the client has no way of telling anyways so this is a security bonus. // log. the client has no way of telling anyways so this is a security bonus.
debug_warn!("Received request for media `{mxc}` on blocklisted server"); debug_warn!("Received request for media `{mxc}` on blocklisted server");
return Err!(Request(NotFound("Media not found."))); return Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."));
} }
let content_response = services let content_response = services
@@ -537,3 +540,222 @@ async fn get_remote_content(
cache_control: Some(CACHE_CONTROL_IMMUTABLE.to_owned()), cache_control: Some(CACHE_CONTROL_IMMUTABLE.to_owned()),
}) })
} }
async fn download_image(services: &Services, client: &reqwest::Client, url: &str) -> Result<UrlPreviewData> {
let image = client.get(url).send().await?.bytes().await?;
let mxc = format!("mxc://{}/{}", services.globals.server_name(), utils::random_string(MXC_LENGTH));
services
.media
.create(None, &mxc, None, None, &image)
.await?;
let (width, height) = match ImgReader::new(Cursor::new(&image)).with_guessed_format() {
Err(_) => (None, None),
Ok(reader) => match reader.into_dimensions() {
Err(_) => (None, None),
Ok((width, height)) => (Some(width), Some(height)),
},
};
Ok(UrlPreviewData {
image: Some(mxc),
image_size: Some(image.len()),
image_width: width,
image_height: height,
..Default::default()
})
}
async fn download_html(services: &Services, client: &reqwest::Client, url: &str) -> Result<UrlPreviewData> {
let mut response = client.get(url).send().await?;
let mut bytes: Vec<u8> = Vec::new();
while let Some(chunk) = response.chunk().await? {
bytes.extend_from_slice(&chunk);
if bytes.len() > services.globals.url_preview_max_spider_size() {
debug!(
"Response body from URL {} exceeds url_preview_max_spider_size ({}), not processing the rest of the \
response body and assuming our necessary data is in this range.",
url,
services.globals.url_preview_max_spider_size()
);
break;
}
}
let body = String::from_utf8_lossy(&bytes);
let Ok(html) = HTML::from_string(body.to_string(), Some(url.to_owned())) else {
return Err(Error::BadRequest(ErrorKind::Unknown, "Failed to parse HTML"));
};
let mut data = match html.opengraph.images.first() {
None => UrlPreviewData::default(),
Some(obj) => download_image(services, client, &obj.url).await?,
};
let props = html.opengraph.properties;
/* use OpenGraph title/description, but fall back to HTML if not available */
data.title = props.get("title").cloned().or(html.title);
data.description = props.get("description").cloned().or(html.description);
Ok(data)
}
async fn request_url_preview(services: &Services, url: &str) -> Result<UrlPreviewData> {
if let Ok(ip) = IPAddress::parse(url) {
if !services.globals.valid_cidr_range(&ip) {
return Err(Error::BadServerResponse("Requesting from this address is forbidden"));
}
}
let client = &services.client.url_preview;
let response = client.head(url).send().await?;
if let Some(remote_addr) = response.remote_addr() {
if let Ok(ip) = IPAddress::parse(remote_addr.ip().to_string()) {
if !services.globals.valid_cidr_range(&ip) {
return Err(Error::BadServerResponse("Requesting from this address is forbidden"));
}
}
}
let Some(content_type) = response
.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|x| x.to_str().ok())
else {
return Err(Error::BadRequest(ErrorKind::Unknown, "Unknown Content-Type"));
};
let data = match content_type {
html if html.starts_with("text/html") => download_html(services, client, url).await?,
img if img.starts_with("image/") => download_image(services, client, url).await?,
_ => return Err(Error::BadRequest(ErrorKind::Unknown, "Unsupported Content-Type")),
};
services.media.set_url_preview(url, &data).await?;
Ok(data)
}
async fn get_url_preview(services: &Services, url: &str) -> Result<UrlPreviewData> {
if let Some(preview) = services.media.get_url_preview(url).await {
return Ok(preview);
}
// ensure that only one request is made per URL
let _request_lock = services.media.url_preview_mutex.lock(url).await;
match services.media.get_url_preview(url).await {
Some(preview) => Ok(preview),
None => request_url_preview(services, url).await,
}
}
fn url_preview_allowed(services: &Services, url_str: &str) -> bool {
let url: Url = match Url::parse(url_str) {
Ok(u) => u,
Err(e) => {
warn!("Failed to parse URL from a str: {}", e);
return false;
},
};
if ["http", "https"]
.iter()
.all(|&scheme| scheme != url.scheme().to_lowercase())
{
debug!("Ignoring non-HTTP/HTTPS URL to preview: {}", url);
return false;
}
let host = match url.host_str() {
None => {
debug!("Ignoring URL preview for a URL that does not have a host (?): {}", url);
return false;
},
Some(h) => h.to_owned(),
};
let allowlist_domain_contains = services.globals.url_preview_domain_contains_allowlist();
let allowlist_domain_explicit = services.globals.url_preview_domain_explicit_allowlist();
let denylist_domain_explicit = services.globals.url_preview_domain_explicit_denylist();
let allowlist_url_contains = services.globals.url_preview_url_contains_allowlist();
if allowlist_domain_contains.contains(&"*".to_owned())
|| allowlist_domain_explicit.contains(&"*".to_owned())
|| allowlist_url_contains.contains(&"*".to_owned())
{
debug!("Config key contains * which is allowing all URL previews. Allowing URL {}", url);
return true;
}
if !host.is_empty() {
if denylist_domain_explicit.contains(&host) {
debug!(
"Host {} is not allowed by url_preview_domain_explicit_denylist (check 1/4)",
&host
);
return false;
}
if allowlist_domain_explicit.contains(&host) {
debug!("Host {} is allowed by url_preview_domain_explicit_allowlist (check 2/4)", &host);
return true;
}
if allowlist_domain_contains
.iter()
.any(|domain_s| domain_s.contains(&host.clone()))
{
debug!("Host {} is allowed by url_preview_domain_contains_allowlist (check 3/4)", &host);
return true;
}
if allowlist_url_contains
.iter()
.any(|url_s| url.to_string().contains(&url_s.to_string()))
{
debug!("URL {} is allowed by url_preview_url_contains_allowlist (check 4/4)", &host);
return true;
}
// check root domain if available and if user has root domain checks
if services.globals.url_preview_check_root_domain() {
debug!("Checking root domain");
match host.split_once('.') {
None => return false,
Some((_, root_domain)) => {
if denylist_domain_explicit.contains(&root_domain.to_owned()) {
debug!(
"Root domain {} is not allowed by url_preview_domain_explicit_denylist (check 1/3)",
&root_domain
);
return true;
}
if allowlist_domain_explicit.contains(&root_domain.to_owned()) {
debug!(
"Root domain {} is allowed by url_preview_domain_explicit_allowlist (check 2/3)",
&root_domain
);
return true;
}
if allowlist_domain_contains
.iter()
.any(|domain_s| domain_s.contains(&root_domain.to_owned()))
{
debug!(
"Root domain {} is allowed by url_preview_domain_contains_allowlist (check 3/3)",
&root_domain
);
return true;
}
},
}
}
}
false
}
+29 -24
View File
@@ -8,11 +8,11 @@ use std::{
use axum::extract::State; use axum::extract::State;
use axum_client_ip::InsecureClientIp; use axum_client_ip::InsecureClientIp;
use conduit::{ use conduit::{
debug, debug_error, debug_warn, err, error, info, debug, debug_warn, error, info,
pdu::{gen_event_id_canonical_json, PduBuilder}, pdu::{gen_event_id_canonical_json, PduBuilder},
trace, utils, trace, utils,
utils::math::continue_exponential_backoff_secs, utils::math::continue_exponential_backoff_secs,
warn, Err, Error, PduEvent, Result, warn, Error, PduEvent, Result,
}; };
use ruma::{ use ruma::{
api::{ api::{
@@ -705,11 +705,11 @@ async fn join_room_by_id_helper_remote(
{ {
room_version room_version
}, },
_ => return Err!(BadServerResponse("Room version is not supported")), _ => return Err(Error::BadServerResponse("Room version is not supported")),
}; };
let mut join_event_stub: CanonicalJsonObject = serde_json::from_str(make_join_response.event.get()) let mut join_event_stub: CanonicalJsonObject = serde_json::from_str(make_join_response.event.get())
.map_err(|e| err!(BadServerResponse("Invalid make_join event json received from server: {e:?}")))?; .map_err(|_| Error::BadServerResponse("Invalid make_join event json received from server."))?;
let join_authorized_via_users_server = join_event_stub let join_authorized_via_users_server = join_event_stub
.get("content") .get("content")
@@ -876,14 +876,15 @@ async fn join_room_by_id_helper_remote(
info!("Parsing join event"); info!("Parsing join event");
let parsed_join_pdu = PduEvent::from_id_val(event_id, join_event.clone()) let parsed_join_pdu = PduEvent::from_id_val(event_id, join_event.clone())
.map_err(|e| err!(BadServerResponse("Invalid join event PDU: {e:?}")))?; .map_err(|_| Error::BadServerResponse("Invalid join event PDU."))?;
let mut state = HashMap::new(); let mut state = HashMap::new();
let pub_key_map = RwLock::new(BTreeMap::new()); let pub_key_map = RwLock::new(BTreeMap::new());
info!("Fetching join signing keys"); info!("Fetching join signing keys");
services services
.server_keys .rooms
.event_handler
.fetch_join_signing_keys(&send_join_response, &room_version_id, &pub_key_map) .fetch_join_signing_keys(&send_join_response, &room_version_id, &pub_key_map)
.await?; .await?;
@@ -899,8 +900,8 @@ async fn join_room_by_id_helper_remote(
}; };
let pdu = PduEvent::from_id_val(&event_id, value.clone()).map_err(|e| { let pdu = PduEvent::from_id_val(&event_id, value.clone()).map_err(|e| {
debug_warn!("Invalid PDU in send_join response: {value:#?}"); warn!("Invalid PDU in send_join response: {} {:?}", e, value);
err!(BadServerResponse("Invalid PDU in send_join response: {e:?}")) Error::BadServerResponse("Invalid PDU in send_join response.")
})?; })?;
services.rooms.outlier.add_pdu_outlier(&event_id, &value)?; services.rooms.outlier.add_pdu_outlier(&event_id, &value)?;
@@ -1122,10 +1123,10 @@ async fn join_room_by_id_helper_local(
{ {
room_version_id room_version_id
}, },
_ => return Err!(BadServerResponse("Room version is not supported")), _ => return Err(Error::BadServerResponse("Room version is not supported")),
}; };
let mut join_event_stub: CanonicalJsonObject = serde_json::from_str(make_join_response.event.get()) let mut join_event_stub: CanonicalJsonObject = serde_json::from_str(make_join_response.event.get())
.map_err(|e| err!(BadServerResponse("Invalid make_join event json received from server: {e:?}")))?; .map_err(|_| Error::BadServerResponse("Invalid make_join event json received from server."))?;
let join_authorized_via_users_server = join_event_stub let join_authorized_via_users_server = join_event_stub
.get("content") .get("content")
.map(|s| { .map(|s| {
@@ -1229,7 +1230,8 @@ async fn join_room_by_id_helper_local(
drop(state_lock); drop(state_lock);
let pub_key_map = RwLock::new(BTreeMap::new()); let pub_key_map = RwLock::new(BTreeMap::new());
services services
.server_keys .rooms
.event_handler
.fetch_required_signing_keys([&signed_value], &pub_key_map) .fetch_required_signing_keys([&signed_value], &pub_key_map)
.await?; .await?;
services services
@@ -1250,7 +1252,7 @@ async fn join_room_by_id_helper_local(
async fn make_join_request( async fn make_join_request(
services: &Services, sender_user: &UserId, room_id: &RoomId, servers: &[OwnedServerName], services: &Services, sender_user: &UserId, room_id: &RoomId, servers: &[OwnedServerName],
) -> Result<(federation::membership::prepare_join_event::v1::Response, OwnedServerName)> { ) -> Result<(federation::membership::prepare_join_event::v1::Response, OwnedServerName)> {
let mut make_join_response_and_server = Err!(BadServerResponse("No server available to assist in joining.")); let mut make_join_response_and_server = Err(Error::BadServerResponse("No server available to assist in joining."));
let mut make_join_counter: u16 = 0; let mut make_join_counter: u16 = 0;
let mut incompatible_room_version_count: u8 = 0; let mut incompatible_room_version_count: u8 = 0;
@@ -1291,7 +1293,8 @@ async fn make_join_request(
"15 servers have responded with M_INCOMPATIBLE_ROOM_VERSION or M_UNSUPPORTED_ROOM_VERSION, \ "15 servers have responded with M_INCOMPATIBLE_ROOM_VERSION or M_UNSUPPORTED_ROOM_VERSION, \
assuming that Conduwuit does not support the room {room_id}: {e}" assuming that Conduwuit does not support the room {room_id}: {e}"
); );
make_join_response_and_server = Err!(BadServerResponse("Room version is not supported by Conduwuit")); make_join_response_and_server =
Err(Error::BadServerResponse("Room version is not supported by Conduwuit"));
return make_join_response_and_server; return make_join_response_and_server;
} }
@@ -1299,7 +1302,8 @@ async fn make_join_request(
warn!( warn!(
"50 servers failed to provide valid make_join response, assuming no server can assist in joining." "50 servers failed to provide valid make_join response, assuming no server can assist in joining."
); );
make_join_response_and_server = Err!(BadServerResponse("No server available to assist in joining.")); make_join_response_and_server =
Err(Error::BadServerResponse("No server available to assist in joining."));
return make_join_response_and_server; return make_join_response_and_server;
} }
} }
@@ -1319,8 +1323,8 @@ pub async fn validate_and_add_event_id(
pub_key_map: &RwLock<BTreeMap<String, BTreeMap<String, Base64>>>, pub_key_map: &RwLock<BTreeMap<String, BTreeMap<String, Base64>>>,
) -> Result<(OwnedEventId, CanonicalJsonObject)> { ) -> Result<(OwnedEventId, CanonicalJsonObject)> {
let mut value: CanonicalJsonObject = serde_json::from_str(pdu.get()).map_err(|e| { let mut value: CanonicalJsonObject = serde_json::from_str(pdu.get()).map_err(|e| {
debug_error!("Invalid PDU in server response: {pdu:#?}"); error!("Invalid PDU in server response: {:?}: {:?}", pdu, e);
err!(BadServerResponse("Invalid PDU in server response: {e:?}")) Error::BadServerResponse("Invalid PDU in server response")
})?; })?;
let event_id = EventId::parse(format!( let event_id = EventId::parse(format!(
"${}", "${}",
@@ -1356,15 +1360,15 @@ pub async fn validate_and_add_event_id(
const MIN: u64 = 60 * 5; const MIN: u64 = 60 * 5;
const MAX: u64 = 60 * 60 * 24; const MAX: u64 = 60 * 60 * 24;
if continue_exponential_backoff_secs(MIN, MAX, time.elapsed(), *tries) { if continue_exponential_backoff_secs(MIN, MAX, time.elapsed(), *tries) {
return Err!(BadServerResponse("bad event {event_id:?}, still backing off")); debug!("Backing off from {event_id}");
return Err(Error::BadServerResponse("bad event, still backing off"));
} }
} }
if let Err(e) = ruma::signatures::verify_event(&*pub_key_map.read().await, &value, room_version) { if let Err(e) = ruma::signatures::verify_event(&*pub_key_map.read().await, &value, room_version) {
debug_error!("Event {event_id} failed verification {pdu:#?}"); warn!("Event {} failed verification {:?} {}", event_id, pdu, e);
let e = Err!(BadServerResponse(debug_error!("Event {event_id} failed verification: {e:?}")));
back_off(event_id).await; back_off(event_id).await;
return e; return Err(Error::BadServerResponse("Event failed verification."));
} }
value.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.as_str().to_owned())); value.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.as_str().to_owned()));
@@ -1470,7 +1474,8 @@ pub(crate) async fn invite_helper(
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Origin field is invalid."))?; .map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Origin field is invalid."))?;
services services
.server_keys .rooms
.event_handler
.fetch_required_signing_keys([&value], &pub_key_map) .fetch_required_signing_keys([&value], &pub_key_map)
.await?; .await?;
@@ -1646,7 +1651,7 @@ pub async fn leave_room(services: &Services, user_id: &UserId, room_id: &RoomId,
} }
async fn remote_leave_room(services: &Services, user_id: &UserId, room_id: &RoomId) -> Result<()> { async fn remote_leave_room(services: &Services, user_id: &UserId, room_id: &RoomId) -> Result<()> {
let mut make_leave_response_and_server = Err!(BadServerResponse("No server available to assist in leaving.")); let mut make_leave_response_and_server = Err(Error::BadServerResponse("No server available to assist in leaving."));
let invite_state = services let invite_state = services
.rooms .rooms
@@ -1703,11 +1708,11 @@ async fn remote_leave_room(services: &Services, user_id: &UserId, room_id: &Room
{ {
version version
}, },
_ => return Err!(BadServerResponse("Room version is not supported")), _ => return Err(Error::BadServerResponse("Room version is not supported")),
}; };
let mut leave_event_stub = serde_json::from_str::<CanonicalJsonObject>(make_leave_response.event.get()) let mut leave_event_stub = serde_json::from_str::<CanonicalJsonObject>(make_leave_response.event.get())
.map_err(|e| err!(BadServerResponse("Invalid make_leave event json received from server: {e:?}")))?; .map_err(|_| Error::BadServerResponse("Invalid make_leave event json received from server."))?;
// TODO: Is origin needed? // TODO: Is origin needed?
leave_event_stub.insert( leave_event_stub.insert(
+96 -34
View File
@@ -1,7 +1,7 @@
use std::{cmp::max, collections::BTreeMap}; use std::{cmp::max, collections::BTreeMap};
use axum::extract::State; use axum::extract::State;
use conduit::{debug_info, debug_warn}; use conduit::{debug_info, debug_warn, err};
use ruma::{ use ruma::{
api::client::{ api::client::{
error::ErrorKind, error::ErrorKind,
@@ -20,6 +20,7 @@ use ruma::{
tombstone::RoomTombstoneEventContent, tombstone::RoomTombstoneEventContent,
topic::RoomTopicEventContent, topic::RoomTopicEventContent,
}, },
space::child::SpaceChildEventContent,
StateEventType, TimelineEventType, StateEventType, TimelineEventType,
}, },
int, int,
@@ -475,10 +476,7 @@ pub(crate) async fn get_room_event_route(
.rooms .rooms
.timeline .timeline
.get_pdu(&body.event_id)? .get_pdu(&body.event_id)?
.ok_or_else(|| { .ok_or_else(|| err!(Request(NotFound("Event {} not found.", &body.event_id))))?;
warn!("Event not found, event ID: {:?}", &body.event_id);
Error::BadRequest(ErrorKind::NotFound, "Event not found.")
})?;
if !services if !services
.rooms .rooms
@@ -621,22 +619,14 @@ pub(crate) async fn upgrade_room_route(
V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => { V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => {
create_event_content.insert( create_event_content.insert(
"creator".into(), "creator".into(),
json!(&sender_user).try_into().map_err(|e| { json!(&sender_user)
info!("Error forming creation event: {e}"); .try_into()
Error::BadRequest(ErrorKind::BadJson, "Error forming creation event") .map_err(|_| err!(Request(BadJson("Error forming creation event"))))?,
})?,
); );
}, },
V11 => {
// "creator" key no longer exists in V11 rooms
create_event_content.remove("creator");
},
_ => { _ => {
warn!("Unexpected or unsupported room version {}", body.new_version); // "creator" key no longer exists in V11+ rooms
return Err(Error::BadRequest( create_event_content.remove("creator");
ErrorKind::BadJson,
"Unexpected or unsupported room version found",
));
}, },
} }
} }
@@ -645,26 +635,100 @@ pub(crate) async fn upgrade_room_route(
"room_version".into(), "room_version".into(),
json!(&body.new_version) json!(&body.new_version)
.try_into() .try_into()
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"))?, .map_err(|_| err!(Request(BadJson("Error forming creation event"))))?,
); );
create_event_content.insert( create_event_content.insert(
"predecessor".into(), "predecessor".into(),
json!(predecessor) json!(predecessor)
.try_into() .try_into()
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"))?, .map_err(|_| err!(Request(BadJson("Error forming creation event"))))?,
); );
// Validate creation event content // if the room was a space:
if serde_json::from_str::<CanonicalJsonObject>( // - migrate m.space.child and/or m.space.parent
to_raw_value(&create_event_content) // - add space type to replacement room m.room.create
.expect("Error forming creation event") //
.get(), // as apart of MSC4168
) if services
.is_err() .rooms
.state_accessor
.get_room_type(&body.room_id)
.unwrap_or(None)
.is_some_and(|room_type| room_type == ruma::room::RoomType::Space)
{ {
return Err(Error::BadRequest(ErrorKind::BadJson, "Error forming creation event")); create_event_content.insert(
"type".into(),
json!(ruma::room::RoomType::Space)
.try_into()
.map_err(|_| err!(Request(BadJson("Error forming creation event"))))?,
);
if let Some(event_content) = services
.rooms
.state_accessor
.room_state_get(&body.room_id, &StateEventType::SpaceChild, body.room_id.as_str())
.unwrap_or(None)
{
// space contents that are empty are not apart of the space
if !event_content.content.get().is_empty() {
// remove all but us from via
let mut new_content: SpaceChildEventContent = serde_json::from_str(event_content.content.get())
.map_err(|_| err!(Database(error!("Invalid m.space.child content in database"))))?;
new_content.via = vec![services.globals.config.server_name.clone()];
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: StateEventType::SpaceChild.to_string().into(),
content: to_raw_value(&new_content).expect("we just created it"),
unsigned: None,
state_key: Some(replacement_room.to_string()),
redacts: None,
},
sender_user,
&replacement_room,
&state_lock,
)
.await?;
}
}
if let Some(event_content) = services
.rooms
.state_accessor
.room_state_get(&body.room_id, &StateEventType::SpaceParent, body.room_id.as_str())
.unwrap_or(None)
{
// space contents that are empty are not apart of the space
if !event_content.content.get().is_empty() {
// remove all but us from via
let mut new_content: SpaceChildEventContent = serde_json::from_str(event_content.content.get())
.map_err(|_| err!(Database(error!("Invalid m.space.child content in database"))))?;
new_content.via = vec![services.globals.config.server_name.clone()];
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: StateEventType::SpaceParent.to_string().into(),
content: to_raw_value(&new_content).expect("we just created it"),
unsigned: None,
state_key: Some(replacement_room.to_string()),
redacts: None,
},
sender_user,
&replacement_room,
&state_lock,
)
.await?;
}
}
} }
// m.room.create
services services
.rooms .rooms
.timeline .timeline
@@ -758,11 +822,11 @@ pub(crate) async fn upgrade_room_route(
.rooms .rooms
.state_accessor .state_accessor
.room_state_get(&body.room_id, &StateEventType::RoomPowerLevels, "")? .room_state_get(&body.room_id, &StateEventType::RoomPowerLevels, "")?
.ok_or_else(|| Error::bad_database("Found room without m.room.create event."))? .ok_or_else(|| err!(Database(error!("Found room without m.room.create event."))))?
.content .content
.get(), .get(),
) )
.map_err(|_| Error::bad_database("Invalid room event in database."))?; .map_err(|_| err!(Database(error!("Invalid room event in database."))))?;
// Setting events_default and invite to the greater of 50 and users_default + 1 // Setting events_default and invite to the greater of 50 and users_default + 1
let new_level = max( let new_level = max(
@@ -770,9 +834,7 @@ pub(crate) async fn upgrade_room_route(
power_levels_event_content power_levels_event_content
.users_default .users_default
.checked_add(int!(1)) .checked_add(int!(1))
.ok_or_else(|| { .ok_or_else(|| err!(Request(BadJson("users_default power levels event content is not valid"))))?,
Error::BadRequest(ErrorKind::BadJson, "users_default power levels event content is not valid")
})?,
); );
power_levels_event_content.events_default = new_level; power_levels_event_content.events_default = new_level;
power_levels_event_content.invite = new_level; power_levels_event_content.invite = new_level;
@@ -836,7 +898,7 @@ fn default_power_levels_content(
if let Some(power_level_content_override) = power_level_content_override { if let Some(power_level_content_override) = power_level_content_override {
let json: JsonObject = serde_json::from_str(power_level_content_override.json().get()) let json: JsonObject = serde_json::from_str(power_level_content_override.json().get())
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Invalid power_level_content_override."))?; .map_err(|_| err!(Request(BadJson("Invalid power_level_content_override"))))?;
for (key, value) in json { for (key, value) in json {
power_levels_content[key] = value; power_levels_content[key] = value;
+4 -5
View File
@@ -34,9 +34,8 @@ struct Claims {
/// ///
/// Get the supported login types of this server. One of these should be used as /// Get the supported login types of this server. One of these should be used as
/// the `type` field when logging in. /// the `type` field when logging in.
#[tracing::instrument(skip_all, fields(%client), name = "register")]
pub(crate) async fn get_login_types_route( pub(crate) async fn get_login_types_route(
InsecureClientIp(client): InsecureClientIp, _body: Ruma<get_login_types::v3::Request>, _body: Ruma<get_login_types::v3::Request>,
) -> Result<get_login_types::v3::Response> { ) -> Result<get_login_types::v3::Response> {
Ok(get_login_types::v3::Response::new(vec![ Ok(get_login_types::v3::Response::new(vec![
get_login_types::v3::LoginType::Password(PasswordLoginType::default()), get_login_types::v3::LoginType::Password(PasswordLoginType::default()),
@@ -58,7 +57,7 @@ pub(crate) async fn get_login_types_route(
/// Note: You can use [`GET /// Note: You can use [`GET
/// /_matrix/client/r0/login`](fn.get_supported_versions_route.html) to see /// /_matrix/client/r0/login`](fn.get_supported_versions_route.html) to see
/// supported login types. /// supported login types.
#[tracing::instrument(skip_all, fields(%client), name = "register")] #[tracing::instrument(skip_all, fields(%client), name = "login")]
pub(crate) async fn login_route( pub(crate) async fn login_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp, body: Ruma<login::v3::Request>, State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp, body: Ruma<login::v3::Request>,
) -> Result<login::v3::Response> { ) -> Result<login::v3::Response> {
@@ -221,7 +220,7 @@ pub(crate) async fn login_route(
/// last seen ts) /// last seen ts)
/// - Forgets to-device events /// - Forgets to-device events
/// - Triggers device list updates /// - Triggers device list updates
#[tracing::instrument(skip_all, fields(%client), name = "register")] #[tracing::instrument(skip_all, fields(%client), name = "logout")]
pub(crate) async fn logout_route( pub(crate) async fn logout_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp, body: Ruma<logout::v3::Request>, State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp, body: Ruma<logout::v3::Request>,
) -> Result<logout::v3::Response> { ) -> Result<logout::v3::Response> {
@@ -249,7 +248,7 @@ pub(crate) async fn logout_route(
/// Note: This is equivalent to calling [`GET /// Note: This is equivalent to calling [`GET
/// /_matrix/client/r0/logout`](fn.logout_route.html) from each device of this /// /_matrix/client/r0/logout`](fn.logout_route.html) from each device of this
/// user. /// user.
#[tracing::instrument(skip_all, fields(%client), name = "register")] #[tracing::instrument(skip_all, fields(%client), name = "logout")]
pub(crate) async fn logout_all_route( pub(crate) async fn logout_all_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp, State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<logout_all::v3::Request>, body: Ruma<logout_all::v3::Request>,
+6 -1
View File
@@ -2,7 +2,7 @@ use std::time::{Duration, SystemTime};
use axum::extract::State; use axum::extract::State;
use base64::{engine::general_purpose, Engine as _}; use base64::{engine::general_purpose, Engine as _};
use conduit::utils; use conduit::{utils, Err};
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};
use ruma::{api::client::voip::get_turn_server_info, SecondsSinceUnixEpoch, UserId}; use ruma::{api::client::voip::get_turn_server_info, SecondsSinceUnixEpoch, UserId};
use sha1::Sha1; use sha1::Sha1;
@@ -19,6 +19,11 @@ type HmacSha1 = Hmac<Sha1>;
pub(crate) async fn turn_server_route( pub(crate) async fn turn_server_route(
State(services): State<crate::State>, body: Ruma<get_turn_server_info::v3::Request>, State(services): State<crate::State>, body: Ruma<get_turn_server_info::v3::Request>,
) -> Result<get_turn_server_info::v3::Response> { ) -> Result<get_turn_server_info::v3::Response> {
// MSC4166: return M_NOT_FOUND 404 if no TURN URIs are specified in any way
if services.server.config.turn_uris.is_empty() {
return Err!(Request(NotFound("Not Found")));
}
let turn_secret = services.globals.turn_secret().clone(); let turn_secret = services.globals.turn_secret().clone();
let (username, password) = if !turn_secret.is_empty() { let (username, password) = if !turn_secret.is_empty() {
+2 -1
View File
@@ -10,7 +10,8 @@ extern crate conduit_service as service;
pub(crate) use conduit::{debug_info, pdu::PduEvent, utils, Error, Result}; pub(crate) use conduit::{debug_info, pdu::PduEvent, utils, Error, Result};
pub(crate) use service::services; pub(crate) use service::services;
pub(crate) use self::router::{Ruma, RumaResponse, State}; pub use crate::router::State;
pub(crate) use crate::router::{Ruma, RumaResponse};
conduit::mod_ctor! {} conduit::mod_ctor! {}
conduit::mod_dtor! {} conduit::mod_dtor! {}
+5 -2
View File
@@ -3,7 +3,8 @@ mod auth;
mod handler; mod handler;
mod request; mod request;
mod response; mod response;
pub mod state;
use std::sync::Arc;
use axum::{ use axum::{
response::IntoResponse, response::IntoResponse,
@@ -14,9 +15,11 @@ use conduit::{err, Server};
use http::Uri; use http::Uri;
use self::handler::RouterExt; use self::handler::RouterExt;
pub(super) use self::{args::Args as Ruma, response::RumaResponse, state::State}; pub(super) use self::{args::Args as Ruma, response::RumaResponse};
use crate::{client, server}; use crate::{client, server};
pub type State = Arc<service::Services>;
pub fn build(router: Router<State>, server: &Server) -> Router<State> { pub fn build(router: Router<State>, server: &Server) -> Router<State> {
let config = &server.config; let config = &server.config;
let router = router let router = router
+15 -3
View File
@@ -6,7 +6,7 @@ use axum_extra::{
typed_header::TypedHeaderRejectionReason, typed_header::TypedHeaderRejectionReason,
TypedHeader, TypedHeader,
}; };
use conduit::{warn, Err, Error, Result}; use conduit::{debug_info, warn, Err, Error, Result};
use http::uri::PathAndQuery; use http::uri::PathAndQuery;
use ruma::{ use ruma::{
api::{client::error::ErrorKind, AuthScheme, Metadata}, api::{client::error::ErrorKind, AuthScheme, Metadata},
@@ -185,7 +185,7 @@ fn auth_appservice(services: &Services, request: &Request, info: Box<Registratio
async fn auth_server( async fn auth_server(
services: &Services, request: &mut Request, json_body: &Option<CanonicalJsonValue>, services: &Services, request: &mut Request, json_body: &Option<CanonicalJsonValue>,
) -> Result<Auth> { ) -> Result<Auth> {
if !services.globals.allow_federation() { if !services.server.config.allow_federation {
return Err!(Config("allow_federation", "Federation is disabled.")); return Err!(Config("allow_federation", "Federation is disabled."));
} }
@@ -206,6 +206,17 @@ async fn auth_server(
})?; })?;
let origin = &x_matrix.origin; let origin = &x_matrix.origin;
if services
.server
.config
.forbidden_remote_server_names
.contains(origin)
{
debug_info!("Refusing to accept inbound federation request to {origin}");
return Err!(Request(Forbidden("Federation with this homeserver is not allowed.")));
}
let signatures = let signatures =
BTreeMap::from_iter([(x_matrix.key.clone(), CanonicalJsonValue::String(x_matrix.sig.to_string()))]); BTreeMap::from_iter([(x_matrix.key.clone(), CanonicalJsonValue::String(x_matrix.sig.to_string()))]);
let signatures = BTreeMap::from_iter([( let signatures = BTreeMap::from_iter([(
@@ -250,7 +261,8 @@ async fn auth_server(
}; };
let keys_result = services let keys_result = services
.server_keys .rooms
.event_handler
.fetch_signing_keys_for_server(origin, vec![x_matrix.key.to_string()]) .fetch_signing_keys_for_server(origin, vec![x_matrix.key.to_string()])
.await; .await;
-79
View File
@@ -1,79 +0,0 @@
use std::{ops::Deref, sync::Arc};
use conduit_service::Services;
#[derive(Clone, Copy)]
pub struct State {
services: *const Services,
}
pub struct Guard {
services: Arc<Services>,
}
pub fn create(services: Arc<Services>) -> (State, Guard) {
let state = State {
services: Arc::into_raw(services.clone()),
};
let guard = Guard {
services,
};
(state, guard)
}
impl Drop for Guard {
fn drop(&mut self) {
let ptr = Arc::as_ptr(&self.services);
// SAFETY: Parity with Arc::into_raw() called in create(). This revivifies the
// Arc lost to State so it can be dropped, otherwise Services will leak.
let arc = unsafe { Arc::from_raw(ptr) };
debug_assert!(
Arc::strong_count(&arc) > 1,
"Services usually has more than one reference and is not dropped here"
);
}
}
impl Deref for State {
type Target = Services;
fn deref(&self) -> &Self::Target {
deref(&self.services).expect("dereferenced Services pointer in State must not be null")
}
}
/// SAFETY: State is a thin wrapper containing a raw const pointer to Services
/// in lieu of an Arc. Services is internally threadsafe. If State contains
/// additional fields this notice should be reevaluated.
unsafe impl Send for State {}
/// SAFETY: State is a thin wrapper containing a raw const pointer to Services
/// in lieu of an Arc. Services is internally threadsafe. If State contains
/// additional fields this notice should be reevaluated.
unsafe impl Sync for State {}
fn deref(services: &*const Services) -> Option<&Services> {
// SAFETY: We replaced Arc<Services> with *const Services in State. This is
// worth about 10 clones (20 reference count updates) for each request handled.
// Though this is not an incredibly large quantity, it's woefully unnecessary
// given the context as explained below; though it is not currently known to be
// a performance bottleneck, the front-line position justifies preempting it.
//
// Services is created prior to the axum/tower stack and Router, and prior
// to serving any requests through the handlers in this crate. It is then
// dropped only after all requests have completed, the listening sockets
// have been closed, axum/tower has been dropped. Thus Services is
// expected to live at least as long as any instance of State, making the
// constant updates to the prior Arc unnecessary to keep Services alive.
//
// Nevertheless if it is possible to accomplish this by annotating State
// with a lifetime to hold a reference (and be aware I have made a
// significant effort trying to make this work) this unsafety may not be
// necessary. It is either very difficult or impossible to get a
// lifetime'ed reference through Router / RumaHandler; though it is
// possible to pass a reference through axum's `with_state()` in trivial
// configurations as the only requirement of a State is Clone.
unsafe { services.as_ref() }
}
+2 -1
View File
@@ -105,7 +105,8 @@ async fn handle_pdus(
let pub_key_map = RwLock::new(BTreeMap::new()); let pub_key_map = RwLock::new(BTreeMap::new());
if !parsed_pdus.is_empty() { if !parsed_pdus.is_empty() {
services services
.server_keys .rooms
.event_handler
.fetch_required_signing_keys(parsed_pdus.iter().map(|(_event_id, event, _room_id)| event), &pub_key_map) .fetch_required_signing_keys(parsed_pdus.iter().map(|(_event_id, event, _room_id)| event), &pub_key_map)
.await .await
.unwrap_or_else(|e| warn!("Could not fetch all signatures for PDUs from {origin}: {e:?}")); .unwrap_or_else(|e| warn!("Could not fetch all signatures for PDUs from {origin}: {e:?}"));
+2 -1
View File
@@ -138,7 +138,8 @@ async fn create_join_event(
} }
services services
.server_keys .rooms
.event_handler
.fetch_required_signing_keys([&value], &pub_key_map) .fetch_required_signing_keys([&value], &pub_key_map)
.await?; .await?;
+2 -1
View File
@@ -150,7 +150,8 @@ async fn create_leave_event(
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "origin is not a server name."))?; .map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "origin is not a server name."))?;
services services
.server_keys .rooms
.event_handler
.fetch_required_signing_keys([&value], &pub_key_map) .fetch_required_signing_keys([&value], &pub_key_map)
.await?; .await?;
-2
View File
@@ -51,13 +51,11 @@ sha256_media = []
[dependencies] [dependencies]
argon2.workspace = true argon2.workspace = true
arrayvec.workspace = true
axum.workspace = true axum.workspace = true
bytes.workspace = true bytes.workspace = true
cargo_toml.workspace = true cargo_toml.workspace = true
checked_ops.workspace = true checked_ops.workspace = true
chrono.workspace = true chrono.workspace = true
clap.workspace = true
conduit-macros.workspace = true conduit-macros.workspace = true
const-str.workspace = true const-str.workspace = true
ctor.workspace = true ctor.workspace = true
+1 -6
View File
@@ -236,8 +236,6 @@ pub struct Config {
pub rocksdb_compaction_ioprio_idle: bool, pub rocksdb_compaction_ioprio_idle: bool,
#[serde(default = "true_fn")] #[serde(default = "true_fn")]
pub rocksdb_compaction: bool, pub rocksdb_compaction: bool,
#[serde(default = "default_rocksdb_stats_level")]
pub rocksdb_stats_level: u8,
pub emergency_password: Option<String>, pub emergency_password: Option<String>,
@@ -289,7 +287,7 @@ pub struct Config {
#[serde(default = "true_fn")] #[serde(default = "true_fn")]
pub media_startup_check: bool, pub media_startup_check: bool,
#[serde(default)] #[serde(default = "true_fn")]
pub media_compat_file_link: bool, pub media_compat_file_link: bool,
#[serde(default = "Vec::new")] #[serde(default = "Vec::new")]
pub prevent_media_downloads_from: Vec<OwnedServerName>, pub prevent_media_downloads_from: Vec<OwnedServerName>,
@@ -720,7 +718,6 @@ impl fmt::Display for Config {
&self.rocksdb_compaction_ioprio_idle.to_string(), &self.rocksdb_compaction_ioprio_idle.to_string(),
); );
line("RocksDB Compaction enabled", &self.rocksdb_compaction.to_string()); line("RocksDB Compaction enabled", &self.rocksdb_compaction.to_string());
line("RocksDB Statistics level", &self.rocksdb_stats_level.to_string());
line("Media integrity checks on startup", &self.media_startup_check.to_string()); line("Media integrity checks on startup", &self.media_startup_check.to_string());
line("Media compatibility filesystem links", &self.media_compat_file_link.to_string()); line("Media compatibility filesystem links", &self.media_compat_file_link.to_string());
line("Prevent Media Downloads From", { line("Prevent Media Downloads From", {
@@ -1005,8 +1002,6 @@ fn default_rocksdb_compression_level() -> i32 { 32767 }
#[allow(clippy::doc_markdown)] #[allow(clippy::doc_markdown)]
fn default_rocksdb_bottommost_compression_level() -> i32 { 32767 } fn default_rocksdb_bottommost_compression_level() -> i32 { 32767 }
fn default_rocksdb_stats_level() -> u8 { 1 }
// I know, it's a great name // I know, it's a great name
#[must_use] #[must_use]
pub fn default_default_room_version() -> RoomVersionId { RoomVersionId::V10 } pub fn default_default_room_version() -> RoomVersionId { RoomVersionId::V10 }
+1 -5
View File
@@ -14,7 +14,7 @@ pub use crate::utils::debug::*;
#[macro_export] #[macro_export]
macro_rules! debug_event { macro_rules! debug_event {
( $level:expr, $($x:tt)+ ) => { ( $level:expr, $($x:tt)+ ) => {
if $crate::debug::logging() { if cfg!(debug_assertions) && cfg!(not(feature = "dev_release_log_level")) {
::tracing::event!( $level, $($x)+ ) ::tracing::event!( $level, $($x)+ )
} else { } else {
::tracing::debug!( $($x)+ ) ::tracing::debug!( $($x)+ )
@@ -88,7 +88,3 @@ pub fn panic_str(p: &Box<dyn Any + Send>) -> &'static str { p.downcast_ref::<&st
#[inline(always)] #[inline(always)]
#[must_use] #[must_use]
pub fn type_name<T>(_: &T) -> &'static str { std::any::type_name::<T>() } pub fn type_name<T>(_: &T) -> &'static str { std::any::type_name::<T>() }
#[must_use]
#[inline]
pub const fn logging() -> bool { cfg!(debug_assertions) && cfg!(not(feature = "dev_release_log_level")) }
+27 -116
View File
@@ -41,149 +41,60 @@ macro_rules! Err {
#[macro_export] #[macro_export]
macro_rules! err { macro_rules! err {
(Request(Forbidden($level:ident!($($args:tt)+)))) => {{ (Config($item:literal, $($args:expr),*)) => {{
let mut buf = String::new(); $crate::error!(config = %$item, $($args),*);
$crate::error::Error::Config($item, $crate::format_maybe!($($args),*))
}};
(Request(Forbidden($level:ident!($($args:expr),*)))) => {{
$crate::$level!($($args),*);
$crate::error::Error::Request( $crate::error::Error::Request(
::ruma::api::client::error::ErrorKind::forbidden(), ::ruma::api::client::error::ErrorKind::forbidden(),
$crate::err_log!(buf, $level, $($args)+), $crate::format_maybe!($($args),*),
::http::StatusCode::BAD_REQUEST ::http::StatusCode::BAD_REQUEST
) )
}}; }};
(Request(Forbidden($($args:tt)+))) => { (Request(Forbidden($($args:expr),*))) => {
$crate::error::Error::Request( $crate::error::Error::Request(
::ruma::api::client::error::ErrorKind::forbidden(), ::ruma::api::client::error::ErrorKind::forbidden(),
$crate::format_maybe!($($args)+), $crate::format_maybe!($($args),*),
::http::StatusCode::BAD_REQUEST ::http::StatusCode::BAD_REQUEST
) )
}; };
(Request($variant:ident($level:ident!($($args:tt)+)))) => {{ (Request($variant:ident($level:ident!($($args:expr),*)))) => {{
let mut buf = String::new(); $crate::$level!($($args),*);
$crate::error::Error::Request( $crate::error::Error::Request(
::ruma::api::client::error::ErrorKind::$variant, ::ruma::api::client::error::ErrorKind::$variant,
$crate::err_log!(buf, $level, $($args)+), $crate::format_maybe!($($args),*),
::http::StatusCode::BAD_REQUEST ::http::StatusCode::BAD_REQUEST
) )
}}; }};
(Request($variant:ident($($args:tt)+))) => { (Request($variant:ident($($args:expr),*))) => {
$crate::error::Error::Request( $crate::error::Error::Request(
::ruma::api::client::error::ErrorKind::$variant, ::ruma::api::client::error::ErrorKind::$variant,
$crate::format_maybe!($($args)+), $crate::format_maybe!($($args),*),
::http::StatusCode::BAD_REQUEST ::http::StatusCode::BAD_REQUEST
) )
}; };
(Config($item:literal, $($args:tt)+)) => {{ ($variant:ident($level:ident!($($args:expr),*))) => {{
let mut buf = String::new(); $crate::$level!($($args),*);
$crate::error::Error::Config($item, $crate::err_log!(buf, error, config = %$item, $($args)+)) $crate::error::Error::$variant($crate::format_maybe!($($args),*))
}}; }};
($variant:ident($level:ident!($($args:tt)+))) => {{ ($variant:ident($($args:expr),*)) => {
let mut buf = String::new(); $crate::error::Error::$variant($crate::format_maybe!($($args),*))
$crate::error::Error::$variant($crate::err_log!(buf, $level, $($args)+)) };
($level:ident!($($args:expr),*)) => {{
$crate::$level!($($args),*);
$crate::error::Error::Err($crate::format_maybe!($($args),*))
}}; }};
($variant:ident($($args:tt)+)) => { ($($args:expr),*) => {
$crate::error::Error::$variant($crate::format_maybe!($($args)+)) $crate::error::Error::Err($crate::format_maybe!($($args),*))
};
($level:ident!($($args:tt)+)) => {{
let mut buf = String::new();
$crate::error::Error::Err($crate::err_log!(buf, $level, $($args)+))
}};
($($args:tt)+) => {
$crate::error::Error::Err($crate::format_maybe!($($args)+))
};
}
/// A trinity of integration between tracing, logging, and Error. This is a
/// customization of tracing::event! with the primary purpose of sharing the
/// error string, fieldset parsing and formatting. An added benefit is that we
/// can share the same callsite metadata for the source of our Error and the
/// associated logging and tracing event dispatches.
#[macro_export]
macro_rules! err_log {
($out:ident, $level:ident, $($fields:tt)+) => {{
use std::{fmt, fmt::Write};
use ::tracing::{
callsite, callsite2, level_enabled, metadata, valueset, Callsite, Event, __macro_support,
__tracing_log,
field::{Field, ValueSet, Visit},
Level,
};
const LEVEL: Level = $crate::err_lev!($level);
static __CALLSITE: callsite::DefaultCallsite = callsite2! {
name: std::concat! {
"event ",
std::file!(),
":",
std::line!(),
},
kind: metadata::Kind::EVENT,
target: std::module_path!(),
level: LEVEL,
fields: $($fields)+,
};
let visit = &mut |vs: ValueSet<'_>| {
struct Visitor<'a>(&'a mut String);
impl Visit for Visitor<'_> {
fn record_debug(&mut self, field: &Field, val: &dyn fmt::Debug) {
if field.name() == "message" {
write!(self.0, "{:?}", val).expect("stream error");
} else {
write!(self.0, " {}={:?}", field.name(), val).expect("stream error");
}
}
}
let meta = __CALLSITE.metadata();
let enabled = level_enabled!(LEVEL) && {
let interest = __CALLSITE.interest();
!interest.is_never() && __macro_support::__is_enabled(meta, interest)
};
if enabled {
Event::dispatch(meta, &vs);
}
__tracing_log!(LEVEL, __CALLSITE, &vs);
vs.record(&mut Visitor(&mut $out));
};
(visit)(valueset!(__CALLSITE.metadata().fields(), $($fields)+));
($out).into()
}}
}
#[macro_export]
macro_rules! err_lev {
(debug_warn) => {
if $crate::debug::logging() {
::tracing::Level::WARN
} else {
::tracing::Level::DEBUG
}
};
(debug_error) => {
if $crate::debug::logging() {
::tracing::Level::ERROR
} else {
::tracing::Level::DEBUG
}
};
(warn) => {
::tracing::Level::WARN
};
(error) => {
::tracing::Level::ERROR
}; };
} }
+4 -5
View File
@@ -5,7 +5,8 @@ mod response;
use std::{any::Any, borrow::Cow, convert::Infallible, fmt}; use std::{any::Any, borrow::Cow, convert::Infallible, fmt};
pub use self::log::*; pub use log::*;
use crate::error; use crate::error;
#[derive(thiserror::Error)] #[derive(thiserror::Error)]
@@ -62,8 +63,6 @@ pub enum Error {
TomlSerError(#[from] toml::ser::Error), TomlSerError(#[from] toml::ser::Error),
#[error("{0}")] #[error("{0}")]
TomlDeError(#[from] toml::de::Error), TomlDeError(#[from] toml::de::Error),
#[error("{0}")]
Clap(#[from] clap::error::Error),
// ruma // ruma
#[error("{0}")] #[error("{0}")]
@@ -88,13 +87,13 @@ pub enum Error {
// conduwuit // conduwuit
#[error("Arithmetic operation failed: {0}")] #[error("Arithmetic operation failed: {0}")]
Arithmetic(Cow<'static, str>), Arithmetic(Cow<'static, str>),
#[error("{0}")]
BadServerResponse(Cow<'static, str>),
#[error("There was a problem with the '{0}' directive in your configuration: {1}")] #[error("There was a problem with the '{0}' directive in your configuration: {1}")]
Config(&'static str, Cow<'static, str>), Config(&'static str, Cow<'static, str>),
#[error("{0}")] #[error("{0}")]
Database(Cow<'static, str>), Database(Cow<'static, str>),
#[error("{0}")] #[error("{0}")]
BadServerResponse(&'static str),
#[error("{0}")]
Conflict(&'static str), // This is only needed for when a room alias already exists Conflict(&'static str), // This is only needed for when a room alias already exists
// unique / untyped // unique / untyped
-3
View File
@@ -6,6 +6,3 @@ pub mod rustc;
pub mod version; pub mod version;
pub use conduit_macros::rustc_flags_capture; pub use conduit_macros::rustc_flags_capture;
pub const MODULE_ROOT: &str = const_str::split!(std::module_path!(), "::")[0];
pub const CRATE_PREFIX: &str = const_str::split!(MODULE_ROOT, '_')[0];
+5 -8
View File
@@ -2,20 +2,15 @@ use tracing::Level;
use tracing_core::{span::Current, Event}; use tracing_core::{span::Current, Event};
use super::{layer::Value, Layer}; use super::{layer::Value, Layer};
use crate::{info, utils::string::EMPTY};
pub struct Data<'a> { pub struct Data<'a> {
pub layer: &'a Layer, pub layer: &'a Layer,
pub event: &'a Event<'a>, pub event: &'a Event<'a>,
pub current: &'a Current, pub current: &'a Current,
pub values: &'a [Value], pub values: Option<&'a mut [Value]>,
pub scope: &'a [&'static str],
} }
impl Data<'_> { impl Data<'_> {
#[must_use]
pub fn our_modules(&self) -> bool { self.mod_name().starts_with(info::CRATE_PREFIX) }
#[must_use] #[must_use]
pub fn level(&self) -> Level { *self.event.metadata().level() } pub fn level(&self) -> Level { *self.event.metadata().level() }
@@ -23,13 +18,15 @@ impl Data<'_> {
pub fn mod_name(&self) -> &str { self.event.metadata().module_path().unwrap_or_default() } pub fn mod_name(&self) -> &str { self.event.metadata().module_path().unwrap_or_default() }
#[must_use] #[must_use]
pub fn span_name(&self) -> &str { self.current.metadata().map_or(EMPTY, |s| s.name()) } pub fn span_name(&self) -> &str { self.current.metadata().map_or("", |s| s.name()) }
#[must_use] #[must_use]
pub fn message(&self) -> &str { pub fn message(&self) -> &str {
self.values self.values
.as_ref()
.expect("values are not composed for a filter")
.iter() .iter()
.find(|(k, _)| *k == "message") .find(|(k, _)| *k == "message")
.map_or(EMPTY, |(_, v)| v.as_str()) .map_or("", |(_, v)| v.as_str())
} }
} }
+6 -21
View File
@@ -1,25 +1,21 @@
use std::{fmt, sync::Arc}; use std::{fmt, sync::Arc};
use arrayvec::ArrayVec;
use tracing::field::{Field, Visit}; use tracing::field::{Field, Visit};
use tracing_core::{Event, Subscriber}; use tracing_core::{Event, Subscriber};
use tracing_subscriber::{layer::Context, registry::LookupSpan}; use tracing_subscriber::{layer::Context, registry::LookupSpan};
use super::{Capture, Data, State}; use super::{Capture, Data, State};
pub type Value = (&'static str, String);
pub struct Layer { pub struct Layer {
state: Arc<State>, state: Arc<State>,
} }
struct Visitor { struct Visitor {
values: Values, values: Vec<Value>,
} }
type Values = ArrayVec<Value, 32>;
pub type Value = (&'static str, String);
type ScopeNames = ArrayVec<&'static str, 32>;
impl Layer { impl Layer {
#[inline] #[inline]
pub fn new(state: &Arc<State>) -> Self { pub fn new(state: &Arc<State>) -> Self {
@@ -55,9 +51,8 @@ fn handle<S>(layer: &Layer, capture: &Capture, event: &Event<'_>, ctx: &Context<
where where
S: Subscriber + for<'a> LookupSpan<'a>, S: Subscriber + for<'a> LookupSpan<'a>,
{ {
let names = ScopeNames::new();
let mut visitor = Visitor { let mut visitor = Visitor {
values: Values::new(), values: Vec::new(),
}; };
event.record(&mut visitor); event.record(&mut visitor);
@@ -66,8 +61,7 @@ where
layer, layer,
event, event,
current: &ctx.current_span(), current: &ctx.current_span(),
values: &visitor.values, values: Some(&mut visitor.values),
scope: &names,
}); });
} }
@@ -75,21 +69,12 @@ fn filter<S>(layer: &Layer, capture: &Capture, event: &Event<'_>, ctx: &Context<
where where
S: Subscriber + for<'a> LookupSpan<'a>, S: Subscriber + for<'a> LookupSpan<'a>,
{ {
let values = Values::new();
let mut names = ScopeNames::new();
if let Some(scope) = ctx.event_scope(event) {
for span in scope {
names.push(span.name());
}
}
capture.filter.as_ref().map_or(true, |filter| { capture.filter.as_ref().map_or(true, |filter| {
filter(Data { filter(Data {
layer, layer,
event, event,
current: &ctx.current_span(), current: &ctx.current_span(),
values: &values, values: None,
scope: &names,
}) })
}) })
} }
+6 -6
View File
@@ -5,7 +5,7 @@ use crate::Result;
pub fn html<S>(out: &mut S, level: &Level, span: &str, msg: &str) -> Result<()> pub fn html<S>(out: &mut S, level: &Level, span: &str, msg: &str) -> Result<()>
where where
S: Write + ?Sized, S: Write,
{ {
let color = color::code_tag(level); let color = color::code_tag(level);
let level = level.as_str().to_uppercase(); let level = level.as_str().to_uppercase();
@@ -19,7 +19,7 @@ where
pub fn markdown<S>(out: &mut S, level: &Level, span: &str, msg: &str) -> Result<()> pub fn markdown<S>(out: &mut S, level: &Level, span: &str, msg: &str) -> Result<()>
where where
S: Write + ?Sized, S: Write,
{ {
let level = level.as_str().to_uppercase(); let level = level.as_str().to_uppercase();
writeln!(out, "`{level:>5}` `{span:^12}` `{msg}`")?; writeln!(out, "`{level:>5}` `{span:^12}` `{msg}`")?;
@@ -29,19 +29,19 @@ where
pub fn markdown_table<S>(out: &mut S, level: &Level, span: &str, msg: &str) -> Result<()> pub fn markdown_table<S>(out: &mut S, level: &Level, span: &str, msg: &str) -> Result<()>
where where
S: Write + ?Sized, S: Write,
{ {
let level = level.as_str().to_uppercase(); let level = level.as_str().to_uppercase();
writeln!(out, "| {level:>5} | {span:^12} | {msg} |")?; writeln!(out, "| `{level:>5}` | `{span:^12}` | `{msg} |")?;
Ok(()) Ok(())
} }
pub fn markdown_table_head<S>(out: &mut S) -> Result<()> pub fn markdown_table_head<S>(out: &mut S) -> Result<()>
where where
S: Write + ?Sized, S: Write,
{ {
write!(out, "| level | span | message |\n| ------: | :-----: | :------- |\n")?; write!(out, "| level | span | message |\n|------:|:----:|:--------|\n")?;
Ok(()) Ok(())
} }
+1 -1
View File
@@ -16,7 +16,7 @@ pub use error::Error;
pub use info::{rustc_flags_capture, version, version::version}; pub use info::{rustc_flags_capture, version, version::version};
pub use pdu::{PduBuilder, PduCount, PduEvent}; pub use pdu::{PduBuilder, PduCount, PduEvent};
pub use server::Server; pub use server::Server;
pub use utils::{ctor, dtor, implement}; pub use utils::{ctor, dtor};
pub use crate as conduit_core; pub use crate as conduit_core;
+5 -3
View File
@@ -23,7 +23,7 @@ use serde_json::{
value::{to_raw_value, RawValue as RawJsonValue}, value::{to_raw_value, RawValue as RawJsonValue},
}; };
use crate::{err, warn, Error}; use crate::{warn, Error};
#[derive(Deserialize)] #[derive(Deserialize)]
struct ExtractRedactedBecause { struct ExtractRedactedBecause {
@@ -396,8 +396,10 @@ impl Ord for PduEvent {
pub fn gen_event_id_canonical_json( pub fn gen_event_id_canonical_json(
pdu: &RawJsonValue, room_version_id: &RoomVersionId, pdu: &RawJsonValue, room_version_id: &RoomVersionId,
) -> crate::Result<(OwnedEventId, CanonicalJsonObject)> { ) -> crate::Result<(OwnedEventId, CanonicalJsonObject)> {
let value: CanonicalJsonObject = serde_json::from_str(pdu.get()) let value: CanonicalJsonObject = serde_json::from_str(pdu.get()).map_err(|e| {
.map_err(|e| err!(BadServerResponse(warn!("Error parsing incoming event: {e:?}"))))?; warn!("Error parsing incoming event {:?}: {:?}", pdu, e);
Error::BadServerResponse("Invalid PDU in server response")
})?;
let event_id = format!( let event_id = format!(
"${}", "${}",
-1
View File
@@ -17,7 +17,6 @@ use std::cmp::{self, Ordering};
pub use ::ctor::{ctor, dtor}; pub use ::ctor::{ctor, dtor};
pub use bytes::{increment, u64_from_bytes, u64_from_u8, u64_from_u8x8}; pub use bytes::{increment, u64_from_bytes, u64_from_u8, u64_from_u8x8};
pub use conduit_macros::implement;
pub use debug::slice_truncated as debug_slice_truncated; pub use debug::slice_truncated as debug_slice_truncated;
pub use hash::calculate_hash; pub use hash::calculate_hash;
pub use html::Escape as HtmlEscape; pub use html::Escape as HtmlEscape;
+3 -17
View File
@@ -8,12 +8,12 @@ pub const EMPTY: &str = "";
/// arguments are provided the first is assumed to be a format string. /// arguments are provided the first is assumed to be a format string.
#[macro_export] #[macro_export]
macro_rules! format_maybe { macro_rules! format_maybe {
($s:literal $(,)?) => { ($s:literal) => {
if $crate::is_format!($s) { std::format!($s).into() } else { $s.into() } if $crate::is_format!($s) { std::format!($s).into() } else { $s.into() }
}; };
($s:literal, $($args:tt)+) => { ($($args:expr),*) => {
std::format!($s, $($args)+).into() std::format!($($args),*).into()
}; };
} }
@@ -24,20 +24,6 @@ macro_rules! is_format {
($s:literal) => { ($s:literal) => {
::const_str::contains!($s, "{") && ::const_str::contains!($s, "}") ::const_str::contains!($s, "{") && ::const_str::contains!($s, "}")
}; };
($($s:tt)+) => {
false
};
}
#[inline]
pub fn collect_stream<F>(func: F) -> Result<String>
where
F: FnOnce(&mut dyn std::fmt::Write) -> Result<()>,
{
let mut out = String::new();
func(&mut out)?;
Ok(out)
} }
#[inline] #[inline]
+1 -9
View File
@@ -2,12 +2,7 @@ use std::{ops::Index, sync::Arc};
use conduit::{Result, Server}; use conduit::{Result, Server};
use crate::{ use crate::{cork::Cork, maps, maps::Maps, Engine, Map};
cork::Cork,
maps,
maps::{Maps, MapsKey, MapsVal},
Engine, Map,
};
pub struct Database { pub struct Database {
pub db: Arc<Engine>, pub db: Arc<Engine>,
@@ -35,9 +30,6 @@ impl Database {
#[inline] #[inline]
#[must_use] #[must_use]
pub fn cork_and_sync(&self) -> Cork { Cork::new(&self.db, true, true) } pub fn cork_and_sync(&self) -> Cork { Cork::new(&self.db, true, true) }
#[inline]
pub fn iter_maps(&self) -> impl Iterator<Item = (&MapsKey, &MapsVal)> + '_ { self.map.iter() }
} }
impl Index<&str> for Database { impl Index<&str> for Database {
+1 -17
View File
@@ -1,6 +1,5 @@
use std::{ use std::{
collections::{BTreeSet, HashMap}, collections::{BTreeSet, HashMap},
ffi::CStr,
fmt::Write, fmt::Write,
path::PathBuf, path::PathBuf,
sync::{atomic::AtomicU32, Arc, Mutex, RwLock}, sync::{atomic::AtomicU32, Arc, Mutex, RwLock},
@@ -10,8 +9,7 @@ use conduit::{debug, error, info, utils::time::rfc2822_from_seconds, warn, Err,
use rocksdb::{ use rocksdb::{
backup::{BackupEngine, BackupEngineOptions}, backup::{BackupEngine, BackupEngineOptions},
perf::get_memory_usage_stats, perf::get_memory_usage_stats,
AsColumnFamilyRef, BoundColumnFamily, Cache, ColumnFamilyDescriptor, DBCommon, DBWithThreadMode, Env, BoundColumnFamily, Cache, ColumnFamilyDescriptor, DBCommon, DBWithThreadMode, Env, MultiThreaded, Options,
MultiThreaded, Options,
}; };
use crate::{ use crate::{
@@ -242,20 +240,6 @@ impl Engine {
}, },
} }
} }
/// Query for database property by null-terminated name which is expected to
/// have a result with an integer representation. This is intended for
/// low-overhead programmatic use.
pub(crate) fn property_integer(&self, cf: &impl AsColumnFamilyRef, name: &CStr) -> Result<u64> {
result(self.db.property_int_value_cf(cf, name))
.and_then(|val| val.map_or_else(|| Err!("Property {name:?} not found."), Ok))
}
/// Query for database property by name receiving the result in a string.
pub(crate) fn property(&self, cf: &impl AsColumnFamilyRef, name: &str) -> Result<String> {
result(self.db.property_value_cf(cf, name))
.and_then(|val| val.map_or_else(|| Err!("Property {name:?} not found."), Ok))
}
} }
pub(crate) fn repair(db_opts: &Options, path: &PathBuf) -> Result<()> { pub(crate) fn repair(db_opts: &Options, path: &PathBuf) -> Result<()> {
+1 -5
View File
@@ -1,4 +1,4 @@
use std::{ffi::CStr, future::Future, mem::size_of, pin::Pin, sync::Arc}; use std::{future::Future, mem::size_of, pin::Pin, sync::Arc};
use conduit::{utils, Result}; use conduit::{utils, Result};
use rocksdb::{ use rocksdb::{
@@ -189,10 +189,6 @@ impl Map {
self.watchers.watch(prefix) self.watchers.watch(prefix)
} }
pub fn property_integer(&self, name: &CStr) -> Result<u64> { self.db.property_integer(&self.cf(), name) }
pub fn property(&self, name: &str) -> Result<String> { self.db.property(&self.cf(), name) }
#[inline] #[inline]
pub fn name(&self) -> &str { &self.name } pub fn name(&self) -> &str { &self.name }
+1 -3
View File
@@ -4,9 +4,7 @@ use conduit::Result;
use crate::{Engine, Map}; use crate::{Engine, Map};
pub type Maps = BTreeMap<MapsKey, MapsVal>; pub type Maps = BTreeMap<String, Arc<Map>>;
pub(crate) type MapsVal = Arc<Map>;
pub(crate) type MapsKey = String;
pub(crate) fn open(db: &Arc<Engine>) -> Result<Maps> { open_list(db, MAPS) } pub(crate) fn open(db: &Arc<Engine>) -> Result<Maps> { open_list(db, MAPS) }
+3 -18
View File
@@ -2,8 +2,8 @@ use std::{cmp, collections::HashMap};
use conduit::{utils, Config}; use conduit::{utils, Config};
use rocksdb::{ use rocksdb::{
statistics::StatsLevel, BlockBasedOptions, Cache, DBCompactionStyle, DBCompressionType, DBRecoveryMode, Env, BlockBasedOptions, Cache, DBCompactionStyle, DBCompressionType, DBRecoveryMode, Env, LogLevel, Options,
LogLevel, Options, UniversalCompactOptions, UniversalCompactionStopStyle, UniversalCompactOptions, UniversalCompactionStopStyle,
}; };
/// Create database-wide options suitable for opening the database. This also /// Create database-wide options suitable for opening the database. This also
@@ -13,11 +13,6 @@ use rocksdb::{
/// through cf_options(). /// through cf_options().
pub(crate) fn db_options(config: &Config, env: &mut Env, row_cache: &Cache, col_cache: &Cache) -> Options { pub(crate) fn db_options(config: &Config, env: &mut Env, row_cache: &Cache, col_cache: &Cache) -> Options {
const MIN_PARALLELISM: usize = 2; const MIN_PARALLELISM: usize = 2;
const DEFAULT_STATS_LEVEL: StatsLevel = if cfg!(debug_assertions) {
StatsLevel::ExceptDetailedTimers
} else {
StatsLevel::DisableAll
};
let mut opts = Options::default(); let mut opts = Options::default();
@@ -73,18 +68,8 @@ pub(crate) fn db_options(config: &Config, env: &mut Env, row_cache: &Cache, col_
set_compression_defaults(&mut opts, config); set_compression_defaults(&mut opts, config);
// Misc // Misc
opts.create_if_missing(true);
opts.set_disable_auto_compactions(!config.rocksdb_compaction); opts.set_disable_auto_compactions(!config.rocksdb_compaction);
opts.create_if_missing(true);
opts.set_statistics_level(match config.rocksdb_stats_level {
0 => StatsLevel::DisableAll,
1 => DEFAULT_STATS_LEVEL,
2 => StatsLevel::ExceptHistogramOrTimers,
3 => StatsLevel::ExceptTimers,
4 => StatsLevel::ExceptDetailedTimers,
5 => StatsLevel::ExceptTimeForMutex,
6_u8..=u8::MAX => StatsLevel::All,
});
// Default: https://github.com/facebook/rocksdb/wiki/WAL-Recovery-Modes#ktoleratecorruptedtailrecords // Default: https://github.com/facebook/rocksdb/wiki/WAL-Recovery-Modes#ktoleratecorruptedtailrecords
// //
+4 -6
View File
@@ -6,7 +6,6 @@ use axum::{
}; };
use axum_client_ip::SecureClientIpSource; use axum_client_ip::SecureClientIpSource;
use conduit::{error, Result, Server}; use conduit::{error, Result, Server};
use conduit_api::router::state::Guard;
use conduit_service::Services; use conduit_service::Services;
use http::{ use http::{
header::{self, HeaderName}, header::{self, HeaderName},
@@ -36,7 +35,7 @@ const CONDUWUIT_CSP: &[&str] = &[
const CONDUWUIT_PERMISSIONS_POLICY: &[&str] = &["interest-cohort=()", "browsing-topics=()"]; const CONDUWUIT_PERMISSIONS_POLICY: &[&str] = &["interest-cohort=()", "browsing-topics=()"];
pub(crate) fn build(services: &Arc<Services>) -> Result<(Router, Guard)> { pub(crate) fn build(services: &Arc<Services>) -> Result<Router> {
let server = &services.server; let server = &services.server;
let layers = ServiceBuilder::new(); let layers = ServiceBuilder::new();
@@ -48,7 +47,7 @@ pub(crate) fn build(services: &Arc<Services>) -> Result<(Router, Guard)> {
let layers = layers let layers = layers
.sensitive_headers([header::AUTHORIZATION]) .sensitive_headers([header::AUTHORIZATION])
.layer(axum::middleware::from_fn_with_state(Arc::clone(services), request::spawn)) .layer(axum::middleware::from_fn_with_state(Arc::clone(server), request::spawn))
.layer( .layer(
TraceLayer::new_for_http() TraceLayer::new_for_http()
.make_span_with(tracing_span::<_>) .make_span_with(tracing_span::<_>)
@@ -56,7 +55,7 @@ pub(crate) fn build(services: &Arc<Services>) -> Result<(Router, Guard)> {
.on_request(DefaultOnRequest::new().level(Level::TRACE)) .on_request(DefaultOnRequest::new().level(Level::TRACE))
.on_response(DefaultOnResponse::new().level(Level::DEBUG)), .on_response(DefaultOnResponse::new().level(Level::DEBUG)),
) )
.layer(axum::middleware::from_fn_with_state(Arc::clone(services), request::handle)) .layer(axum::middleware::from_fn_with_state(Arc::clone(server), request::handle))
.layer(SecureClientIpSource::ConnectInfo.into_extension()) .layer(SecureClientIpSource::ConnectInfo.into_extension())
.layer(SetResponseHeaderLayer::if_not_present( .layer(SetResponseHeaderLayer::if_not_present(
HeaderName::from_static("origin-agent-cluster"), // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin-Agent-Cluster HeaderName::from_static("origin-agent-cluster"), // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin-Agent-Cluster
@@ -86,8 +85,7 @@ pub(crate) fn build(services: &Arc<Services>) -> Result<(Router, Guard)> {
.layer(body_limit_layer(server)) .layer(body_limit_layer(server))
.layer(CatchPanicLayer::custom(catch_panic)); .layer(CatchPanicLayer::custom(catch_panic));
let (router, guard) = router::build(services); Ok(router::build(services).layer(layers))
Ok((router.layer(layers), guard))
} }
#[cfg(any(feature = "zstd_compression", feature = "gzip_compression", feature = "brotli_compression"))] #[cfg(any(feature = "zstd_compression", feature = "gzip_compression", feature = "brotli_compression"))]
+3 -6
View File
@@ -4,15 +4,13 @@ use axum::{
extract::State, extract::State,
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
use conduit::{debug, debug_error, debug_warn, defer, err, error, trace, Result}; use conduit::{debug, debug_error, debug_warn, defer, err, error, trace, Result, Server};
use conduit_service::Services;
use http::{Method, StatusCode, Uri}; use http::{Method, StatusCode, Uri};
#[tracing::instrument(skip_all, level = "debug")] #[tracing::instrument(skip_all, level = "debug")]
pub(crate) async fn spawn( pub(crate) async fn spawn(
State(services): State<Arc<Services>>, req: http::Request<axum::body::Body>, next: axum::middleware::Next, State(server): State<Arc<Server>>, req: http::Request<axum::body::Body>, next: axum::middleware::Next,
) -> Result<Response, StatusCode> { ) -> Result<Response, StatusCode> {
let server = &services.server;
if !server.running() { if !server.running() {
debug_warn!("unavailable pending shutdown"); debug_warn!("unavailable pending shutdown");
return Err(StatusCode::SERVICE_UNAVAILABLE); return Err(StatusCode::SERVICE_UNAVAILABLE);
@@ -36,9 +34,8 @@ pub(crate) async fn spawn(
#[tracing::instrument(skip_all, name = "handle")] #[tracing::instrument(skip_all, name = "handle")]
pub(crate) async fn handle( pub(crate) async fn handle(
State(services): State<Arc<Services>>, req: http::Request<axum::body::Body>, next: axum::middleware::Next, State(server): State<Arc<Server>>, req: http::Request<axum::body::Body>, next: axum::middleware::Next,
) -> Result<Response, StatusCode> { ) -> Result<Response, StatusCode> {
let server = &services.server;
if !server.running() { if !server.running() {
debug_warn!( debug_warn!(
method = %req.method(), method = %req.method(),
+7 -8
View File
@@ -2,20 +2,19 @@ use std::sync::Arc;
use axum::{response::IntoResponse, routing::get, Router}; use axum::{response::IntoResponse, routing::get, Router};
use conduit::Error; use conduit::Error;
use conduit_api::router::{state, state::Guard}; use conduit_api::State;
use conduit_service::Services; use conduit_service::Services;
use http::{StatusCode, Uri}; use http::{StatusCode, Uri};
use ruma::api::client::error::ErrorKind; use ruma::api::client::error::ErrorKind;
pub(crate) fn build(services: &Arc<Services>) -> (Router, Guard) { pub(crate) fn build(services: &Arc<Services>) -> Router {
let router = Router::<state::State>::new(); let router = Router::<State>::new();
let (state, guard) = state::create(services.clone()); let state = services.clone();
let router = conduit_api::router::build(router, &services.server)
conduit_api::router::build(router, &services.server)
.route("/", get(it_works)) .route("/", get(it_works))
.fallback(not_found) .fallback(not_found)
.with_state(state); .with_state(state)
(router, guard)
} }
async fn not_found(_uri: Uri) -> impl IntoResponse { async fn not_found(_uri: Uri) -> impl IntoResponse {
+1 -1
View File
@@ -18,7 +18,7 @@ pub(super) async fn serve(
let server = &services.server; let server = &services.server;
let config = &server.config; let config = &server.config;
let addrs = config.get_bind_addrs(); let addrs = config.get_bind_addrs();
let (app, _guard) = layers::build(&services)?; let app = layers::build(&services)?;
if cfg!(unix) && config.unix_socket_path.is_some() { if cfg!(unix) && config.unix_socket_path.is_some() {
unix::serve(server, app, shutdown).await unix::serve(server, app, shutdown).await
-1
View File
@@ -69,7 +69,6 @@ termimad.optional = true
tokio.workspace = true tokio.workspace = true
tracing.workspace = true tracing.workspace = true
url.workspace = true url.workspace = true
webpage.workspace = true
[lints] [lints]
workspace = true workspace = true
+4 -4
View File
@@ -51,9 +51,9 @@ pub struct CommandInput {
} }
pub type Completer = fn(&str) -> String; pub type Completer = fn(&str) -> String;
pub type Handler = fn(Arc<crate::Services>, CommandInput) -> HandlerFuture; pub type Handler = fn(Arc<crate::Services>, CommandInput) -> HandlerResult;
pub type HandlerFuture = Pin<Box<dyn Future<Output = HandlerResult> + Send>>; pub type HandlerResult = Pin<Box<dyn Future<Output = CommandResult> + Send>>;
pub type HandlerResult = Result<CommandOutput>; pub type CommandResult = Result<CommandOutput, Error>;
pub type CommandOutput = Option<RoomMessageEventContent>; pub type CommandOutput = Option<RoomMessageEventContent>;
const COMMAND_QUEUE_LIMIT: usize = 512; const COMMAND_QUEUE_LIMIT: usize = 512;
@@ -173,7 +173,7 @@ impl Service {
} }
} }
async fn process_command(&self, command: CommandInput) -> HandlerResult { async fn process_command(&self, command: CommandInput) -> CommandResult {
let Some(services) = self let Some(services) = self
.services .services
.services .services
+2 -30
View File
@@ -743,7 +743,6 @@ async fn db_lt_13(services: &Services) -> Result<()> {
/// again. /// again.
async fn migrate_sha256_media(services: &Services) -> Result<()> { async fn migrate_sha256_media(services: &Services) -> Result<()> {
let db = &services.db; let db = &services.db;
let config = &services.server.config;
warn!("Migrating legacy base64 file names to sha256 file names"); warn!("Migrating legacy base64 file names to sha256 file names");
let mediaid_file = &db["mediaid_file"]; let mediaid_file = &db["mediaid_file"];
@@ -760,9 +759,7 @@ async fn migrate_sha256_media(services: &Services) -> Result<()> {
for (old_path, path) in changes { for (old_path, path) in changes {
if old_path.exists() { if old_path.exists() {
tokio::fs::rename(&old_path, &path).await?; tokio::fs::rename(&old_path, &path).await?;
if config.media_compat_file_link { tokio::fs::symlink(&path, &old_path).await?;
tokio::fs::symlink(&path, &old_path).await?;
}
} }
} }
@@ -825,14 +822,8 @@ async fn handle_media_check(
let (mediaid_file, mediaid_user) = dbs; let (mediaid_file, mediaid_user) = dbs;
let new_exists = files.contains(new_path);
let old_exists = files.contains(old_path); let old_exists = files.contains(old_path);
let old_is_symlink = || async { let new_exists = files.contains(new_path);
tokio::fs::symlink_metadata(old_path)
.await
.map_or(false, |md| md.is_symlink())
};
if !old_exists && !new_exists { if !old_exists && !new_exists {
error!( error!(
media_id = ?encode_key(key), ?new_path, ?old_path, media_id = ?encode_key(key), ?new_path, ?old_path,
@@ -858,29 +849,10 @@ async fn handle_media_check(
"Legacy media found without sha256 migration. Fixing..." "Legacy media found without sha256 migration. Fixing..."
); );
debug_assert!(
old_is_symlink().await,
"Legacy media not expected to be a symlink without an existing sha256 migration."
);
tokio::fs::rename(&old_path, &new_path).await?; tokio::fs::rename(&old_path, &new_path).await?;
tokio::fs::symlink(&new_path, &old_path).await?; tokio::fs::symlink(&new_path, &old_path).await?;
} }
if !config.media_compat_file_link && old_exists && old_is_symlink().await {
debug_warn!(
media_id = ?encode_key(key), ?new_path, ?old_path,
"Legacy link found but compat disabled. Cleansing symlink..."
);
debug_assert!(
new_exists,
"sha256 migration into new file expected prior to cleaning legacy symlink here."
);
tokio::fs::remove_file(&old_path).await?;
}
Ok(()) Ok(())
} }
+1 -1
View File
@@ -4,7 +4,7 @@ use conduit::{debug, debug_info, utils::string_from_bytes, Error, Result};
use database::{Database, Map}; use database::{Database, Map};
use ruma::api::client::error::ErrorKind; use ruma::api::client::error::ErrorKind;
use super::preview::UrlPreviewData; use crate::media::UrlPreviewData;
pub(crate) struct Data { pub(crate) struct Data {
mediaid_file: Arc<Map>, mediaid_file: Arc<Map>,
+42 -20
View File
@@ -1,5 +1,4 @@
mod data; mod data;
mod preview;
mod tests; mod tests;
mod thumbnail; mod thumbnail;
@@ -7,15 +6,16 @@ use std::{path::PathBuf, sync::Arc, time::SystemTime};
use async_trait::async_trait; use async_trait::async_trait;
use base64::{engine::general_purpose, Engine as _}; use base64::{engine::general_purpose, Engine as _};
use conduit::{debug, debug_error, err, error, trace, utils, utils::MutexMap, Err, Result, Server}; use conduit::{debug, debug_error, err, error, utils, utils::MutexMap, Err, Result, Server};
use data::{Data, Metadata}; use data::{Data, Metadata};
use ruma::{OwnedMxcUri, OwnedUserId}; use ruma::{OwnedMxcUri, OwnedUserId};
use serde::Serialize;
use tokio::{ use tokio::{
fs, fs,
io::{AsyncReadExt, AsyncWriteExt, BufReader}, io::{AsyncReadExt, AsyncWriteExt, BufReader},
}; };
use crate::{client, globals, Dep}; use crate::{globals, Dep};
#[derive(Debug)] #[derive(Debug)]
pub struct FileMeta { pub struct FileMeta {
@@ -24,32 +24,43 @@ pub struct FileMeta {
pub content_disposition: Option<String>, pub content_disposition: Option<String>,
} }
#[derive(Serialize, Default)]
pub struct UrlPreviewData {
#[serde(skip_serializing_if = "Option::is_none", rename(serialize = "og:title"))]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename(serialize = "og:description"))]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename(serialize = "og:image"))]
pub image: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename(serialize = "matrix:image:size"))]
pub image_size: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none", rename(serialize = "og:image:width"))]
pub image_width: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none", rename(serialize = "og:image:height"))]
pub image_height: Option<u32>,
}
pub struct Service { pub struct Service {
url_preview_mutex: MutexMap<String, ()>,
pub(crate) db: Data,
services: Services, services: Services,
pub(crate) db: Data,
pub url_preview_mutex: MutexMap<String, ()>,
} }
struct Services { struct Services {
server: Arc<Server>, server: Arc<Server>,
client: Dep<client::Service>,
globals: Dep<globals::Service>, globals: Dep<globals::Service>,
} }
/// generated MXC ID (`media-id`) length
pub const MXC_LENGTH: usize = 32;
#[async_trait] #[async_trait]
impl crate::Service for Service { impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> { fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
Ok(Arc::new(Self { Ok(Arc::new(Self {
url_preview_mutex: MutexMap::new(),
db: Data::new(args.db),
services: Services { services: Services {
server: args.server.clone(), server: args.server.clone(),
client: args.depend::<client::Service>("client"),
globals: args.depend::<globals::Service>("globals"), globals: args.depend::<globals::Service>("globals"),
}, },
db: Data::new(args.db),
url_preview_mutex: MutexMap::new(),
})) }))
} }
@@ -88,15 +99,10 @@ impl Service {
pub async fn delete(&self, mxc: &str) -> Result<()> { pub async fn delete(&self, mxc: &str) -> Result<()> {
if let Ok(keys) = self.db.search_mxc_metadata_prefix(mxc) { if let Ok(keys) = self.db.search_mxc_metadata_prefix(mxc) {
for key in keys { for key in keys {
trace!(?mxc, ?key, "Deleting from filesystem"); self.remove_media_file(&key).await?;
if let Err(e) = self.remove_media_file(&key).await {
error!(?mxc, ?key, "Failed to remove media file: {e}");
}
trace!(?mxc, ?key, "Deleting from database"); debug!("Deleting MXC {mxc} from database");
if let Err(e) = self.db.delete_file_mxc(mxc) { self.db.delete_file_mxc(mxc)?;
error!(?mxc, ?key, "Failed to remove media from database: {e}");
}
} }
Ok(()) Ok(())
@@ -218,6 +224,22 @@ impl Service {
Ok(deletion_count) Ok(deletion_count)
} }
pub async fn get_url_preview(&self, url: &str) -> Option<UrlPreviewData> { self.db.get_url_preview(url) }
/// TODO: use this?
#[allow(dead_code)]
pub async fn remove_url_preview(&self, url: &str) -> Result<()> {
// TODO: also remove the downloaded image
self.db.remove_url_preview(url)
}
pub async fn set_url_preview(&self, url: &str, data: &UrlPreviewData) -> Result<()> {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("valid system time");
self.db.set_url_preview(url, data, now)
}
pub async fn create_media_dir(&self) -> Result<()> { pub async fn create_media_dir(&self) -> Result<()> {
let dir = self.get_media_dir(); let dir = self.get_media_dir();
Ok(fs::create_dir_all(dir).await?) Ok(fs::create_dir_all(dir).await?)
-274
View File
@@ -1,274 +0,0 @@
use std::{io::Cursor, time::SystemTime};
use conduit::{debug, utils, warn, Err, Result};
use conduit_core::implement;
use image::ImageReader as ImgReader;
use ipaddress::IPAddress;
use serde::Serialize;
use url::Url;
use webpage::HTML;
use super::{Service, MXC_LENGTH};
#[derive(Serialize, Default)]
pub struct UrlPreviewData {
#[serde(skip_serializing_if = "Option::is_none", rename(serialize = "og:title"))]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename(serialize = "og:description"))]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename(serialize = "og:image"))]
pub image: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename(serialize = "matrix:image:size"))]
pub image_size: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none", rename(serialize = "og:image:width"))]
pub image_width: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none", rename(serialize = "og:image:height"))]
pub image_height: Option<u32>,
}
#[implement(Service)]
pub async fn remove_url_preview(&self, url: &str) -> Result<()> {
// TODO: also remove the downloaded image
self.db.remove_url_preview(url)
}
#[implement(Service)]
pub async fn set_url_preview(&self, url: &str, data: &UrlPreviewData) -> Result<()> {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("valid system time");
self.db.set_url_preview(url, data, now)
}
#[implement(Service)]
pub async fn download_image(&self, url: &str) -> Result<UrlPreviewData> {
let client = &self.services.client.url_preview;
let image = client.get(url).send().await?.bytes().await?;
let mxc = format!(
"mxc://{}/{}",
self.services.globals.server_name(),
utils::random_string(MXC_LENGTH)
);
self.create(None, &mxc, None, None, &image).await?;
let (width, height) = match ImgReader::new(Cursor::new(&image)).with_guessed_format() {
Err(_) => (None, None),
Ok(reader) => match reader.into_dimensions() {
Err(_) => (None, None),
Ok((width, height)) => (Some(width), Some(height)),
},
};
Ok(UrlPreviewData {
image: Some(mxc),
image_size: Some(image.len()),
image_width: width,
image_height: height,
..Default::default()
})
}
#[implement(Service)]
pub async fn get_url_preview(&self, url: &str) -> Result<UrlPreviewData> {
if let Some(preview) = self.db.get_url_preview(url) {
return Ok(preview);
}
// ensure that only one request is made per URL
let _request_lock = self.url_preview_mutex.lock(url).await;
match self.db.get_url_preview(url) {
Some(preview) => Ok(preview),
None => self.request_url_preview(url).await,
}
}
#[implement(Service)]
async fn request_url_preview(&self, url: &str) -> Result<UrlPreviewData> {
if let Ok(ip) = IPAddress::parse(url) {
if !self.services.globals.valid_cidr_range(&ip) {
return Err!(BadServerResponse("Requesting from this address is forbidden"));
}
}
let client = &self.services.client.url_preview;
let response = client.head(url).send().await?;
if let Some(remote_addr) = response.remote_addr() {
if let Ok(ip) = IPAddress::parse(remote_addr.ip().to_string()) {
if !self.services.globals.valid_cidr_range(&ip) {
return Err!(BadServerResponse("Requesting from this address is forbidden"));
}
}
}
let Some(content_type) = response
.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|x| x.to_str().ok())
else {
return Err!(Request(Unknown("Unknown Content-Type")));
};
let data = match content_type {
html if html.starts_with("text/html") => self.download_html(url).await?,
img if img.starts_with("image/") => self.download_image(url).await?,
_ => return Err!(Request(Unknown("Unsupported Content-Type"))),
};
self.set_url_preview(url, &data).await?;
Ok(data)
}
#[implement(Service)]
async fn download_html(&self, url: &str) -> Result<UrlPreviewData> {
let client = &self.services.client.url_preview;
let mut response = client.get(url).send().await?;
let mut bytes: Vec<u8> = Vec::new();
while let Some(chunk) = response.chunk().await? {
bytes.extend_from_slice(&chunk);
if bytes.len() > self.services.globals.url_preview_max_spider_size() {
debug!(
"Response body from URL {} exceeds url_preview_max_spider_size ({}), not processing the rest of the \
response body and assuming our necessary data is in this range.",
url,
self.services.globals.url_preview_max_spider_size()
);
break;
}
}
let body = String::from_utf8_lossy(&bytes);
let Ok(html) = HTML::from_string(body.to_string(), Some(url.to_owned())) else {
return Err!(Request(Unknown("Failed to parse HTML")));
};
let mut data = match html.opengraph.images.first() {
None => UrlPreviewData::default(),
Some(obj) => self.download_image(&obj.url).await?,
};
let props = html.opengraph.properties;
/* use OpenGraph title/description, but fall back to HTML if not available */
data.title = props.get("title").cloned().or(html.title);
data.description = props.get("description").cloned().or(html.description);
Ok(data)
}
#[implement(Service)]
pub fn url_preview_allowed(&self, url_str: &str) -> bool {
let url: Url = match Url::parse(url_str) {
Ok(u) => u,
Err(e) => {
warn!("Failed to parse URL from a str: {}", e);
return false;
},
};
if ["http", "https"]
.iter()
.all(|&scheme| scheme != url.scheme().to_lowercase())
{
debug!("Ignoring non-HTTP/HTTPS URL to preview: {}", url);
return false;
}
let host = match url.host_str() {
None => {
debug!("Ignoring URL preview for a URL that does not have a host (?): {}", url);
return false;
},
Some(h) => h.to_owned(),
};
let allowlist_domain_contains = self
.services
.globals
.url_preview_domain_contains_allowlist();
let allowlist_domain_explicit = self
.services
.globals
.url_preview_domain_explicit_allowlist();
let denylist_domain_explicit = self.services.globals.url_preview_domain_explicit_denylist();
let allowlist_url_contains = self.services.globals.url_preview_url_contains_allowlist();
if allowlist_domain_contains.contains(&"*".to_owned())
|| allowlist_domain_explicit.contains(&"*".to_owned())
|| allowlist_url_contains.contains(&"*".to_owned())
{
debug!("Config key contains * which is allowing all URL previews. Allowing URL {}", url);
return true;
}
if !host.is_empty() {
if denylist_domain_explicit.contains(&host) {
debug!(
"Host {} is not allowed by url_preview_domain_explicit_denylist (check 1/4)",
&host
);
return false;
}
if allowlist_domain_explicit.contains(&host) {
debug!("Host {} is allowed by url_preview_domain_explicit_allowlist (check 2/4)", &host);
return true;
}
if allowlist_domain_contains
.iter()
.any(|domain_s| domain_s.contains(&host.clone()))
{
debug!("Host {} is allowed by url_preview_domain_contains_allowlist (check 3/4)", &host);
return true;
}
if allowlist_url_contains
.iter()
.any(|url_s| url.to_string().contains(&url_s.to_string()))
{
debug!("URL {} is allowed by url_preview_url_contains_allowlist (check 4/4)", &host);
return true;
}
// check root domain if available and if user has root domain checks
if self.services.globals.url_preview_check_root_domain() {
debug!("Checking root domain");
match host.split_once('.') {
None => return false,
Some((_, root_domain)) => {
if denylist_domain_explicit.contains(&root_domain.to_owned()) {
debug!(
"Root domain {} is not allowed by url_preview_domain_explicit_denylist (check 1/3)",
&root_domain
);
return true;
}
if allowlist_domain_explicit.contains(&root_domain.to_owned()) {
debug!(
"Root domain {} is allowed by url_preview_domain_explicit_allowlist (check 2/3)",
&root_domain
);
return true;
}
if allowlist_domain_contains
.iter()
.any(|domain_s| domain_s.contains(&root_domain.to_owned()))
{
debug!(
"Root domain {} is allowed by url_preview_domain_contains_allowlist (check 3/3)",
&root_domain
);
return true;
}
},
}
}
}
false
}
-1
View File
@@ -18,7 +18,6 @@ pub mod pusher;
pub mod resolver; pub mod resolver;
pub mod rooms; pub mod rooms;
pub mod sending; pub mod sending;
pub mod server_keys;
pub mod transaction_ids; pub mod transaction_ids;
pub mod uiaa; pub mod uiaa;
pub mod updates; pub mod updates;
+17 -16
View File
@@ -3,7 +3,7 @@ mod data;
use std::{fmt::Debug, mem, sync::Arc}; use std::{fmt::Debug, mem, sync::Arc};
use bytes::BytesMut; use bytes::BytesMut;
use conduit::{debug_error, err, trace, utils::string_from_bytes, warn, Err, PduEvent, Result}; use conduit::{debug_info, info, trace, utils::string_from_bytes, warn, Error, PduEvent, Result};
use ipaddress::IPAddress; use ipaddress::IPAddress;
use ruma::{ use ruma::{
api::{ api::{
@@ -84,9 +84,8 @@ impl Service {
let http_request = request let http_request = request
.try_into_http_request::<BytesMut>(&dest, SendAccessToken::IfRequired(""), &VERSIONS) .try_into_http_request::<BytesMut>(&dest, SendAccessToken::IfRequired(""), &VERSIONS)
.map_err(|e| { .map_err(|e| {
err!(BadServerResponse(warn!( warn!("Failed to find destination {dest} for push gateway: {e}");
"Failed to find destination {dest} for push gateway: {e}" Error::BadServerResponse("Invalid push gateway destination")
)))
})? })?
.map(BytesMut::freeze); .map(BytesMut::freeze);
@@ -96,7 +95,7 @@ impl Service {
trace!("Checking request URL for IP"); trace!("Checking request URL for IP");
if let Ok(ip) = IPAddress::parse(url_host) { if let Ok(ip) = IPAddress::parse(url_host) {
if !self.services.globals.valid_cidr_range(&ip) { if !self.services.globals.valid_cidr_range(&ip) {
return Err!(BadServerResponse("Not allowed to send requests to this IP")); return Err(Error::BadServerResponse("Not allowed to send requests to this IP"));
} }
} }
} }
@@ -111,7 +110,7 @@ impl Service {
if let Some(remote_addr) = response.remote_addr() { if let Some(remote_addr) = response.remote_addr() {
if let Ok(ip) = IPAddress::parse(remote_addr.ip().to_string()) { if let Ok(ip) = IPAddress::parse(remote_addr.ip().to_string()) {
if !self.services.globals.valid_cidr_range(&ip) { if !self.services.globals.valid_cidr_range(&ip) {
return Err!(BadServerResponse("Not allowed to send requests to this IP")); return Err(Error::BadServerResponse("Not allowed to send requests to this IP"));
} }
} }
} }
@@ -130,10 +129,10 @@ impl Service {
let body = response.bytes().await?; // TODO: handle timeout let body = response.bytes().await?; // TODO: handle timeout
if !status.is_success() { if !status.is_success() {
debug_error!("Push gateway response body: {:?}", string_from_bytes(&body)); info!("Push gateway {dest} returned unsuccessful HTTP response ({status})");
return Err!(BadServerResponse(error!( debug_info!("Push gateway response body: {:?}", string_from_bytes(&body));
"Push gateway {dest} returned unsuccessful HTTP response: {status}"
))); return Err(Error::BadServerResponse("Push gateway returned unsuccessful response"));
} }
let response = T::IncomingResponse::try_from_http_response( let response = T::IncomingResponse::try_from_http_response(
@@ -141,11 +140,13 @@ impl Service {
.body(body) .body(body)
.expect("reqwest body is valid http body"), .expect("reqwest body is valid http body"),
); );
response response.map_err(|e| {
.map_err(|e| err!(BadServerResponse(error!("Push gateway {dest} returned invalid response: {e}")))) warn!("Push gateway {dest} returned invalid response bytes: {e}");
Error::BadServerResponse("Push gateway returned bad/invalid response")
})
}, },
Err(e) => { Err(e) => {
debug_error!("Could not send request to pusher {dest}: {e}"); warn!("Could not send request to pusher {dest}: {e}");
Err(e.into()) Err(e.into())
}, },
} }
@@ -164,7 +165,7 @@ impl Service {
.room_state_get(&pdu.room_id, &StateEventType::RoomPowerLevels, "")? .room_state_get(&pdu.room_id, &StateEventType::RoomPowerLevels, "")?
.map(|ev| { .map(|ev| {
serde_json::from_str(ev.content.get()) serde_json::from_str(ev.content.get())
.map_err(|e| err!(Database("invalid m.room.power_levels event: {e:?}"))) .map_err(|_| Error::bad_database("invalid m.room.power_levels event"))
}) })
.transpose()? .transpose()?
.unwrap_or_default(); .unwrap_or_default();
@@ -180,8 +181,8 @@ impl Service {
}; };
if notify.is_some() { if notify.is_some() {
return Err!(Database( return Err(Error::bad_database(
r#"Malformed pushrule contains more than one of these actions: ["dont_notify", "notify", "coalesce"]"# r#"Malformed pushrule contains more than one of these actions: ["dont_notify", "notify", "coalesce"]"#,
)); ));
} }
+6 -4
View File
@@ -4,7 +4,7 @@ use std::{
sync::Arc, sync::Arc,
}; };
use conduit::{debug, debug_error, debug_info, debug_warn, err, trace, Err, Result}; use conduit::{debug, debug_error, debug_info, debug_warn, trace, Err, Error, Result};
use hickory_resolver::{error::ResolveError, lookup::SrvLookup}; use hickory_resolver::{error::ResolveError, lookup::SrvLookup};
use ipaddress::IPAddress; use ipaddress::IPAddress;
use ruma::ServerName; use ruma::ServerName;
@@ -329,8 +329,10 @@ impl super::Service {
dest.is_ip_literal() || !IPAddress::is_valid(dest.host()), dest.is_ip_literal() || !IPAddress::is_valid(dest.host()),
"Destination is not an IP literal." "Destination is not an IP literal."
); );
let ip = IPAddress::parse(dest.host()) let ip = IPAddress::parse(dest.host()).map_err(|e| {
.map_err(|e| err!(BadServerResponse(debug_error!("Failed to parse IP literal from string: {e}"))))?; debug_error!("Failed to parse IP literal from string: {}", e);
Error::BadServerResponse("Invalid IP address")
})?;
self.validate_ip(&ip)?; self.validate_ip(&ip)?;
@@ -339,7 +341,7 @@ impl super::Service {
pub(crate) fn validate_ip(&self, ip: &IPAddress) -> Result<()> { pub(crate) fn validate_ip(&self, ip: &IPAddress) -> Result<()> {
if !self.services.globals.valid_cidr_range(ip) { if !self.services.globals.valid_cidr_range(ip) {
return Err!(BadServerResponse("Not allowed to send requests to this IP")); return Err(Error::BadServerResponse("Not allowed to send requests to this IP"));
} }
Ok(()) Ok(())
+13 -16
View File
@@ -1,4 +1,5 @@
mod parse_incoming_pdu; mod parse_incoming_pdu;
mod signing_keys;
use std::{ use std::{
collections::{hash_map, BTreeMap, HashMap, HashSet}, collections::{hash_map, BTreeMap, HashMap, HashSet},
@@ -34,7 +35,7 @@ use ruma::{
use tokio::sync::RwLock; use tokio::sync::RwLock;
use super::state_compressor::CompressedStateEvent; use super::state_compressor::CompressedStateEvent;
use crate::{globals, rooms, sending, server_keys, Dep}; use crate::{globals, rooms, sending, Dep};
pub struct Service { pub struct Service {
services: Services, services: Services,
@@ -49,7 +50,6 @@ struct Services {
metadata: Dep<rooms::metadata::Service>, metadata: Dep<rooms::metadata::Service>,
outlier: Dep<rooms::outlier::Service>, outlier: Dep<rooms::outlier::Service>,
pdu_metadata: Dep<rooms::pdu_metadata::Service>, pdu_metadata: Dep<rooms::pdu_metadata::Service>,
server_keys: Dep<server_keys::Service>,
short: Dep<rooms::short::Service>, short: Dep<rooms::short::Service>,
state: Dep<rooms::state::Service>, state: Dep<rooms::state::Service>,
state_accessor: Dep<rooms::state_accessor::Service>, state_accessor: Dep<rooms::state_accessor::Service>,
@@ -77,7 +77,6 @@ impl crate::Service for Service {
auth_chain: args.depend::<rooms::auth_chain::Service>("rooms::auth_chain"), auth_chain: args.depend::<rooms::auth_chain::Service>("rooms::auth_chain"),
metadata: args.depend::<rooms::metadata::Service>("rooms::metadata"), metadata: args.depend::<rooms::metadata::Service>("rooms::metadata"),
outlier: args.depend::<rooms::outlier::Service>("rooms::outlier"), outlier: args.depend::<rooms::outlier::Service>("rooms::outlier"),
server_keys: args.depend::<server_keys::Service>("server_keys"),
pdu_metadata: args.depend::<rooms::pdu_metadata::Service>("rooms::pdu_metadata"), pdu_metadata: args.depend::<rooms::pdu_metadata::Service>("rooms::pdu_metadata"),
short: args.depend::<rooms::short::Service>("rooms::short"), short: args.depend::<rooms::short::Service>("rooms::short"),
state: args.depend::<rooms::state::Service>("rooms::state"), state: args.depend::<rooms::state::Service>("rooms::state"),
@@ -1188,19 +1187,17 @@ impl Service {
// We go through all the signatures we see on the PDUs and their unresolved // We go through all the signatures we see on the PDUs and their unresolved
// dependencies and fetch the corresponding signing keys // dependencies and fetch the corresponding signing keys
self.services self.fetch_required_signing_keys(
.server_keys events_with_auth_events
.fetch_required_signing_keys( .iter()
events_with_auth_events .flat_map(|(_id, _local_pdu, events)| events)
.iter() .map(|(_event_id, event)| event),
.flat_map(|(_id, _local_pdu, events)| events) pub_key_map,
.map(|(_event_id, event)| event), )
pub_key_map, .await
) .unwrap_or_else(|e| {
.await warn!("Could not fetch all signatures for PDUs from {}: {:?}", origin, e);
.unwrap_or_else(|e| { });
warn!("Could not fetch all signatures for PDUs from {}: {:?}", origin, e);
});
let mut pdus = Vec::with_capacity(events_with_auth_events.len()); let mut pdus = Vec::with_capacity(events_with_auth_events.len());
for (id, local_pdu, events_in_reverse_order) in events_with_auth_events { for (id, local_pdu, events_in_reverse_order) in events_with_auth_events {
@@ -1,18 +1,18 @@
use conduit::{debug_warn, err, pdu::gen_event_id_canonical_json, Err, Result}; use conduit::{pdu::gen_event_id_canonical_json, warn, Err, Error, Result};
use ruma::{CanonicalJsonObject, OwnedEventId, OwnedRoomId, RoomId}; use ruma::{api::client::error::ErrorKind, CanonicalJsonObject, OwnedEventId, OwnedRoomId, RoomId};
use serde_json::value::RawValue as RawJsonValue; use serde_json::value::RawValue as RawJsonValue;
impl super::Service { impl super::Service {
pub fn parse_incoming_pdu(&self, pdu: &RawJsonValue) -> Result<(OwnedEventId, CanonicalJsonObject, OwnedRoomId)> { pub fn parse_incoming_pdu(&self, pdu: &RawJsonValue) -> Result<(OwnedEventId, CanonicalJsonObject, OwnedRoomId)> {
let value: CanonicalJsonObject = serde_json::from_str(pdu.get()).map_err(|e| { let value: CanonicalJsonObject = serde_json::from_str(pdu.get()).map_err(|e| {
debug_warn!("Error parsing incoming event {pdu:#?}"); warn!("Error parsing incoming event {pdu:?}: {e:?}");
err!(BadServerResponse("Error parsing incoming event {e:?}")) Error::BadServerResponse("Invalid PDU in server response")
})?; })?;
let room_id: OwnedRoomId = value let room_id: OwnedRoomId = value
.get("room_id") .get("room_id")
.and_then(|id| RoomId::parse(id.as_str()?).ok()) .and_then(|id| RoomId::parse(id.as_str()?).ok())
.ok_or(err!(Request(InvalidParam("Invalid room id in pdu"))))?; .ok_or(Error::BadRequest(ErrorKind::InvalidParam, "Invalid room id in pdu"))?;
let Ok(room_version_id) = self.services.state.get_room_version(&room_id) else { let Ok(room_version_id) = self.services.state.get_room_version(&room_id) else {
return Err!("Server is not in room {room_id}"); return Err!("Server is not in room {room_id}");
@@ -1,10 +1,9 @@
use std::{ use std::{
collections::{BTreeMap, HashMap, HashSet}, collections::{BTreeMap, HashMap, HashSet},
sync::Arc,
time::{Duration, SystemTime}, time::{Duration, SystemTime},
}; };
use conduit::{debug, debug_error, debug_warn, err, error, info, trace, warn, Err, Result}; use conduit::{debug, error, info, trace, warn, Error, Result};
use futures_util::{stream::FuturesUnordered, StreamExt}; use futures_util::{stream::FuturesUnordered, StreamExt};
use ruma::{ use ruma::{
api::federation::{ api::federation::{
@@ -22,31 +21,7 @@ use ruma::{
use serde_json::value::RawValue as RawJsonValue; use serde_json::value::RawValue as RawJsonValue;
use tokio::sync::{RwLock, RwLockWriteGuard}; use tokio::sync::{RwLock, RwLockWriteGuard};
use crate::{globals, sending, Dep}; impl super::Service {
pub struct Service {
services: Services,
}
struct Services {
globals: Dep<globals::Service>,
sending: Dep<sending::Service>,
}
impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
Ok(Arc::new(Self {
services: Services {
globals: args.depend::<globals::Service>("globals"),
sending: args.depend::<sending::Service>("sending"),
},
}))
}
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
}
impl Service {
pub async fn fetch_required_signing_keys<'a, E>( pub async fn fetch_required_signing_keys<'a, E>(
&'a self, events: E, pub_key_map: &RwLock<BTreeMap<String, BTreeMap<String, Base64>>>, &'a self, events: E, pub_key_map: &RwLock<BTreeMap<String, BTreeMap<String, Base64>>>,
) -> Result<()> ) -> Result<()>
@@ -57,13 +32,13 @@ impl Service {
for event in events { for event in events {
for (signature_server, signature) in event for (signature_server, signature) in event
.get("signatures") .get("signatures")
.ok_or(err!(BadServerResponse("No signatures in server response pdu.")))? .ok_or(Error::BadServerResponse("No signatures in server response pdu."))?
.as_object() .as_object()
.ok_or(err!(BadServerResponse("Invalid signatures object in server response pdu.")))? .ok_or(Error::BadServerResponse("Invalid signatures object in server response pdu."))?
{ {
let signature_object = signature.as_object().ok_or(err!(BadServerResponse( let signature_object = signature.as_object().ok_or(Error::BadServerResponse(
"Invalid signatures content object in server response pdu.", "Invalid signatures content object in server response pdu.",
)))?; ))?;
for signature_id in signature_object.keys() { for signature_id in signature_object.keys() {
server_key_ids server_key_ids
@@ -94,12 +69,10 @@ impl Service {
.map(|(signature_server, signature_ids)| async { .map(|(signature_server, signature_ids)| async {
let fetch_res = self let fetch_res = self
.fetch_signing_keys_for_server( .fetch_signing_keys_for_server(
signature_server.as_str().try_into().map_err(|e| { signature_server.as_str().try_into().map_err(|_| {
( (
signature_server.clone(), signature_server.clone(),
err!(BadServerResponse( Error::BadServerResponse("Invalid servername in signatures of server response pdu."),
"Invalid servername in signatures of server response pdu: {e:?}"
)),
) )
})?, })?,
signature_ids.into_iter().collect(), // HashSet to Vec signature_ids.into_iter().collect(), // HashSet to Vec
@@ -109,9 +82,7 @@ impl Service {
match fetch_res { match fetch_res {
Ok(keys) => Ok((signature_server, keys)), Ok(keys) => Ok((signature_server, keys)),
Err(e) => { Err(e) => {
debug_error!( warn!("Signature verification failed: Could not fetch signing key for {signature_server}: {e}",);
"Signature verification failed: Could not fetch signing key for {signature_server}: {e}",
);
Err((signature_server, e)) Err((signature_server, e))
}, },
} }
@@ -127,7 +98,7 @@ impl Service {
.insert(signature_server.clone(), keys); .insert(signature_server.clone(), keys);
}, },
Err((signature_server, e)) => { Err((signature_server, e)) => {
debug_warn!("Failed to fetch keys for {signature_server}: {e:?}"); warn!("Failed to fetch keys for {}: {:?}", signature_server, e);
}, },
} }
} }
@@ -145,37 +116,35 @@ impl Service {
pub_key_map: &mut RwLockWriteGuard<'_, BTreeMap<String, BTreeMap<String, Base64>>>, pub_key_map: &mut RwLockWriteGuard<'_, BTreeMap<String, BTreeMap<String, Base64>>>,
) -> Result<()> { ) -> Result<()> {
let value: CanonicalJsonObject = serde_json::from_str(pdu.get()).map_err(|e| { let value: CanonicalJsonObject = serde_json::from_str(pdu.get()).map_err(|e| {
debug_error!("Invalid PDU in server response: {pdu:#?}"); error!("Invalid PDU in server response: {:?}: {:?}", pdu, e);
err!(BadServerResponse(error!("Invalid PDU in server response: {e:?}"))) Error::BadServerResponse("Invalid PDU in server response")
})?; })?;
let signatures = value let signatures = value
.get("signatures") .get("signatures")
.ok_or(err!(BadServerResponse("No signatures in server response pdu.")))? .ok_or(Error::BadServerResponse("No signatures in server response pdu."))?
.as_object() .as_object()
.ok_or(err!(BadServerResponse("Invalid signatures object in server response pdu.")))?; .ok_or(Error::BadServerResponse("Invalid signatures object in server response pdu."))?;
for (signature_server, signature) in signatures { for (signature_server, signature) in signatures {
let signature_object = signature.as_object().ok_or(err!(BadServerResponse( let signature_object = signature.as_object().ok_or(Error::BadServerResponse(
"Invalid signatures content object in server response pdu.", "Invalid signatures content object in server response pdu.",
)))?; ))?;
let signature_ids = signature_object.keys().cloned().collect::<Vec<_>>(); let signature_ids = signature_object.keys().cloned().collect::<Vec<_>>();
let contains_all_ids = let contains_all_ids =
|keys: &BTreeMap<String, Base64>| signature_ids.iter().all(|id| keys.contains_key(id)); |keys: &BTreeMap<String, Base64>| signature_ids.iter().all(|id| keys.contains_key(id));
let origin = <&ServerName>::try_from(signature_server.as_str()).map_err(|e| { let origin = <&ServerName>::try_from(signature_server.as_str())
err!(BadServerResponse( .map_err(|_| Error::BadServerResponse("Invalid servername in signatures of server response pdu."))?;
"Invalid servername in signatures of server response pdu: {e:?}"
))
})?;
if servers.contains_key(origin) || pub_key_map.contains_key(origin.as_str()) { if servers.contains_key(origin) || pub_key_map.contains_key(origin.as_str()) {
continue; continue;
} }
debug!("Loading signing keys for {origin}"); debug!("Loading signing keys for {}", origin);
let result: BTreeMap<_, _> = self let result: BTreeMap<_, _> = self
.services .services
.globals .globals
@@ -185,7 +154,7 @@ impl Service {
.collect(); .collect();
if !contains_all_ids(&result) { if !contains_all_ids(&result) {
debug_warn!("Signing key not loaded for {origin}"); debug!("Signing key not loaded for {}", origin);
servers.insert(origin.to_owned(), BTreeMap::new()); servers.insert(origin.to_owned(), BTreeMap::new());
} }
@@ -202,7 +171,7 @@ impl Service {
pub_key_map: &RwLock<BTreeMap<String, BTreeMap<String, Base64>>>, pub_key_map: &RwLock<BTreeMap<String, BTreeMap<String, Base64>>>,
) -> Result<()> { ) -> Result<()> {
for server in self.services.globals.trusted_servers() { for server in self.services.globals.trusted_servers() {
debug!("Asking batch signing keys from trusted server {server}"); debug!("Asking batch signing keys from trusted server {}", server);
match self match self
.services .services
.sending .sending
@@ -215,16 +184,14 @@ impl Service {
.await .await
{ {
Ok(keys) => { Ok(keys) => {
debug!("Got signing keys: {keys:?}"); debug!("Got signing keys: {:?}", keys);
let mut pkm = pub_key_map.write().await; let mut pkm = pub_key_map.write().await;
for k in keys.server_keys { for k in keys.server_keys {
let k = match k.deserialize() { let k = match k.deserialize() {
Ok(key) => key, Ok(key) => key,
Err(e) => { Err(e) => {
warn!( warn!("Received error {e} while fetching keys from trusted server {server}");
"Received error {e} while fetching keys from trusted server {server}: {:#?}", warn!("{}", k.into_json());
k.into_json()
);
continue; continue;
}, },
}; };
@@ -244,10 +211,13 @@ impl Service {
pkm.insert(k.server_name.to_string(), result); pkm.insert(k.server_name.to_string(), result);
} }
}, },
Err(e) => error!( Err(e) => {
"Failed sending batched key request to trusted key server {server} for the remote servers \ warn!(
{servers:?}: {e}" "Failed sending batched key request to trusted key server {server} for the remote servers \
), {:?}: {e}",
servers
);
},
} }
} }
@@ -483,6 +453,7 @@ impl Service {
} }
} else { } else {
info!("query_trusted_key_servers_first is set to false, querying {origin} first"); info!("query_trusted_key_servers_first is set to false, querying {origin} first");
debug!("Asking {origin} for their signing keys over federation"); debug!("Asking {origin} for their signing keys over federation");
if let Some(server_key) = self if let Some(server_key) = self
.services .services
@@ -540,7 +511,7 @@ impl Service {
.filter_map(|e| e.deserialize().ok()) .filter_map(|e| e.deserialize().ok())
.collect::<Vec<_>>() .collect::<Vec<_>>()
}) { }) {
debug!("Got signing keys: {server_keys:?}"); debug!("Got signing keys: {:?}", server_keys);
for k in server_keys { for k in server_keys {
self.services self.services
.globals .globals
@@ -565,6 +536,7 @@ impl Service {
} }
} }
Err!(BadServerResponse(warn!("Failed to find public key for server {origin:?}"))) warn!("Failed to find public key for server: {origin}");
Err(Error::BadServerResponse("Failed to find public key for server"))
} }
} }
+2 -4
View File
@@ -41,7 +41,7 @@ use tokio::sync::RwLock;
use self::data::Data; use self::data::Data;
use crate::{ use crate::{
account_data, admin, appservice, appservice::NamespaceRegex, globals, pusher, rooms, account_data, admin, appservice, appservice::NamespaceRegex, globals, pusher, rooms,
rooms::state_compressor::CompressedStateEvent, sending, server_keys, Dep, rooms::state_compressor::CompressedStateEvent, sending, Dep,
}; };
// Update Relationships // Update Relationships
@@ -86,7 +86,6 @@ struct Services {
pdu_metadata: Dep<rooms::pdu_metadata::Service>, pdu_metadata: Dep<rooms::pdu_metadata::Service>,
read_receipt: Dep<rooms::read_receipt::Service>, read_receipt: Dep<rooms::read_receipt::Service>,
sending: Dep<sending::Service>, sending: Dep<sending::Service>,
server_keys: Dep<server_keys::Service>,
user: Dep<rooms::user::Service>, user: Dep<rooms::user::Service>,
pusher: Dep<pusher::Service>, pusher: Dep<pusher::Service>,
threads: Dep<rooms::threads::Service>, threads: Dep<rooms::threads::Service>,
@@ -115,7 +114,6 @@ impl crate::Service for Service {
pdu_metadata: args.depend::<rooms::pdu_metadata::Service>("rooms::pdu_metadata"), pdu_metadata: args.depend::<rooms::pdu_metadata::Service>("rooms::pdu_metadata"),
read_receipt: args.depend::<rooms::read_receipt::Service>("rooms::read_receipt"), read_receipt: args.depend::<rooms::read_receipt::Service>("rooms::read_receipt"),
sending: args.depend::<sending::Service>("sending"), sending: args.depend::<sending::Service>("sending"),
server_keys: args.depend::<server_keys::Service>("server_keys"),
user: args.depend::<rooms::user::Service>("rooms::user"), user: args.depend::<rooms::user::Service>("rooms::user"),
pusher: args.depend::<pusher::Service>("pusher"), pusher: args.depend::<pusher::Service>("pusher"),
threads: args.depend::<rooms::threads::Service>("rooms::threads"), threads: args.depend::<rooms::threads::Service>("rooms::threads"),
@@ -1183,7 +1181,7 @@ impl Service {
} }
self.services self.services
.server_keys .event_handler
.fetch_required_signing_keys([&value], pub_key_map) .fetch_required_signing_keys([&value], pub_key_map)
.await?; .await?;
+12 -9
View File
@@ -1,7 +1,7 @@
use std::{fmt::Debug, mem}; use std::{fmt::Debug, mem};
use bytes::BytesMut; use bytes::BytesMut;
use conduit::{debug_error, err, trace, utils, warn, Err, Result}; use conduit::{debug_error, trace, utils, warn, Error, Result};
use reqwest::Client; use reqwest::Client;
use ruma::api::{appservice::Registration, IncomingResponse, MatrixVersion, OutgoingRequest, SendAccessToken}; use ruma::api::{appservice::Registration, IncomingResponse, MatrixVersion, OutgoingRequest, SendAccessToken};
@@ -26,7 +26,10 @@ where
let hs_token = registration.hs_token.as_str(); let hs_token = registration.hs_token.as_str();
let mut http_request = request let mut http_request = request
.try_into_http_request::<BytesMut>(&dest, SendAccessToken::IfRequired(hs_token), &VERSIONS) .try_into_http_request::<BytesMut>(&dest, SendAccessToken::IfRequired(hs_token), &VERSIONS)
.map_err(|e| err!(BadServerResponse(warn!("Failed to find destination {dest}: {e}"))))? .map_err(|e| {
warn!("Failed to find destination {dest}: {e}");
Error::BadServerResponse("Invalid appservice destination")
})?
.map(BytesMut::freeze); .map(BytesMut::freeze);
let mut parts = http_request.uri().clone().into_parts(); let mut parts = http_request.uri().clone().into_parts();
@@ -66,11 +69,13 @@ where
let body = response.bytes().await?; // TODO: handle timeout let body = response.bytes().await?; // TODO: handle timeout
if !status.is_success() { if !status.is_success() {
debug_error!("Appservice response bytes: {:?}", utils::string_from_bytes(&body)); warn!(
return Err!(BadServerResponse(error!(
"Appservice \"{}\" returned unsuccessful HTTP response {status} at {dest}", "Appservice \"{}\" returned unsuccessful HTTP response {status} at {dest}",
registration.id registration.id
))); );
debug_error!("Appservice response bytes: {:?}", utils::string_from_bytes(&body));
return Err(Error::BadServerResponse("Appservice returned unsuccessful HTTP response"));
} }
let response = T::IncomingResponse::try_from_http_response( let response = T::IncomingResponse::try_from_http_response(
@@ -80,9 +85,7 @@ where
); );
response.map(Some).map_err(|e| { response.map(Some).map_err(|e| {
err!(BadServerResponse(error!( warn!("Appservice \"{}\" returned invalid response bytes {dest}: {e}", registration.id);
"Appservice \"{}\" returned invalid response bytes {dest}: {e}", Error::BadServerResponse("Appservice returned bad/invalid response")
registration.id
)))
}) })
} }
+343
View File
@@ -0,0 +1,343 @@
use std::{
fmt::Debug,
net::{IpAddr, SocketAddr},
};
use conduit::{debug, debug_error, debug_info, debug_warn, trace, Err, Error, Result};
use hickory_resolver::{error::ResolveError, lookup::SrvLookup};
use ipaddress::IPAddress;
use ruma::ServerName;
use crate::{
resolver::{add_port_to_hostname, get_ip_with_port, CachedDest, CachedOverride, FedDest},
services,
};
#[derive(Clone, Debug)]
pub(crate) struct ActualDest {
pub(crate) dest: FedDest,
pub(crate) host: String,
pub(crate) string: String,
pub(crate) cached: bool,
}
#[tracing::instrument(skip_all, name = "resolve")]
pub(crate) async fn get_actual_dest(server_name: &ServerName) -> Result<ActualDest> {
let cached;
let cached_result = services().resolver.get_cached_destination(server_name);
let CachedDest {
dest,
host,
..
} = if let Some(result) = cached_result {
cached = true;
result
} else {
cached = false;
validate_dest(server_name)?;
resolve_actual_dest(server_name, true).await?
};
let string = dest.clone().into_https_string();
Ok(ActualDest {
dest,
host,
string,
cached,
})
}
/// Returns: `actual_destination`, host header
/// Implemented according to the specification at <https://matrix.org/docs/spec/server_server/r0.1.4#resolving-server-names>
/// Numbers in comments below refer to bullet points in linked section of
/// specification
#[tracing::instrument(skip_all, name = "actual")]
pub async fn resolve_actual_dest(dest: &ServerName, cache: bool) -> Result<CachedDest> {
trace!("Finding actual destination for {dest}");
let mut host = dest.as_str().to_owned();
let actual_dest = match get_ip_with_port(dest.as_str()) {
Some(host_port) => actual_dest_1(host_port)?,
None => {
if let Some(pos) = dest.as_str().find(':') {
actual_dest_2(dest, cache, pos).await?
} else if let Some(delegated) = request_well_known(dest.as_str()).await? {
actual_dest_3(&mut host, cache, delegated).await?
} else if let Some(overrider) = query_srv_record(dest.as_str()).await? {
actual_dest_4(&host, cache, overrider).await?
} else {
actual_dest_5(dest, cache).await?
}
},
};
// Can't use get_ip_with_port here because we don't want to add a port
// to an IP address if it wasn't specified
let host = if let Ok(addr) = host.parse::<SocketAddr>() {
FedDest::Literal(addr)
} else if let Ok(addr) = host.parse::<IpAddr>() {
FedDest::Named(addr.to_string(), ":8448".to_owned())
} else if let Some(pos) = host.find(':') {
let (host, port) = host.split_at(pos);
FedDest::Named(host.to_owned(), port.to_owned())
} else {
FedDest::Named(host, ":8448".to_owned())
};
debug!("Actual destination: {actual_dest:?} hostname: {host:?}");
Ok(CachedDest {
dest: actual_dest,
host: host.into_uri_string(),
expire: CachedDest::default_expire(),
})
}
fn actual_dest_1(host_port: FedDest) -> Result<FedDest> {
debug!("1: IP literal with provided or default port");
Ok(host_port)
}
async fn actual_dest_2(dest: &ServerName, cache: bool, pos: usize) -> Result<FedDest> {
debug!("2: Hostname with included port");
let (host, port) = dest.as_str().split_at(pos);
conditional_query_and_cache_override(host, host, port.parse::<u16>().unwrap_or(8448), cache).await?;
Ok(FedDest::Named(host.to_owned(), port.to_owned()))
}
async fn actual_dest_3(host: &mut String, cache: bool, delegated: String) -> Result<FedDest> {
debug!("3: A .well-known file is available");
*host = add_port_to_hostname(&delegated).into_uri_string();
match get_ip_with_port(&delegated) {
Some(host_and_port) => actual_dest_3_1(host_and_port),
None => {
if let Some(pos) = delegated.find(':') {
actual_dest_3_2(cache, delegated, pos).await
} else {
trace!("Delegated hostname has no port in this branch");
if let Some(overrider) = query_srv_record(&delegated).await? {
actual_dest_3_3(cache, delegated, overrider).await
} else {
actual_dest_3_4(cache, delegated).await
}
}
},
}
}
fn actual_dest_3_1(host_and_port: FedDest) -> Result<FedDest> {
debug!("3.1: IP literal in .well-known file");
Ok(host_and_port)
}
async fn actual_dest_3_2(cache: bool, delegated: String, pos: usize) -> Result<FedDest> {
debug!("3.2: Hostname with port in .well-known file");
let (host, port) = delegated.split_at(pos);
conditional_query_and_cache_override(host, host, port.parse::<u16>().unwrap_or(8448), cache).await?;
Ok(FedDest::Named(host.to_owned(), port.to_owned()))
}
async fn actual_dest_3_3(cache: bool, delegated: String, overrider: FedDest) -> Result<FedDest> {
debug!("3.3: SRV lookup successful");
let force_port = overrider.port();
conditional_query_and_cache_override(&delegated, &overrider.hostname(), force_port.unwrap_or(8448), cache).await?;
if let Some(port) = force_port {
Ok(FedDest::Named(delegated, format!(":{port}")))
} else {
Ok(add_port_to_hostname(&delegated))
}
}
async fn actual_dest_3_4(cache: bool, delegated: String) -> Result<FedDest> {
debug!("3.4: No SRV records, just use the hostname from .well-known");
conditional_query_and_cache_override(&delegated, &delegated, 8448, cache).await?;
Ok(add_port_to_hostname(&delegated))
}
async fn actual_dest_4(host: &str, cache: bool, overrider: FedDest) -> Result<FedDest> {
debug!("4: No .well-known; SRV record found");
let force_port = overrider.port();
conditional_query_and_cache_override(host, &overrider.hostname(), force_port.unwrap_or(8448), cache).await?;
if let Some(port) = force_port {
Ok(FedDest::Named(host.to_owned(), format!(":{port}")))
} else {
Ok(add_port_to_hostname(host))
}
}
async fn actual_dest_5(dest: &ServerName, cache: bool) -> Result<FedDest> {
debug!("5: No SRV record found");
conditional_query_and_cache_override(dest.as_str(), dest.as_str(), 8448, cache).await?;
Ok(add_port_to_hostname(dest.as_str()))
}
#[tracing::instrument(skip_all, name = "well-known")]
async fn request_well_known(dest: &str) -> Result<Option<String>> {
trace!("Requesting well known for {dest}");
if !services().resolver.has_cached_override(dest) {
query_and_cache_override(dest, dest, 8448).await?;
}
let response = services()
.client
.well_known
.get(&format!("https://{dest}/.well-known/matrix/server"))
.send()
.await;
trace!("response: {:?}", response);
if let Err(e) = &response {
debug!("error: {e:?}");
return Ok(None);
}
let response = response?;
if !response.status().is_success() {
debug!("response not 2XX");
return Ok(None);
}
let text = response.text().await?;
trace!("response text: {:?}", text);
if text.len() >= 12288 {
debug_warn!("response contains junk");
return Ok(None);
}
let body: serde_json::Value = serde_json::from_str(&text).unwrap_or_default();
let m_server = body
.get("m.server")
.unwrap_or(&serde_json::Value::Null)
.as_str()
.unwrap_or_default();
if ruma::identifiers_validation::server_name::validate(m_server).is_err() {
debug_error!("response content missing or invalid");
return Ok(None);
}
debug_info!("{:?} found at {:?}", dest, m_server);
Ok(Some(m_server.to_owned()))
}
#[inline]
async fn conditional_query_and_cache_override(overname: &str, hostname: &str, port: u16, cache: bool) -> Result<()> {
if cache {
query_and_cache_override(overname, hostname, port).await
} else {
Ok(())
}
}
#[tracing::instrument(skip_all, name = "ip")]
async fn query_and_cache_override(overname: &'_ str, hostname: &'_ str, port: u16) -> Result<()> {
match services()
.resolver
.resolver
.lookup_ip(hostname.to_owned())
.await
{
Err(e) => handle_resolve_error(&e),
Ok(override_ip) => {
if hostname != overname {
debug_info!("{overname:?} overriden by {hostname:?}");
}
services().resolver.set_cached_override(
overname.to_owned(),
CachedOverride {
ips: override_ip.iter().collect(),
port,
expire: CachedOverride::default_expire(),
},
);
Ok(())
},
}
}
#[tracing::instrument(skip_all, name = "srv")]
async fn query_srv_record(hostname: &'_ str) -> Result<Option<FedDest>> {
fn handle_successful_srv(srv: &SrvLookup) -> Option<FedDest> {
srv.iter().next().map(|result| {
FedDest::Named(
result.target().to_string().trim_end_matches('.').to_owned(),
format!(":{}", result.port()),
)
})
}
async fn lookup_srv(hostname: &str) -> Result<SrvLookup, ResolveError> {
debug!("querying SRV for {:?}", hostname);
let hostname = hostname.trim_end_matches('.');
services()
.resolver
.resolver
.srv_lookup(hostname.to_owned())
.await
}
let hostnames = [format!("_matrix-fed._tcp.{hostname}."), format!("_matrix._tcp.{hostname}.")];
for hostname in hostnames {
match lookup_srv(&hostname).await {
Ok(result) => return Ok(handle_successful_srv(&result)),
Err(e) => handle_resolve_error(&e)?,
}
}
Ok(None)
}
#[allow(clippy::single_match_else)]
fn handle_resolve_error(e: &ResolveError) -> Result<()> {
use hickory_resolver::error::ResolveErrorKind;
match *e.kind() {
ResolveErrorKind::NoRecordsFound {
..
} => {
// Raise to debug_warn if we can find out the result wasn't from cache
debug!("{e}");
Ok(())
},
_ => Err!(error!("DNS {e}")),
}
}
fn validate_dest(dest: &ServerName) -> Result<()> {
if dest == services().globals.server_name() {
return Err!("Won't send federation request to ourselves");
}
if dest.is_ip_literal() || IPAddress::is_valid(dest.host()) {
validate_dest_ip_literal(dest)?;
}
Ok(())
}
fn validate_dest_ip_literal(dest: &ServerName) -> Result<()> {
trace!("Destination is an IP literal, checking against IP range denylist.",);
debug_assert!(
dest.is_ip_literal() || !IPAddress::is_valid(dest.host()),
"Destination is not an IP literal."
);
let ip = IPAddress::parse(dest.host()).map_err(|e| {
debug_error!("Failed to parse IP literal from string: {}", e);
Error::BadServerResponse("Invalid IP address")
})?;
validate_ip(&ip)?;
Ok(())
}
pub(crate) fn validate_ip(ip: &IPAddress) -> Result<()> {
if !services().globals.valid_cidr_range(ip) {
return Err(Error::BadServerResponse("Not allowed to send requests to this IP"));
}
Ok(())
}
+17 -3
View File
@@ -1,7 +1,8 @@
use std::{fmt::Debug, mem}; use std::{fmt::Debug, mem};
use conduit::{ use conduit::{
debug, debug_error, debug_warn, err, error::inspect_debug_log, trace, utils::string::EMPTY, Err, Error, Result, debug, debug_error, debug_info, debug_warn, error::inspect_debug_log, trace, utils::string::EMPTY, Err, Error,
Result,
}; };
use http::{header::AUTHORIZATION, HeaderValue}; use http::{header::AUTHORIZATION, HeaderValue};
use ipaddress::IPAddress; use ipaddress::IPAddress;
@@ -31,6 +32,16 @@ impl super::Service {
return Err!(Config("allow_federation", "Federation is disabled.")); return Err!(Config("allow_federation", "Federation is disabled."));
} }
if self
.server
.config
.forbidden_remote_server_names
.contains(&dest.to_owned())
{
debug_info!("Refusing to send outbound federation request to {dest}");
return Err!(Request(Forbidden("Federation with this homeserver is not allowed.")));
}
let actual = self.services.resolver.get_actual_dest(dest).await?; let actual = self.services.resolver.get_actual_dest(dest).await?;
let request = self.prepare::<T>(dest, &actual, req).await?; let request = self.prepare::<T>(dest, &actual, req).await?;
self.execute::<T>(dest, &actual, request, client).await self.execute::<T>(dest, &actual, request, client).await
@@ -62,7 +73,7 @@ impl super::Service {
trace!("Preparing request"); trace!("Preparing request");
let mut http_request = req let mut http_request = req
.try_into_http_request::<Vec<u8>>(&actual.string, SATIR, &VERSIONS) .try_into_http_request::<Vec<u8>>(&actual.string, SATIR, &VERSIONS)
.map_err(|e| err!(BadServerResponse("Invalid destination: {e:?}")))?; .map_err(|_| Error::BadServerResponse("Invalid destination"))?;
sign_request::<T>(&self.services.globals, dest, &mut http_request); sign_request::<T>(&self.services.globals, dest, &mut http_request);
@@ -139,7 +150,10 @@ where
); );
} }
response.map_err(|e| err!(BadServerResponse("Server returned bad 200 response: {e:?}"))) match response {
Err(_) => Err(Error::BadServerResponse("Server returned bad 200 response.")),
Ok(response) => Ok(response),
}
} }
fn handle_error<T>( fn handle_error<T>(
+1 -3
View File
@@ -12,7 +12,7 @@ use tokio::sync::Mutex;
use crate::{ use crate::{
account_data, admin, appservice, client, emergency, globals, key_backups, account_data, admin, appservice, client, emergency, globals, key_backups,
manager::Manager, manager::Manager,
media, presence, pusher, resolver, rooms, sending, server_keys, service, media, presence, pusher, resolver, rooms, sending, service,
service::{Args, Map, Service}, service::{Args, Map, Service},
transaction_ids, uiaa, updates, users, transaction_ids, uiaa, updates, users,
}; };
@@ -31,7 +31,6 @@ pub struct Services {
pub resolver: Arc<resolver::Service>, pub resolver: Arc<resolver::Service>,
pub rooms: rooms::Service, pub rooms: rooms::Service,
pub sending: Arc<sending::Service>, pub sending: Arc<sending::Service>,
pub server_keys: Arc<server_keys::Service>,
pub transaction_ids: Arc<transaction_ids::Service>, pub transaction_ids: Arc<transaction_ids::Service>,
pub uiaa: Arc<uiaa::Service>, pub uiaa: Arc<uiaa::Service>,
pub updates: Arc<updates::Service>, pub updates: Arc<updates::Service>,
@@ -95,7 +94,6 @@ impl Services {
user: build!(rooms::user::Service), user: build!(rooms::user::Service),
}, },
sending: build!(sending::Service), sending: build!(sending::Service),
server_keys: build!(server_keys::Service),
transaction_ids: build!(transaction_ids::Service), transaction_ids: build!(transaction_ids::Service),
uiaa: build!(uiaa::Service), uiaa: build!(uiaa::Service),
updates: build!(updates::Service), updates: build!(updates::Service),