Compare commits

...

112 Commits

Author SHA1 Message Date
Jason Volk b8b93a2e86 Bump 0.4.1
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk 29d69b7688 update complement test results
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
strawberry bd07fb61e0 add hot_reload.md to SUMMARY.md
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry a41a60ef07 media: dont ignore requested filename on /download for Content-Disposition
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
Jason Volk ec7a9ab726 add toolchain and build/check shortcut to smoketest
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk 25f598ce6c enable http2 feature for reqwest.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk dbcb3be0ab fix duplicate output; increase wait in smoketest.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk a537462d51 replace num_cpus dependency with available_parallelism()
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk d2aef071bc add possibly referenced rocksdb symbol to export list.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk d68b11e8ff fix rustflags for release-max-perf
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk 9cf5b0926e fix regressed jemalloc stats feature
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk ff0b57c89c remove unused jemalloc dep in main module.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk b94045a468 dissolve key_value/*
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk 3122648767 split ruma_wrapper from_request() related.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk 3f5349ad76 simplify RumaHandler for Router building.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk 27dcf213f1 tweak error strings.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk a1b526b3b7 tweak log levels
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk dc614e11d6 check invite target is our server.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk c5569b4c6e dedup acl checks
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk 71a1285c7b hoist receipt ACL check
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
strawberry abdda6cf32 check invited user's server against ACLs on /invite
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry 4d21f9d962 use ok_or_else instead of ok_or for function calls in server_server.rs
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry 1013fe5a42 check for membership join state at /send_join
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry f31b7b9420 ignore inbound EDUs for users that dont belong to origin server
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry e5e358cc68 compare X-Matrix origin + body origin and check PDU/EDU length at /send txn
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry 50bc7cc005 check state_key matches sender user at /send_leave
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry 445015e9ea check user ID server against ACLs at /send_leave
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry 7a38c12e5d check for member event type at /send_leave
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry 2a77951152 check for membership leave state at /send_leave
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry 0256c27363 check if we know about room at /make_leave
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry 826edc0a3a check state_key matches sender user at /send_join
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry a5043a38e1 only allow membership event types at /send_join
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry bfd471a863 check user ID server against ACLs for /send_join
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry 3981e77ec6 check user ID server against ACLs for /make_join
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry 81bf4b7150 check user ID server against ACLs for /make_leave
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry b8ec763a7c ignore read receipts from ACL'd servers and users not joined
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry 003d4edbfa debug log receiving typing EDUs for users not in room
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry 4f0006d18a ignore typing EDUs from ACL'd user's servers
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry b822e3a94c listen on IPv6 localhost by default
this is dual-stack by default on linux, resolves
issues with nginx using `localhost` and randomly
choosing between 127.0.0.1 and [::1], causing
intermittent upstream issues

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry 68fffe8e96 check room ACLs on sender user's server for incoming PDUs
`handle_incoming_pdu`

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry 7328ed7509 rename misleading sender_servername to origin
this is the X-Matrix origin/server, NOT the `"sender"``
user's server name.

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
Benjamin Lee 6ccf578437 bump rocksdb input
Nedded to pull in [1], which is rared for dynamic rocksdb builds with
liburing.

[1]: https://github.com/girlbossceo/rocksdb/commit/c8a1450231e9c608edf535538dbe8ca1a8d2f3bc
2024-05-27 04:54:57 -04:00
Benjamin Lee 8a1848a814 Revert "nix: default output to scopeHostStatic instead of scopeHost"
This reverts commit a37b2b9e64.

Dynamic builds are working again, so we'd prefer having that be the
default output for consistency with nixpkgs.
2024-05-27 04:54:57 -04:00
Benjamin Lee b4cd8e9140 fix dynamic builds with liburing
The original implementation of this was really weird, so I restructed it
a lot while debugging, and am just gonna leave the restructured version.

Root cause of the segfault seems to be that upstream nixpkgs liburing
derivation is generating both static and dynamic libraries, causing
rocksdb to statically link liburing in a dynamic build, pulling in some
allocator stuff at the same time. I created a PR[1] to fix this upstream,
but it probably won't be available on nixos-unstable for quite a while,
so we can also patch it locally.

[1]: https://github.com/NixOS/nixpkgs/pull/314945
2024-05-27 04:54:57 -04:00
Benjamin Lee a08f90b161 add a smoke-test to CI for the nix 'default' output
I talked to somebody yesterday in #conduwuit:puppygock.gay that was using
this output in their system config. The dynamically-linked jemalloc build
is quite fragile, and is not tested by anything else in CI. We want to
make sure we don't break it again in the future.
2024-05-27 04:54:57 -04:00
Benjamin Lee 207979579c fix dynamically linked devshell
This failed to inherit the fix from bec507d739
because the crane package's buildInputs become propagatedBuildInputs in
a static stdenv, but become normal buildInputs in a dynamic stdenv. Since
we were only pulling propagatedBuildInputs into the devshell, dynamically
linked devshells did not include the rust-jemalloc-sys package. This
causes tikv-jemalloc-sys to build it's own static jemalloc package, and
we end up loading libc before jemalloc at runtime.
2024-05-27 04:54:57 -04:00
Benjamin Lee 68b96026ec unmark dynamically-linked jemalloc builds as broken
It turns out that this was actually fixed by
bec507d739 and
857ac42aac, but we didn't identify it at the
time. Notably, the `dynamic` devshell is still broken.
2024-05-27 04:54:57 -04:00
strawberry 30beb20230 conditionally static link rust-rocksdb-uwu by hot reload cfg
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry 19e7779693 update complement test results
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry 6269822613 actually fix all let_underscore_must_use lints
CI caught some more

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry 0877ee6191 allow let underscore use lint for rocksdb create cf for now
the workaround needs to be extended to rocksdb caches, but
i dont know that part of code

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry a37b2b9e64 nix: default output to scopeHostStatic instead of scopeHost
defaults to static builds instead of dynamically linked builds

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry 29fe960efa bump hyper-util and libz-sys
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry 6bf2e73830 ci: run cache dependencies in ci.yml as well
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry 630760b5da bump rocksdb to v9.2.1
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry 61e7f1e614 remove rpath = true from dev profile as the rustflags have it
needed for hot reloading but rpath being true by default
causes linker errors on lld because of the sad rpath bug

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry 7ebed7aa3e clarify disable-room message after banning room
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry ad3eeaf4c1 delete audit.toml
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry 5215fbe695 drop redaction calculated hash log to debug_info
this is normal redactions. no valid reason this needs to be
warn as it just causes confusion.

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry dc9fe657d5 fix guest accounts being logged still
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry 1c7c5bc09c feat: add /_conduwuit/local_user_count endpoint
only enabled if federation is enabled

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry 32161801ed use/enable let_underscore_must_use lint
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry 71bdcb958a fix: dont drop remote federation error on 4xx responses
for a very long time, if a remote server responded to us with
a valid but unsuccessful (HTTP 4xx) response and the caller was the
`send_federation_request` function, we may find ourselves
with a warning message only containing the destination's
server name which was very unhelpful. the true error was
buried away in trace logs. this would primarily be noticed
with server key fetch requests from us.

conduit has been throwing away the ruma request error: https://gitlab.com/famedly/conduit/-/blame/next/src/utils/error.rs#L62

before: 2024-05-23T04:45:02.930224Z  WARN router:{path=/_matrix/client/v3/publicRooms}:handle: conduit_api::client_server::directory: Failed to return our /publicRooms: matrix.org
after: 2024-05-23T05:05:02.435272Z  WARN router:{path=/_matrix/client/v3/publicRooms}:handle: conduit_api::client_server::directory: Failed to return our /publicRooms: matrix.org: [401 / M_UNAUTHORIZED] Failed to find any key to satisfy: _FetchKeyRequest(server_name='your.server.name', minimum_valid_until_ts=1716440702337, key_ids=['ed25519:RQB3XPQX'])

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry d3db0ad4e2 renovate: label PRs as dependencies and github_actions
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry e098448b9d init a few vecs in event_handler using with_capacity
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry d49507bc21 media: decomplexify get_all_media_keys for deleting all MXC URIs
wow this was terrible, early strawberry code

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry cb73ae3732 add registration token validity endpoint as per matrix 1.2
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry 06bec40591 fix: add missing fetch_required_signing_keys for remote send_leave
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry 9a7ba94ccf explicity define unstable support for sliding sync
this matrix-react-sdk PR (and the cited sliding sync MSC)
says that they will intend on checking sliding sync support
from this unstable feature flag at /versions until the CORS
header stuff is specced

https://github.com/matrix-org/matrix-react-sdk/pull/12498

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry 2990c30ac9 nix: bump rocksdb input
• Updated input 'rocksdb':
    'github:girlbossceo/rocksdb/db6df0b185774778457dabfcbd822cb81760cade' (2024-05-03)
  → 'github:girlbossceo/rocksdb/be68b3c95ccd225f3121ba33a67cfaf3c3596afc' (2024-05-23)

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry d9c575d96f bump deps
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry c32406aa0e replace deprecated config option for complement
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry 03d12cb44e update docs a tad
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry bef7dbd1cb finally error on complement diff mismatch, remove jemalloc builds from
CI

jemalloc is now a default feature

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry 08577873b4 update complement test results
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry a3931b0f1f nix: bump flake.lock
• Updated input 'crane':
    'github:ipetkov/crane/27025ab71bdca30e7ed0a16c88fd74c5970fc7f5' (2024-05-09)
  → 'github:ipetkov/crane/7443df1c478947bf96a2e699209f53b2db26209d' (2024-05-19)
• Updated input 'fenix':
    'github:nix-community/fenix/297c756ba6249d483c1dafe42378560458842173' (2024-05-10)
  → 'github:nix-community/fenix/063d7e5fac454edd35b7e2cedb6ca9fb1410c79b' (2024-05-21)
• Updated input 'fenix/rust-analyzer-src':
    'github:rust-lang/rust-analyzer/5bf2f85c8054d80424899fa581db1b192230efb5' (2024-05-09)
  → 'github:rust-lang/rust-analyzer/21ec8f523812b88418b2bfc64240c62b3dd967bd' (2024-05-19)
• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/f1010e0469db743d14519a1efd37e23f8513d714' (2024-05-09)
  → 'github:NixOS/nixpkgs/3eaeaeb6b1e08a016380c279f8846e0bd8808916' (2024-05-21)

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry ba2f22b5d3 nix: remove jemalloc (now default) targets, add jq input for default
jq input change was from https://gitlab.computer.surgery/matrix/grapevine-fork/-/commit/17eb3545906d21f2ed18f0f0f917a4638f12ef6c
to prevent unnecessary bindgen rebuilds

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry 0914aaa1b6 skip a few known flaky/unreliable complement tests
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry f3427afc7f nix: use new public keys for binary caches due to attic issues
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-24 18:13:02 -04:00
morguldir 9aa372d83b nix: Allow excluding features, allow disabling release_max_log_level 2024-05-24 15:12:23 -04:00
morguldir 5893901a75 Explicitly include snappy as well
Not sure what changed that we need this

Signed-off-by: morguldir <morguldir@protonmail.com>
2024-05-24 12:46:15 -04:00
morguldir 8ba9b33a95 Make sure we use the liburing of the platform we're building for
Signed-off-by: morguldir <morguldir@protonmail.com>
2024-05-24 12:46:15 -04:00
morguldir 70047ff26d Make rocksdb include liburing, and tell gcc the path during the build
With: strings /nix/store/9skicdac6xs4yww1nd3h7m6xydv4hxlj-rocksdb-9.1.1/lib/librocksdb.so.9|rg io_uring|wc -l
112
With: strings result/bin/conduit |rg io_uring|wc -l
5

Without: strings static-x86_64-unknown-linux-musl-jemalloc |rg io_uring | wc -l
0

Signed-off-by: morguldir <morguldir@protonmail.com>
2024-05-24 12:46:15 -04:00
Benjamin Lee 1d57e14dc0 set C/LDFLAGS for complement dependencies directly
Previously we were relying on NIX_CFLAGS_COMPILE, but this is not being
set in static devshells. A cleaner solution for complement would likely
be to build the tests in their own nix derivation instead of building
them in the devshell, but this change unblocks CI for now.
2024-05-24 10:53:47 -04:00
Benjamin Lee 5d81203277 use a statically-linked binary for complement
Dynamically-linked jemalloc is broken.
2024-05-24 10:53:47 -04:00
Benjamin Lee ad39a34c16 add a dynamically-linked devshell
This is broken on linux, but can be used by darwin users for development,
since static/jemalloc/darwin is broken.
2024-05-24 10:53:47 -04:00
Benjamin Lee a007338b34 mark dynamic jemalloc builds as broken on linux 2024-05-24 10:53:47 -04:00
Benjamin Lee 3d1507e6dd mark static rocksdb broken on darwin 2024-05-24 10:53:47 -04:00
Benjamin Lee 4cb7c0b982 don't use prefixed jemalloc with rocksdb
This is causing build failures on Mac:

> In file included from /tmp/nix-build-rocksdb-static-aarch64-apple-darwin-9.1.1.drv-0/source/memory/memory_allocator.cc:8:
> In file included from /tmp/nix-build-rocksdb-static-aarch64-apple-darwin-9.1.1.drv-0/source/memory/jemalloc_nodump_allocator.h:11:
> /tmp/nix-build-rocksdb-static-aarch64-apple-darwin-9.1.1.drv-0/source/port/jemalloc_helper.h:63:36: warning: unknown attribute '_rjem_malloc' ignored [-Wunknown-attributes]
> mallocx(size_t, int) JEMALLOC_ATTR(malloc) JEMALLOC_ALLOC_SIZE(1)
>                                    ^~~~~~
> /nix/store/3bix0kzy670dyhhizri3dwb1qfj3sdpa-jemalloc-static-aarch64-apple-darwin-5.3.0/include/jemalloc/jemalloc.h:412:18: note: expanded from macro 'malloc'
> #  define malloc je_malloc
>                  ^~~~~~~~~
> /nix/store/3bix0kzy670dyhhizri3dwb1qfj3sdpa-jemalloc-static-aarch64-apple-darwin-5.3.0/include/jemalloc/jemalloc.h:75:21: note: expanded from macro 'je_malloc'
> #  define je_malloc _rjem_malloc
>                     ^~~~~~~~~~~~
> /nix/store/3bix0kzy670dyhhizri3dwb1qfj3sdpa-jemalloc-static-aarch64-apple-darwin-5.3.0/include/jemalloc/jemalloc.h:183:43: note: expanded from macro 'JEMALLOC_ATTR'
> #  define JEMALLOC_ATTR(s) __attribute__((s))

Full build log at <https://girlboss.ceo/~strawberry/pb/ygJ3>. This is
likely fixable with patches to rocksdb, but not worth it since darwin is
only a dev platform.
2024-05-24 10:53:47 -04:00
Benjamin Lee 0c34cf95ce set show-trace for nix in CI 2024-05-24 10:53:47 -04:00
Benjamin Lee 17cc02ff99 add a 'no-features' devshell for local testing 2024-05-24 10:53:47 -04:00
Benjamin Lee c0f8253fc5 enable all-features in nix for CI builds
CI is running `cargo build --all-features`, so we should be passing all
the features to nix as well.

The only thing this currently affects is the jemalloc_prof feature, but if
we add any non-default features that affect nix in the future they should
also be handled correctly now.
2024-05-24 10:53:47 -04:00
Benjamin Lee 0fd0a5d73c switch default devshell to static linking
Dynamically-linked jemalloc doesn't work due to link-order issues, and we
want CI to be testing a static binary anyway since that's what we're
publishing in releases.
2024-05-24 10:53:47 -04:00
Benjamin Lee 4e6fc2f2df factor devshell out into a helper function
We're planning to add a second devshell with `all-features` for CI.
2024-05-24 10:53:47 -04:00
Benjamin Lee a6742ce8a7 remove liburing from devshell
This doesn't seem to be necessary to build, and the derivation is broken
in pkgsStatic.
2024-05-24 10:53:47 -04:00
Benjamin Lee 188dea13e0 do default-feature unification in nix
Some of the features affect nix dependencies, so we need to have a
full feature list available when constructing the nix derivation. This
incidentally fixes the bug where we weren't enabling jemalloc on rocksdb
in CI/devshells, because jemalloc is now a default feature. It does not
fix the more general class of that issue, where CI is performing an
`--all-features` build in a nix devshell built for default-features.

I am now passing `--no-default-features` to cargo, and having it use our
unified feature list rather than duplicating the unification inside cargo.
2024-05-24 10:53:47 -04:00
Benjamin Lee a7fe434086 only link to one jemalloc build
Without setting JEMALLOC_OVERRIDE, we end up linking to two different
jemalloc builds. Once dynamically, as a transitive dependency through
rocksdb, and a second time to the static jemalloc that tikv-jemalloc-sys
builds.
2024-05-24 10:53:47 -04:00
renovate[bot] eb8dd9cb44 chore(deps): update aquasecurity/trivy-action action to v0.21.0 2024-05-23 01:30:36 -04:00
strawberry 474d50d10c bump conduwuit version to 0.4.0
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-21 20:22:17 -04:00
Jason Volk 2e732c711c docs: Update docs for hot-reloading.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-21 20:22:17 -04:00
strawberry 981ec51ec0 docs: add initial docs for hot reload
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-21 20:22:17 -04:00
Jason Volk 2dd5cf8c68 move clap; fix version
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-21 20:22:17 -04:00
Jason Volk 74832bdc47 fix smoke from builds produced by --all-features
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-21 20:22:17 -04:00
Jason Volk fdc9a9a1b8 add cargo smoketest
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-21 20:22:17 -04:00
Jason Volk 1f3a9a40e5 lint clippy::collapsible_match (nightly)
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-21 20:22:17 -04:00
Jason Volk 362649ff87 rename src/bin to src/main
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-21 20:22:17 -04:00
strawberry 4aeec78ab4 debian: remove old symlink on postrm
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-21 20:22:17 -04:00
strawberry 9bfa89a555 adjust debian metadata, set crane workspace name
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-21 20:22:17 -04:00
Jason Volk 6c1434c165 Hot-Reloading Refactor
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-21 20:22:17 -04:00
slonkazoid ae1a4fd283 add modification time fallback if birth time is not supported on this platform 2024-05-21 16:58:30 -04:00
Benjamin Lee 9eb0784f6f don't return extra member count or e2ee device updates from sync
Previously, we were returning redundant member count updates or encrypted
device updates from the /sync endpoint in some cases. The extra member
count updates are spec-compliant, but unnecessary, while the extra
encrypted device updates violate the spec.

The refactor necessary to fix this bug is also necessary to support
filtering on state events in sync.

Details:

Joined room incremental sync needs to examine state events for four
purposes:

 1. determining whether we need to return an update to room member counts
 2. determining the set of left/joined devices for encrypted rooms
    (returned in `device_lists`)
 3. returning state events to the client (in `rooms.joined.*.state`)
 4. tracking which member events we have sent to the client, so they can
    be omitted on future requests when lazy-loading is enabled.

The state events that we need to examine for the first two cases is member
events in the delta between `since` and the end of `timeline`. For the
second two cases, we need the delta between `since` and the start of
`timeline`, plus contextual member events for any senders that occur in
`timeline`. The second list is subject to filtering, while the first is
not.

Before this change, we were using the same set of state events that we are
returning to the client (cases 3/4) to do the analysis for cases 1/2.
In a compliant implementation, this would result in us missing some
relevant member events in 1/2 in addition to seeing redundant member
events. In current conduwuit this is not the case because the set of
events that we return to the client is always a superset of the set that
is needed for cases 1/2. This is because we don't support filtering, and
we have an existing bug[1] where we are returning the delta between
`since` and the end of `timeline` rather than the start.

[1]: https://github.com/girlbossceo/conduwuit/issues/361

Fixing this is necessary to implement filtering because otherwise
we would start missing some member events for member count or encrypted
device updates if the relevant member events are rejected by the filter.
This would be much worse than our current behavior.
2024-05-20 20:55:56 -04:00
Benjamin Lee 8bffcfe82b remove sync response cache
This cache can serve invalid responses, and has an extremely low hit
rate.

It serves invalid responses because because it's only keyed off
the `since` parameter, but many of the other request parameters also
affect the response or it's side effects. This will become worse once we
implement filtering, because there will be a wider space of parameters
with different responses. This problem is fixable, but not worth it
because of the low hit rate.

The low hit rate is because normal clients will always issue the next
sync request with `since` set to the `prev_batch` value of the previous
response. The only time we expect to see multiple requests with the same
`since` is when the response is empty, but we don't cache empty
responses.

This was confirmed experimentally by logging cache hits and misses over
15 minutes with a wide variety of clients. This test was run on
matrix.computer.surgery, which has only a few active users, but a
large volume of sync traffic from many rooms. Over the test period, we
had 3 hits and 5309 misses. All hits occurred in the first minute, so I
suspect that they had something to do with client recovery from an
offline state. The clients that were connected during the test are:

 - element web
 - schildichat web
 - iamb
 - gomuks
 - nheko
 - fractal
 - fluffychat web
 - fluffychat android
 - cinny web
 - element android
 - element X android

Fixes: #336
2024-05-17 18:13:11 -04:00
241 changed files with 12136 additions and 10273 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
use flake
use flake ".#${DIRENV_DEVSHELL:-default}"
PATH_add bin
+18 -12
View File
@@ -35,6 +35,11 @@ env:
# Custom nix binary cache if fork is being used
ATTIC_ENDPOINT: ${{ vars.ATTIC_ENDPOINT }}
ATTIC_PUBLIC_KEY: ${{ vars.ATTIC_PUBLIC_KEY }}
# Use the all-features devshell instead of default, to ensure that features
# match between nix and cargo
DIRENV_DEVSHELL: all-features
# Get error output from nix that we can actually use
NIX_CONFIG: show-trace = true
permissions:
packages: write
@@ -76,7 +81,7 @@ jobs:
run: |
sudo tee -a /etc/nix/nix.conf > /dev/null <<EOF
extra-substituters = https://attic.kennel.juneis.dog/conduit https://attic.kennel.juneis.dog/conduwuit https://cache.lix.systems
extra-trusted-public-keys = conduit:Isq8FGyEC6FOXH6nD+BOeAA+bKp6X6UIbupSlGEPuOg= conduwuit:lYPVh7o1hLu1idH4Xt2QHaRa49WRGSAqzcfFd94aOTw= cache.lix.systems:aBnZUw8zA7H35Cz2RyKFVs3H4PlGTLawyY5KRbvJR8o=
extra-trusted-public-keys = conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wk= conduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTE= cache.lix.systems:aBnZUw8zA7H35Cz2RyKFVs3H4PlGTLawyY5KRbvJR8o=
EOF
- name: Use alternative Nix binary caches if specified
@@ -92,7 +97,11 @@ jobs:
echo 'source $HOME/.nix-profile/share/nix-direnv/direnvrc' > "$HOME/.direnvrc"
nix profile install --impure --inputs-from . nixpkgs#direnv nixpkgs#nix-direnv
direnv allow
nix develop --command true
nix develop .#all-features --command true
- name: Cache CI dependencies
run: |
bin/nix-build-and-cache ci
- name: Run CI tests
run: |
@@ -131,8 +140,6 @@ jobs:
if-no-files-found: error
- name: Diff Complement results with checked-in repo results
# TODO: figure out why our complement results are not 100% consistent so we don't need to allow failures
continue-on-error: true
run: |
diff -u --color=always tests/test_results/complement/test_results.jsonl complement_test_results.jsonl > >(tee -a complement_test_output.log)
@@ -163,9 +170,7 @@ jobs:
matrix:
include:
- target: aarch64-unknown-linux-musl
- target: aarch64-unknown-linux-musl-jemalloc
- target: x86_64-unknown-linux-musl
- target: x86_64-unknown-linux-musl-jemalloc
steps:
- name: Sync repository
uses: actions/checkout@v4
@@ -185,8 +190,8 @@ jobs:
- name: Apply Nix binary cache configuration
run: |
sudo tee -a /etc/nix/nix.conf > /dev/null <<EOF
extra-substituters = https://attic.kennel.juneis.dog/conduit https://attic.kennel.juneis.dog/conduwuit
extra-trusted-public-keys = conduit:Isq8FGyEC6FOXH6nD+BOeAA+bKp6X6UIbupSlGEPuOg= conduwuit:lYPVh7o1hLu1idH4Xt2QHaRa49WRGSAqzcfFd94aOTw=
extra-substituters = https://attic.kennel.juneis.dog/conduit https://attic.kennel.juneis.dog/conduwuit https://cache.lix.systems
extra-trusted-public-keys = conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wk= conduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTE= cache.lix.systems:aBnZUw8zA7H35Cz2RyKFVs3H4PlGTLawyY5KRbvJR8o=
EOF
- name: Use alternative Nix binary caches if specified
@@ -202,7 +207,7 @@ jobs:
echo 'source $HOME/.nix-profile/share/nix-direnv/direnvrc' > "$HOME/.direnvrc"
nix profile install --impure --inputs-from . nixpkgs#direnv nixpkgs#nix-direnv
direnv allow
nix develop --command true
nix develop .#all-features --command true
- name: Build static ${{ matrix.target }}
run: |
@@ -213,7 +218,8 @@ jobs:
mkdir -v -p target/$CARGO_DEB_TARGET_TUPLE/release/
cp -v -f result/bin/conduit target/release/conduwuit
cp -v -f result/bin/conduit target/$CARGO_DEB_TARGET_TUPLE/release/conduwuit
direnv exec . cargo deb --verbose --no-build --no-strip --target=$CARGO_DEB_TARGET_TUPLE --output target/release/${{ matrix.target }}.deb
# -p conduit is the main crate name
direnv exec . cargo deb --verbose --no-build --no-strip -p conduit --target=$CARGO_DEB_TARGET_TUPLE --output target/release/${{ matrix.target }}.deb
mv -v target/release/conduwuit static-${{ matrix.target }}
mv -v target/release/${{ matrix.target }}.deb ${{ matrix.target }}.deb
@@ -295,8 +301,8 @@ jobs:
- name: Move OCI images into position
run: |
mv -v oci-image-x86_64-*-jemalloc/*.tar.gz oci-image-amd64.tar.gz
mv -v oci-image-aarch64-*-jemalloc/*.tar.gz oci-image-arm64v8.tar.gz
mv -v oci-image-x86_64-*/*.tar.gz oci-image-amd64.tar.gz
mv -v oci-image-aarch64-*/*.tar.gz oci-image-arm64v8.tar.gz
- name: Load and push amd64 image
if: ${{ (vars.DOCKER_USERNAME != '') && (env.DOCKERHUB_TOKEN != '') }}
+2 -2
View File
@@ -61,9 +61,9 @@ jobs:
extra-substituters = https://crane.cachix.org
extra-trusted-public-keys = crane.cachix.org-1:8Scfpmn9w+hGdXH/Q9tTLiYAE/2dnJYRJP7kl80GuRk=
extra-substituters = https://attic.kennel.juneis.dog/conduit
extra-trusted-public-keys = conduit:Isq8FGyEC6FOXH6nD+BOeAA+bKp6X6UIbupSlGEPuOg=
extra-trusted-public-keys = conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wk=
extra-substituters = https://attic.kennel.juneis.dog/conduwuit
extra-trusted-public-keys = conduwuit:lYPVh7o1hLu1idH4Xt2QHaRa49WRGSAqzcfFd94aOTw=
extra-trusted-public-keys = conduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTE=
- name: Add alternative Nix binary caches if specified
if: ${{ (env.ATTIC_ENDPOINT != '') && (env.ATTIC_PUBLIC_KEY != '') }}
+2 -2
View File
@@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@v4
- name: Run Trivy code and vulnerability scanner on repo
uses: aquasecurity/trivy-action@0.20.0
uses: aquasecurity/trivy-action@0.21.0
with:
scan-type: repo
format: sarif
@@ -34,7 +34,7 @@ jobs:
severity: CRITICAL,HIGH,MEDIUM,LOW
- name: Run Trivy code and vulnerability scanner on filesystem
uses: aquasecurity/trivy-action@0.20.0
uses: aquasecurity/trivy-action@0.21.0
with:
scan-type: fs
format: sarif
+2 -2
View File
@@ -26,10 +26,10 @@ before_script:
# Add conduwuit binary cache
- if command -v nix > /dev/null; then echo "extra-substituters = https://attic.kennel.juneis.dog/conduwuit" >> /etc/nix/nix.conf; fi
- if command -v nix > /dev/null; then echo "extra-trusted-public-keys = conduwuit:lYPVh7o1hLu1idH4Xt2QHaRa49WRGSAqzcfFd94aOTw=" >> /etc/nix/nix.conf; fi
- if command -v nix > /dev/null; then echo "extra-trusted-public-keys = conduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTE=" >> /etc/nix/nix.conf; fi
- if command -v nix > /dev/null; then echo "extra-substituters = https://attic.kennel.juneis.dog/conduit" >> /etc/nix/nix.conf; fi
- if command -v nix > /dev/null; then echo "extra-trusted-public-keys = conduit:Isq8FGyEC6FOXH6nD+BOeAA+bKp6X6UIbupSlGEPuOg=" >> /etc/nix/nix.conf; fi
- if command -v nix > /dev/null; then echo "extra-trusted-public-keys = conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wk=" >> /etc/nix/nix.conf; fi
# Add alternate binary cache
- if command -v nix > /dev/null && [ -n "$ATTIC_ENDPOINT" ]; then echo "extra-substituters = $ATTIC_ENDPOINT" >> /etc/nix/nix.conf; fi
Generated
+317 -302
View File
File diff suppressed because it is too large Load Diff
+470 -350
View File
@@ -1,115 +1,94 @@
[package]
# TODO: when can we rename to conduwuit?
name = "conduit"
#cargo-features = ["profile-rustflags"]
[workspace]
resolver = "2"
members = ["src/*"]
default-members = ["src/*"]
[workspace.package]
description = "a very cool fork of Conduit, a Matrix homeserver written in Rust"
license = "Apache-2.0"
authors = [
"strawberry <strawberry@puppygock.gay>",
"timokoesters <timo@koesters.xyz>",
]
version = "0.4.1"
edition = "2021"
# See also `rust-toolchain.toml`
rust-version = "1.77.0"
homepage = "https://conduwuit.puppyirl.gay/"
repository = "https://github.com/girlbossceo/conduwuit"
readme = "README.md"
version = "0.3.4"
edition = "2021"
# See also `rust-toolchain.toml`
rust-version = "1.77.0"
[workspace.metadata.crane]
name = "conduit"
[dependencies]
# 1.1.17 seems broken on nix from a permission error?
libz-sys = "=1.1.16"
[workspace.dependencies.sanitize-filename]
version = "0.5.0"
console-subscriber = { version = "0.2", optional = true }
[workspace.dependencies.infer]
version = "0.15"
default-features = false
infer = { version = "0.15", default-features = false }
[workspace.dependencies.jsonwebtoken]
version = "9.3.0"
# for hot lib reload
hot-lib-reloader = { version = "^0.7", optional = true }
# Used for secure identifiers
rand = "0.8.5"
# Used for conduit::Error type
thiserror = "1.0.60"
# Used to encode server public key
base64 = "0.22.1"
# Used when hashing the state
ring = "0.17.8"
# Used to find matching events for appservices
regex = "1.10.4"
# Used to load forbidden room/user regex from config
serde_regex = "1.1.0"
# Used to make working with iterators easier, was already a transitive depdendency
itertools = "0.12.1"
# jwt jsonwebtokens
jsonwebtoken = "9.3.0"
# Used for ruma wrapper
serde_html_form = "0.2.6"
[workspace.dependencies.base64]
version = "0.22.1"
# used for TURN server authentication
hmac = "0.12.1"
sha-1 = "0.10.1"
[workspace.dependencies.hmac]
version = "0.12.1"
[workspace.dependencies.sha-1]
version = "0.10.1"
# used for checking if an IP is in specific subnets / CIDR ranges easier
ipaddress = "0.1.3"
[workspace.dependencies.ipaddress]
version = "0.1.3"
# to get the client IP address of requests
#axum-client-ip = "0.4.2"
[workspace.dependencies.rand]
version = "0.8.5"
# to parse user-friendly time durations in admin commands
cyborgtime = "2.1.1"
# all the web/HTTP dependencies
# Used for the http request / response body type for Ruma endpoints used with reqwest
bytes = "1.6.0"
http = "1.1.0"
http-body-util = "0.1.1"
[workspace.dependencies.bytes]
version = "1.6.0"
# used to replace the channels of the tokio runtime
loole = "0.3.0"
[workspace.dependencies.http-body-util]
version = "0.1.1"
# Validating urls in config, was already a transitive dependency
url = { version = "2.5.0", features = ["serde"] }
[workspace.dependencies.http]
version = "1.1.0"
async-trait = "0.1.80"
[workspace.dependencies.regex]
version = "1.10.4"
lru-cache = "0.1.2"
sanitize-filename = "0.5.0"
# standard date and time tools
[dependencies.chrono]
version = "0.4.38"
features = ["alloc"]
default-features = false
# Web framework
[dependencies.axum]
[workspace.dependencies.axum]
version = "0.7.5"
default-features = false
features = ["form", "http1", "http2", "json", "matched-path"]
features = [
"form",
"http1",
"http2",
"json",
"matched-path",
"tokio",
]
[dependencies.axum-extra]
[workspace.dependencies.axum-extra]
version = "0.9.3"
default-features = false
features = ["typed-header"]
[dependencies.axum-server]
[workspace.dependencies.axum-server]
version = "0.6.0"
features = ["tls-rustls"]
[dependencies.tower]
[workspace.dependencies.tower]
version = "0.4.13"
features = ["util"]
[dependencies.tower-http]
[workspace.dependencies.tower-http]
version = "0.5.2"
features = [
"add-extension",
@@ -121,153 +100,168 @@ features = [
"catch-panic",
]
[dependencies.hyper]
version = "1.3.1"
features = ["server", "http1", "http2"]
[dependencies.hyper-util]
version = "0.1.3"
[dependencies.reqwest]
[workspace.dependencies.reqwest]
version = "0.12.4"
default-features = false
features = ["rustls-tls-native-roots", "socks", "hickory-dns"]
features = [
"rustls-tls-native-roots",
"socks",
"hickory-dns",
"http2",
]
# all the serde stuff
# Used for pdu definition
[dependencies.serde]
[workspace.dependencies.serde]
version = "1.0.201"
features = ["rc"]
# Used for appservice registration files
[dependencies.serde_yaml]
version = "0.9.34"
# Used for ruma wrapper
[dependencies.serde_json]
[workspace.dependencies.serde_json]
version = "1.0.117"
features = ["raw_value"]
# Used for appservice registration files
[workspace.dependencies.serde_yaml]
version = "0.9.34"
# Used to load forbidden room/user regex from config
[workspace.dependencies.serde_regex]
version = "1.1.0"
# Used for ruma wrapper
[workspace.dependencies.serde_html_form]
version = "0.2.6"
# Used for password hashing
[dependencies.argon2]
[workspace.dependencies.argon2]
version = "0.5.3"
features = ["alloc", "rand"]
default-features = false
# Used to generate thumbnails for images
[dependencies.image]
[workspace.dependencies.image]
version = "0.25.1"
default-features = false
features = ["jpeg", "png", "gif", "webp"]
features = [
"jpeg",
"png",
"gif",
"webp",
]
# logging
[dependencies.log]
[workspace.dependencies.log]
version = "0.4.21"
default-features = false
[dependencies.tracing]
[workspace.dependencies.tracing]
version = "0.1.40"
default-features = false
[dependencies.tracing-subscriber]
[workspace.dependencies.tracing-subscriber]
version = "0.3.18"
features = ["env-filter"]
# optional SHA256 media keys feature
[dependencies.sha2]
version = "0.10.8"
optional = true
# optional opentelemetry, performance measurements, flamegraphs, etc for performance measurements and monitoring
[dependencies.opentelemetry]
version = "0.21.0"
optional = true
[dependencies.tracing-flame]
version = "0.2.0"
optional = true
[dependencies.tracing-opentelemetry]
version = "0.22.0"
optional = true
[dependencies.opentelemetry_sdk]
version = "0.21.2"
optional = true
features = ["rt-tokio"]
[dependencies.opentelemetry-jaeger]
version = "0.20.0"
optional = true
features = ["rt-tokio"]
# optional sentry metrics for crash/panic reporting
[dependencies.sentry]
version = "0.32.3"
optional = true
default-features = false
features = [
"backtrace",
"contexts",
"debug-images",
"panic",
"rustls",
"tower",
"tower-http",
"tracing",
"reqwest",
"log",
]
[dependencies.sentry-tracing]
version = "0.32.3"
optional = true
[dependencies.sentry-tower]
version = "0.32.3"
optional = true
# optional jemalloc usage
[dependencies.tikv-jemalloc-sys]
version = "0.5.4"
optional = true
default-features = false
features = ["stats", "unprefixed_malloc_on_supported_platforms"]
[dependencies.tikv-jemallocator]
version = "0.5.4"
optional = true
default-features = false
features = ["stats", "unprefixed_malloc_on_supported_platforms"]
[dependencies.tikv-jemalloc-ctl]
version = "0.5.4"
optional = true
default-features = false
features = ["use_std"]
# for URL previews
[dependencies.webpage]
[workspace.dependencies.webpage]
version = "2.0.1"
default-features = false
# to support multiple variations of setting a config option
[dependencies.either]
version = "1.11.0"
features = ["serde"]
# to listen on both HTTP and HTTPS if listening on TLS dierctly from conduwuit for complement or sytest
[dependencies.axum-server-dual-protocol]
version = "0.6"
optional = true
# used for conduit's CLI and admin room command parsing
[dependencies.clap]
[workspace.dependencies.clap]
version = "4.5.4"
default-features = false
features = ["std", "derive", "help", "usage", "error-context", "string"]
features = [
"std",
"derive",
"help",
"usage",
"error-context",
"string",
]
[dependencies.futures-util]
[workspace.dependencies.futures-util]
version = "0.3.30"
default-features = false
[workspace.dependencies.tokio]
version = "1.37.0"
features = [
"fs",
"net",
"macros",
"sync",
"signal",
"time",
"rt-multi-thread",
"io-util",
]
[workspace.dependencies.libloading]
version = "0.8.3"
# Validating urls in config, was already a transitive dependency
[workspace.dependencies.url]
version = "2.5.0"
features = ["serde"]
# standard date and time tools
[workspace.dependencies.chrono]
version = "0.4.38"
features = ["alloc"]
default-features = false
[workspace.dependencies.hyper]
version = "1.3.1"
features = [
"server",
"http1",
"http2",
]
[workspace.dependencies.hyper-util]
version = "0.1.4"
# to support multiple variations of setting a config option
[workspace.dependencies.either]
version = "1.11.0"
features = ["serde"]
# Used for reading the configuration from conduwuit.toml & environment variables
[dependencies.figment]
[workspace.dependencies.figment]
version = "0.10.18"
features = ["env", "toml"]
[workspace.dependencies.hickory-resolver]
version = "0.24.1"
default-features = false
# Used for conduit::Error type
[workspace.dependencies.thiserror]
version = "1.0.61"
# Used when hashing the state
[workspace.dependencies.ring]
version = "0.17.8"
# Used to make working with iterators easier, was already a transitive depdendency
[workspace.dependencies.itertools]
version = "0.13.0"
# to parse user-friendly time durations in admin commands
#TODO: overlaps chrono?
[workspace.dependencies.cyborgtime]
version = "2.1.1"
# used to replace the channels of the tokio runtime
[workspace.dependencies.loole]
version = "0.3.0"
[workspace.dependencies.async-trait]
version = "0.1.80"
[workspace.dependencies.lru-cache]
version = "0.1.2"
# Used for matrix spec type definitions and helpers
[dependencies.ruma]
git = "https://github.com/girlbossceo/ruma"
[workspace.dependencies.ruma]
git = "https://github.com/girlbossceo/ruwuma"
branch = "conduwuit-changes"
features = [
"compat",
@@ -292,60 +286,128 @@ features = [
"unstable-extensible-events",
]
[dependencies.ruma-identifiers-validation]
git = "https://github.com/girlbossceo/ruma"
[workspace.dependencies.ruma-identifiers-validation]
git = "https://github.com/girlbossceo/ruwuma"
branch = "conduwuit-changes"
[dependencies.hickory-resolver]
version = "0.24.1"
[workspace.dependencies.rust-rocksdb]
path = "deps/rust-rocksdb"
package = "rust-rocksdb-uwu"
features = [
"multi-threaded-cf",
"mt_static",
"snappy",
"lz4",
"zstd",
"zlib",
"bzip2",
]
[workspace.dependencies.zstd]
version = "0.13.1"
# to listen on both HTTP and HTTPS if listening on TLS dierctly from conduwuit for complement or sytest
[workspace.dependencies.axum-server-dual-protocol]
version = "0.6"
# optional SHA256 media keys feature
[workspace.dependencies.sha2]
version = "0.10.8"
# optional opentelemetry, performance measurements, flamegraphs, etc for performance measurements and monitoring
[workspace.dependencies.opentelemetry]
version = "0.21.0"
[workspace.dependencies.tracing-flame]
version = "0.2.0"
[workspace.dependencies.tracing-opentelemetry]
version = "0.22.0"
[workspace.dependencies.opentelemetry_sdk]
version = "0.21.2"
features = ["rt-tokio"]
[workspace.dependencies.opentelemetry-jaeger]
version = "0.20.0"
features = ["rt-tokio"]
# optional sentry metrics for crash/panic reporting
[workspace.dependencies.sentry]
version = "0.32.3"
default-features = false
features = [
"backtrace",
"contexts",
"debug-images",
"panic",
"rustls",
"tower",
"tower-http",
"tracing",
"reqwest",
"log",
]
[dependencies.rust-rocksdb]
git = "https://github.com/zaidoon1/rust-rocksdb"
branch = "master"
optional = true
default-features = true
features = ["multi-threaded-cf", "zstd"]
[workspace.dependencies.sentry-tracing]
version = "0.32.3"
[workspace.dependencies.sentry-tower]
version = "0.32.3"
[dependencies.rusqlite]
# jemalloc usage
[workspace.dependencies.tikv-jemalloc-sys]
version = "0.5.4"
default-features = false
features = ["stats", "unprefixed_malloc_on_supported_platforms"]
[workspace.dependencies.tikv-jemallocator]
version = "0.5.4"
default-features = false
features = ["stats", "unprefixed_malloc_on_supported_platforms"]
[workspace.dependencies.tikv-jemalloc-ctl]
version = "0.5.4"
default-features = false
features = ["use_std"]
[workspace.dependencies.rusqlite]
git = "https://github.com/rusqlite/rusqlite"
#branch = "master"
rev = "e00b626e2b1c67347d789fb7f600281705c89381"
optional = true
features = ["bundled"]
# used only by rusqlite
[dependencies.parking_lot]
[workspace.dependencies.parking_lot]
version = "0.12.2"
optional = true
# used only by rusqlite
[dependencies.thread_local]
[workspace.dependencies.thread_local]
version = "1.1.8"
optional = true
# used only by rusqlite and rust-rocksdb
[dependencies.num_cpus]
version = "1.16.0"
[workspace.dependencies.tokio-metrics]
version = "0.3.1"
default-features = false
[dependencies.tokio]
version = "1.37.0"
features = ["fs", "macros", "sync", "signal"]
[workspace.dependencies.console-subscriber]
version = "0.2"
# *nix-specific dependencies
[target.'cfg(unix)'.dependencies]
nix = { version = "0.28.0", features = ["resource"] }
sd-notify = { version = "0.4.1", optional = true } # systemd is only available/relevant on *nix platforms
[workspace.dependencies.nix]
version = "0.28.0"
features = ["resource"]
[workspace.dependencies.sd-notify]
version = "0.4.1"
[target.'cfg(all(not(target_env = "msvc"), target_os = "linux"))'.dependencies]
hardened_malloc-rs = { version = "0.1.2", optional = true, features = [
"static",
"gcc",
"light",
], default-features = false }
#hardened_malloc-rs = { optional = true, features = ["static","clang","light"], path = "../hardened_malloc-rs", default-features = false }
[workspace.dependencies.hardened_malloc-rs]
version = "0.1.2"
default-features = false
features = [
"static",
"gcc",
"light",
]
#
# Patches
#
# backport of [https://github.com/tokio-rs/tracing/pull/2956] to the 0.1.x branch of tracing.
# we can switch back to upstream if #2956 is merged and backported in the upstream repo.
@@ -359,166 +421,219 @@ branch = "tracing-subscriber/env-filter-clone-0.1.x-backport"
git = "https://github.com/girlbossceo/tracing"
branch = "tracing-subscriber/env-filter-clone-0.1.x-backport"
[features]
default = [
"backend_rocksdb",
"systemd",
"element_hacks",
"sentry_telemetry",
"gzip_compression",
"brotli_compression",
"zstd_compression",
"release_max_log_level",
"io_uring",
]
backend_sqlite = ["sqlite"]
backend_rocksdb = ["rocksdb"]
rocksdb = ["dep:rust-rocksdb"]
jemalloc = [
"dep:tikv-jemalloc-sys",
"dep:tikv-jemalloc-ctl",
"dep:tikv-jemallocator",
"rust-rocksdb/jemalloc",
]
jemalloc_prof = ["tikv-jemalloc-sys/profiling"]
sqlite = ["dep:rusqlite", "dep:parking_lot", "dep:thread_local"]
systemd = ["dep:sd-notify"]
sentry_telemetry = ["dep:sentry", "dep:sentry-tracing", "dep:sentry-tower"]
gzip_compression = ["tower-http/compression-gzip", "reqwest/gzip"]
zstd_compression = ["tower-http/compression-zstd"]
brotli_compression = ["tower-http/compression-br", "reqwest/brotli"]
sha256_media = ["dep:sha2"]
io_uring = ["rust-rocksdb/io-uring"]
axum_dual_protocol = ["dep:axum-server-dual-protocol"]
perf_measurements = [
"dep:opentelemetry",
"dep:tracing-flame",
"dep:tracing-opentelemetry",
"dep:opentelemetry_sdk",
"dep:opentelemetry-jaeger",
]
# enable the tokio_console server
# incompatible with release_max_log_level
tokio_console = ["dep:console-subscriber", "tokio/tracing"]
hot_reload = ["dep:hot-lib-reloader"]
hardened_malloc = ["dep:hardened_malloc-rs"]
# increases performance, reduces build times, and reduces binary size by not compiling or
# genreating code for log level filters that users will generally not use (debug and trace) only in release builds
#
# the expense is obviously losing those log level filters for usage at runtime. debug builds will still have all log levels
release_max_log_level = [
"tracing/max_level_trace",
"tracing/release_max_level_info",
"log/max_level_trace",
"log/release_max_level_info",
]
# developer feature useful only in debug builds.
dev_release_log_level = []
# client/server interopability hacks
# Our crates
#
## element has various non-spec compliant behaviour
element_hacks = []
[workspace.dependencies.conduit-router]
package = "conduit_router"
path = "src/router"
default-features = false
[package.metadata.deb]
name = "conduwuit"
maintainer = "strawberry <strawberry@puppygock.gay>"
copyright = "2024, strawberry <strawberry@puppygock.gay>"
license-file = ["LICENSE", "3"]
depends = "$auto, ca-certificates"
extended-description = """\
a cool hard fork of Conduit, a Matrix homeserver written in Rust"""
section = "net"
priority = "optional"
assets = [
[
"debian/README.md",
"usr/share/doc/conduwuit/README.Debian",
"644",
],
[
"README.md",
"usr/share/doc/conduwuit/",
"644",
],
[
"target/release/conduwuit",
"usr/sbin/conduwuit",
"755",
],
[
"conduwuit-example.toml",
"etc/conduwuit/conduwuit.toml",
"640",
],
]
conf-files = ["/etc/conduwuit/conduwuit.toml"]
maintainer-scripts = "debian/"
systemd-units = { unit-name = "conduwuit", start = false }
[workspace.dependencies.conduit-admin]
package = "conduit_admin"
path = "src/admin"
default-features = false
[workspace.dependencies.conduit-api]
package = "conduit_api"
path = "src/api"
default-features = false
[profile.dev]
#debug = 0
lto = 'off'
codegen-units = 512
incremental = true
overflow-checks = true
#panic = "abort"
[workspace.dependencies.conduit-service]
package = "conduit_service"
path = "src/service"
default-features = false
# seems to speed up continuous debug compilations
[profile.dev.build-override]
opt-level = 3
[profile.dev.package."*"] # external dependencies
opt-level = 1
[profile.dev.package."tokio"]
opt-level = 3
[workspace.dependencies.conduit-database]
package = "conduit_database"
path = "src/database"
default-features = false
[workspace.dependencies.conduit-core]
package = "conduit_core"
path = "src/core"
default-features = false
###############################################################################
#
# Release profiles
#
# default release profile
[profile.release]
lto = 'thin'
incremental = false
opt-level = 3
strip = "symbols"
control-flow-guard = true # Windows only
debug = 0
lto = "thin"
# release profile with debug symbols
[profile.release-debuginfo]
inherits = "release"
strip = "none"
debug = "full"
strip = "none"
# high performance release profile which uses fat LTO across all crates, 1 codegen unit, max opt-level, and optimises across all crates
[profile.release-high-perf]
inherits = "release"
lto = 'fat'
lto = "fat"
codegen-units = 1
panic = "abort"
# For releases also try to max optimizations for dependencies:
[profile.release-high-perf.build-override]
debug = 0
opt-level = 3
# do not use without profile-rustflags enabled
[profile.release-max-perf]
inherits = "release"
strip = "symbols"
lto = "fat"
#rustflags = [
# '-Ctarget-cpu=native',
# '-Ztune-cpu=native',
# '-Ctarget-feature=+crt-static',
# '-Crelocation-model=static',
# '-Ztls-model=local-exec',
# '-Zinline-in-all-cgus=true',
# '-Zinline-mir=true',
# '-Zmir-opt-level=3',
# '-Clink-arg=-fuse-ld=gold',
# '-Clink-arg=-Wl,--threads',
# '-Clink-arg=-Wl,--gc-sections',
# '-Clink-arg=-luring',
# '-Clink-arg=-lstdc++',
# '-Clink-arg=-lc',
# '-Ztime-passes',
# '-Ztime-llvm-passes',
#]
[profile.release-max-perf.build-override]
inherits = "release-max-perf"
opt-level = 0
#rustflags = [
# '-Ctarget-feature=-crt-static',
#]
[profile.bench]
inherits = "release"
#rustflags = [
# "-Cremark=all",
# '-Ztime-passes',
# '-Ztime-llvm-passes',
#]
###############################################################################
#
# Developer profile
#
# To enable hot-reloading:
# 1. Uncomment all of the rustflags here.
# 2. Uncomment crate-type=dylib in src/*/Cargo.toml and deps/rust-rocksdb/Cargo.toml
#
# opt-level, mir-opt-level, validate-mir are not known to interfere with reloading
# and can be raised if build times are tolerable.
[profile.dev]
debug = 1
opt-level = 0
panic = "unwind"
debug-assertions = true
incremental = true
codegen-units = 64
#rustflags = [
# '--cfg', 'conduit_mods',
# '-Ztime-passes',
# '-Zmir-opt-level=0',
# '-Zvalidate-mir=false',
# '-Ztls-model=global-dynamic',
# '-Cprefer-dynamic=true',
# '-Zstaticlib-prefer-dynamic=true',
# '-Zstaticlib-allow-rdylib-deps=true',
# '-Zpacked-bundled-libs=false',
# '-Zplt=true',
# '-Crpath=true',
# '-Clink-arg=-Wl,--as-needed',
# '-Clink-arg=-Wl,--allow-shlib-undefined',
# '-Clink-arg=-Wl,-z,keep-text-section-prefix',
# '-Clink-arg=-Wl,-z,lazy',
#]
[profile.dev.package.conduit_core]
inherits = "dev"
incremental = false
#rustflags = [
# '--cfg', 'conduit_mods',
# '-Ztime-passes',
# '-Zmir-opt-level=0',
# '-Ztls-model=initial-exec',
# '-Cprefer-dynamic=true',
# '-Zstaticlib-prefer-dynamic=true',
# '-Zstaticlib-allow-rdylib-deps=true',
# '-Zpacked-bundled-libs=false',
# '-Zplt=true',
# '-Clink-arg=-Wl,--as-needed',
# '-Clink-arg=-Wl,--allow-shlib-undefined',
# '-Clink-arg=-Wl,-z,lazy',
# '-Clink-arg=-Wl,-z,unique',
# '-Clink-arg=-Wl,-z,nodlopen',
# '-Clink-arg=-Wl,-z,nodelete',
#]
[profile.dev.package.conduit]
inherits = "dev"
incremental = false
#rustflags = [
# '--cfg', 'conduit_mods',
# '-Ztime-passes',
# '-Zmir-opt-level=0',
# '-Zvalidate-mir=false',
# '-Ztls-model=global-dynamic',
# '-Cprefer-dynamic=true',
# '-Zexport-executable-symbols=true',
# '-Zplt=true',
# '-Crpath=true',
# '-Clink-arg=-Wl,--as-needed',
# '-Clink-arg=-Wl,--allow-shlib-undefined',
# '-Clink-arg=-Wl,--export-dynamic',
# '-Clink-arg=-Wl,-z,lazy',
#]
[profile.dev.package.rust-rocksdb-uwu]
inherits = "dev"
debug = 'limited'
incremental = false
codegen-units = 1
opt-level = 'z'
#rustflags = [
# '--cfg', 'conduit_mods',
# '-Ztls-model=initial-exec',
# '-Cprefer-dynamic=true',
# '-Zstaticlib-prefer-dynamic=true',
# '-Zstaticlib-allow-rdylib-deps=true',
# '-Zpacked-bundled-libs=true',
# '-Zplt=true',
# '-Clink-arg=-Wl,--no-as-needed',
# '-Clink-arg=-Wl,--allow-shlib-undefined',
# '-Clink-arg=-Wl,-z,lazy',
# '-Clink-arg=-Wl,-z,nodlopen',
# '-Clink-arg=-Wl,-z,nodelete',
#]
[profile.release-high-perf.package."*"]
debug = 0
opt-level = 3
[profile.dev.package.'*']
inherits = "dev"
debug = 'limited'
incremental = false
codegen-units = 1
opt-level = 'z'
#rustflags = [
# '--cfg', 'conduit_mods',
# '-Ztls-model=global-dynamic',
# '-Cprefer-dynamic=true',
# '-Zstaticlib-prefer-dynamic=true',
# '-Zstaticlib-allow-rdylib-deps=true',
# '-Zpacked-bundled-libs=true',
# '-Zplt=true',
# '-Clink-arg=-Wl,--as-needed',
# '-Clink-arg=-Wl,-z,lazy',
# '-Clink-arg=-Wl,-z,nodelete',
#]
[lints]
workspace = true
[profile.test]
incremental = false
[workspace.lints.rust]
missing_abi = "warn"
@@ -540,10 +655,14 @@ unreachable_pub = "warn"
# this seems to suggest broken code and is not working correctly
unused_braces = "allow"
# cfgs cannot be limited to features or cargo build --all-features panics for unsuspecting users.
# cfgs cannot be limited to expected cfgs or their de facto non-transitive/opt-in use-case e.g.
# tokio_unstable will warn.
unexpected_cfgs = "allow"
# some sadness
missing_docs = "allow"
[workspace.lints.clippy]
# pedantic = "warn"
@@ -615,7 +734,6 @@ unnecessary_box_returns = "warn"
map_unwrap_or = "warn"
implicit_clone = "warn"
match_wildcard_for_single_variants = "warn"
unnecessary_wraps = "warn"
match_same_arms = "warn"
ignored_unit_patterns = "warn"
redundant_else = "warn"
@@ -632,6 +750,7 @@ manual_let_else = "warn"
trivially_copy_pass_by_ref = "warn"
wildcard_imports = "warn"
checked_conversions = "warn"
let_underscore_must_use = "warn"
#integer_arithmetic = "warn"
#as_conversions = "warn"
@@ -649,7 +768,7 @@ mod_module_files = "allow"
unwrap_used = "allow"
expect_used = "allow"
if_then_some_else_none = "allow"
let_underscore_must_use = "allow"
let_underscore_future = "allow"
map_err_ignore = "allow"
missing_docs_in_private_items = "allow"
multiple_inherent_impl = "allow"
@@ -657,3 +776,4 @@ error_impl_error = "allow"
string_add = "allow"
string_slice = "allow"
ref_patterns = "allow"
unnecessary_wraps = "allow"
-2
View File
@@ -1,2 +0,0 @@
[advisories]
ignore = ["RUSTSEC-2020-0016"]
+8 -2
View File
@@ -17,11 +17,17 @@ RESULTS_FILE="$3"
OCI_IMAGE="complement-conduit:main"
# Complement tests that are skipped due to flakiness/reliability issues (likely
# Complement itself induced based on various open issues)
#
# According to Go docs, these are separated by forward slashes and not pipes (why)
SKIPPED_COMPLEMENT_TESTS='-skip=TestJumpToDateEndpoint.*|TestJoinFederatedRoomFromApplicationServiceBridgeUser.*|TestFederationRoomsInvite.*|TestClientSpacesSummary.*'
toplevel="$(git rev-parse --show-toplevel)"
pushd "$toplevel" > /dev/null
bin/nix-build-and-cache just .#complement
bin/nix-build-and-cache just .#static-complement
docker load < result
popd > /dev/null
@@ -31,7 +37,7 @@ set +o pipefail
env \
-C "$COMPLEMENT_SRC" \
COMPLEMENT_BASE_IMAGE="$OCI_IMAGE" \
go test -tags="conduwuit_blacklist" -v -timeout 1h -json ./tests | tee "$LOG_FILE"
go test -tags="conduwuit_blacklist" "$SKIPPED_COMPLEMENT_TESTS" -v -timeout 1h -json ./tests | tee "$LOG_FILE"
set -o pipefail
# Post-process the results into an easy-to-compare format, sorted by Test name for reproducible results
+1 -1
View File
@@ -70,7 +70,7 @@ ci() {
--inputs-from "$toplevel"
# Keep sorted
"$toplevel#devShells.x86_64-linux.default"
"$toplevel#devShells.x86_64-linux.all-features"
attic#default
nixpkgs#direnv
nixpkgs#jq
+5
View File
@@ -5,6 +5,7 @@ set -e
CONDUWUIT_CONFIG_PATH=/etc/conduwuit
CONDUWUIT_DATABASE_PATH=/var/lib/conduwuit
CONDUWUIT_DATABASE_PATH_SYMLINK=/var/lib/matrix-conduit
case $1 in
purge)
@@ -21,6 +22,10 @@ case $1 in
if [ -d "$CONDUWUIT_DATABASE_PATH" ]; then
rm -v -r "$CONDUWUIT_DATABASE_PATH"
fi
if [ -d "$CONDUWUIT_DATABASE_PATH_SYMLINK" ]; then
rm -v -r "$CONDUWUIT_DATABASE_PATH_SYMLINK"
fi
;;
esac
+35
View File
@@ -0,0 +1,35 @@
[package]
name = "rust-rocksdb-uwu"
version = "0.0.1"
edition = "2021"
[features]
default = ["snappy", "lz4", "zstd", "zlib", "bzip2"]
jemalloc = ["rust-rocksdb/jemalloc"]
io-uring = ["rust-rocksdb/io-uring"]
valgrind = ["rust-rocksdb/valgrind"]
snappy = ["rust-rocksdb/snappy"]
lz4 = ["rust-rocksdb/lz4"]
zstd = ["rust-rocksdb/zstd"]
zlib = ["rust-rocksdb/zlib"]
bzip2 = ["rust-rocksdb/bzip2"]
rtti = ["rust-rocksdb/rtti"]
mt_static = ["rust-rocksdb/mt_static"]
multi-threaded-cf = ["rust-rocksdb/multi-threaded-cf"]
serde1 = ["rust-rocksdb/serde1"]
malloc-usable-size = ["rust-rocksdb/malloc-usable-size"]
[dependencies.rust-rocksdb]
git = "https://github.com/zaidoon1/rust-rocksdb"
branch = "master"
default-features = false
[lib]
path = "lib.rs"
crate-type = [
"rlib",
# "dylib"
]
[lints]
workspace = true
+61
View File
@@ -0,0 +1,61 @@
pub use rust_rocksdb::*;
#[cfg_attr(not(conduit_mods), link(name = "rocksdb"))]
#[cfg_attr(conduit_mods, link(name = "rocksdb", kind = "static"))]
extern "C" {
pub fn rocksdb_list_column_families();
pub fn rocksdb_logger_create_stderr_logger();
pub fn rocksdb_options_set_info_log();
pub fn rocksdb_get_options_from_string();
pub fn rocksdb_writebatch_create();
pub fn rocksdb_writebatch_destroy();
pub fn rocksdb_writebatch_put_cf();
pub fn rocksdb_writebatch_delete_cf();
pub fn rocksdb_iter_value();
pub fn rocksdb_iter_seek_to_last();
pub fn rocksdb_iter_seek_for_prev();
pub fn rocksdb_iter_seek_to_first();
pub fn rocksdb_iter_next();
pub fn rocksdb_iter_prev();
pub fn rocksdb_iter_seek();
pub fn rocksdb_iter_valid();
pub fn rocksdb_iter_get_error();
pub fn rocksdb_iter_key();
pub fn rocksdb_iter_destroy();
pub fn rocksdb_livefiles();
pub fn rocksdb_livefiles_count();
pub fn rocksdb_livefiles_destroy();
pub fn rocksdb_livefiles_column_family_name();
pub fn rocksdb_livefiles_name();
pub fn rocksdb_livefiles_size();
pub fn rocksdb_livefiles_level();
pub fn rocksdb_livefiles_smallestkey();
pub fn rocksdb_livefiles_largestkey();
pub fn rocksdb_livefiles_entries();
pub fn rocksdb_livefiles_deletions();
pub fn rocksdb_put_cf();
pub fn rocksdb_delete_cf();
pub fn rocksdb_get_pinned_cf();
pub fn rocksdb_create_column_family();
pub fn rocksdb_get_latest_sequence_number();
pub fn rocksdb_batched_multi_get_cf();
pub fn rocksdb_cancel_all_background_work();
pub fn rocksdb_repair_db();
pub fn rocksdb_list_column_families_destroy();
pub fn rocksdb_flush();
pub fn rocksdb_flush_wal();
pub fn rocksdb_open_column_families();
pub fn rocksdb_open_for_read_only_column_families();
pub fn rocksdb_open_as_secondary_column_families();
pub fn rocksdb_open_column_families_with_ttl();
pub fn rocksdb_open();
pub fn rocksdb_open_for_read_only();
pub fn rocksdb_open_with_ttl();
pub fn rocksdb_open_as_secondary();
pub fn rocksdb_write();
pub fn rocksdb_create_iterator_cf();
pub fn rocksdb_backup_engine_create_new_backup_flush();
pub fn rocksdb_backup_engine_options_create();
pub fn rocksdb_write_buffer_manager_destroy();
pub fn rocksdb_options_set_ttl();
}
+1
View File
@@ -16,3 +16,4 @@
- [Development](development.md)
- [Contributing](contributing.md)
- [Testing](development/testing.md)
- [Hot Reloading ("Live" Development)](development/hot_reload.md)
+9 -1
View File
@@ -10,9 +10,17 @@ A binary cache for conduwuit that the CI/CD publishes to is available at the
following places (both are the same just different names):
```
https://attic.kennel.juneis.dog/conduit
conduit:Isq8FGyEC6FOXH6nD+BOeAA+bKp6X6UIbupSlGEPuOg=
conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wk=
https://attic.kennel.juneis.dog/conduwuit
conduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTE=
```
The binary caches have been recreated recently due to attic issues. The old public keys were:
```
conduit:Isq8FGyEC6FOXH6nD+BOeAA+bKp6X6UIbupSlGEPuOg=
conduwuit:lYPVh7o1hLu1idH4Xt2QHaRa49WRGSAqzcfFd94aOTw=
```
+1 -2
View File
@@ -16,8 +16,7 @@ look like this:
RUSTFLAGS="--cfg tokio_unstable" cargo build \
--release \
--no-default-features \
--features
backend_rocksdb,systemd,element_hacks,sentry_telemetry,gzip_compression,brotli_compression,zstd_compression,tokio_console
--features=rocksdb,systemd,element_hacks,sentry_telemetry,gzip_compression,brotli_compression,zstd_compression,tokio_console
```
[1]: https://docs.rs/tokio-console/latest/tokio_console/
Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

+93
View File
@@ -0,0 +1,93 @@
# Hot Reloading ("Live" Development)
### Summary
When developing in debug-builds with the nightly toolchain, conduwuit is modular using dynamic libraries and various parts of the application are hot-reloadable while the server is running: http api handlers, admin commands, services, database, etc. These are all split up into individual workspace crates as seen in the `src/` directory. Changes to sourcecode in a crate rebuild that crate and subsequent crates depending on it. Reloading then occurs for the changed crates.
Release builds still produce static binaries which are unaffected. Rust's soundness guarantees are in full force. Thus you cannot hot-reload release binaries.
### Requirements
Currently, this development setup only works on x86_64 and aarch64 Linux glibc. [musl explicitly does not support hot reloadable libraries, and does not implement `dlclose`][2]. macOS does not fully support our usage of `RTLD_GLOBAL` possibly due to some thread-local issues. [This Rust issue][3] may be of relevance, specifically [this comment][4]. It may be possible to get it working on only very modern macOS versions such as at least Sonoma, as currently loading dylibs is supported, but not unloading them in our setup, and the cited comment mentions an Apple WWDC confirming there have been TLS changes to somewhat make this possible.
As mentioned above this requires the nightly toolchain. This is due to reliance on various Cargo.toml features that are only available on nightly, most specifically `RUSTFLAGS` in Cargo.toml. Some of the implementation could also be simpler based on other various nightly features. We hope lots of nightly features start making it out of nightly sooner as there have been dozens of very helpful features that have been stuck in nightly ("unstable") for at least 5+ years that would make this simpler. We encourage greater community consensus to move these features into stability.
This currently only works on x86_64/aarch64 Linux with a glibc C library. musl C library, macOS, and likely other host architectures are not supported (if other architectures work, feel free to let us know and/or make a PR updating this). This should work on GNU ld and lld (rust-lld) and gcc/clang, however if you happen to have linker issues it's recommended to try using `mold` or `gold` linkers, and please let us know in the [conduwuit Matrix room][7] the linker error and what linker solved this issue so we can figure out a solution. Ideally there should be minimal friction to using this, and in the future a build script (`build.rs`) may be suitable to making this easier to use if the capabilities allow us.
### Usage
As of 19 May 2024, the instructions for using this are:
0. Have patience. Don't hesitate to join the [conduwuit Matrix room][7] to receive help using this. As indicated by the various rustflags used and some of the interesting issues linked at the bottom, this is definitely not something the Rust ecosystem or toolchain is used to doing.
1. Install the nightly toolchain using rustup. You may need to use `rustup override set nightly` in your local conduwuit directory, or use `cargo +nightly` for all actions.
2. Uncomment `cargo-features` at the top level / root Cargo.toml
3. Scroll down to the `# Developer profile` section and uncomment ALL the rustflags for each dev profile and their respective packages.
4. In each workspace crate's Cargo.toml (everything under `src/*` AND `deps/rust-rocksdb/Cargo.toml`), uncomment the `dylib` crate type under `[lib]`.
5. Due to [this rpath issue][5], you must export the `LD_LIBRARY_PATH` environment variable to your nightly Rust toolchain library directory. If using rustup (hopefully), use this: `export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$HOME/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/`
6. Start the server. You can use `cargo +nightly run` for this along with the standard.
7. Make some changes where you need to.
8. In a separate terminal window in the same directory (or using a terminal multiplexer like tmux), run the *build* Cargo command `cargo +nightly build`. Cargo should only rebuild what was changed / what's necessary, so it should not be rebuilding all the crates.
9. In your conduwuit server terminal, hit/send `CTRL+C` signal. This will tell conduwuit to find which libraries need to be reloaded, and reloads them as necessary.
10. If there were no errors, it will tell you it successfully reloaded `#` modules, and your changes should now be visible. Repeat 7 - 9 as needed.
To shutdown conduwuit in this setup, hit/send `CTRL+\`. Normal builds still shutdown with `CTRL+C` as usual.
Steps 1 - 5 are the initial first-time steps for using this. To remove the hot reload setup, revert/comment all the Cargo.toml changes.
As mentioned in the requirements section, if you happen to have some linker issues, try using the `-fuse-ld=` rustflag and specify mold or gold in all the `rustflags` definitions in the top level Cargo.toml, and please let us know in the [conduwuit Matrix room][7] the problem. mold can be installed typically through your distro, and gold is provided by the binutils package.
It's possible a helper script can be made to do all of this, or most preferably a specially made build script (build.rs). `cargo watch` support will be implemented soon which will eliminate the need to manually run `cargo build` all together.
### Addendum
Conduit was inherited as a single crate without modularity or reloading in its design. Reasonable partitioning and abstraction allowed a split into several crates, though many circular dependencies had to be corrected. The resulting crates now form a directed graph as depicted in figures below. The interfacing between these crates is still extremely broad which is not mitigable.
Initially [hot_lib_reload][6] was investigated but found appropriate for a project designed with modularity through limited interfaces, not a large and complex existing codebase. Instead a bespoke solution built directly on [libloading][8] satisfied our constraints. This required relatively minimal modifications and zero maintenance burden compared to what would be required otherwise. The technical difference lies with relocation processing: we leverage global bindings (`RTLD_GLOBAL`) in a very intentional way. Most libraries and off-the-shelf module systems (such as [hot_lib_reload][6]) restrict themselves to local bindings (`RTLD_LOCAL`). This allows them to release software to multiple platforms with much greater consistency, but at the cost of burdening applications to explicitly manage these bindings. In our case with an optional feature for developers, we shrug any such requirement to enjoy the cost/benefit on platforms where global relocations are properly cooperative.
To make use of `RTLD_GLOBAL` the application has to be oriented as a directed acyclic graph. The primary rule is simple and illustrated in the figure below: **no crate is allowed to call a function or use a variable from a crate below it.**
![conduwuit's dynamic library setup diagram - created by Jason Volk](assets/libraries.png)
When a symbol is referenced between crates they become bound: **crates cannot be unloaded until their calling crates are first unloaded.** Thus we start the reloading process from the crate which has no callers. There is a small problem though: the first crate is called by the base executable itself! This is solved by using an `RTLD_LOCAL` binding for just one link between the main executable and the first crate, freeing the executable from all modules as no global binding ever occurs between them.
![conduwuit's reload and load order diagram - created by Jason Volk](assets/reload_order.png)
Proper resource management is essential for reliable reloading to occur. This is a very basic ask in RAII-idiomatic Rust and the exposure to reloading hazards is remarkably low, generally stemming from poor patterns and practices. Unfortunately static analysis doesn't enforce reload-safety programmatically (though it could one day), for now hazards can be avoided by knowing a few basic do's and dont's:
1. Understand that code is memory. Just like one is forbidden from referencing free'd memory, one must not transfer control to free'd code. Exposure to this is primarily from two things:
- Callbacks, which this project makes very little use of.
- Async tasks, which are addressed below.
2. Tie all resources to a scope or object lifetime with greatest possible symmetry (locality). For our purposes this applies to code resources, which means async blocks and tokio tasks.
- **Never spawn a task without receiving and storing its JoinHandle**.
- **Always wait on join handles** before leaving a scope or in another cleanup function called by an owning scope.
3. Know any minor specific quirks documented in code or here:
- Don't use `tokio::spawn`, instead use our `Handle` in `core/server.rs`, which is reachable in most of the codebase via `services()` or other state. This is due to some bugs or assumptions made in tokio, as it happens in `unsafe {}` blocks, which are mitigated by circumventing some thread-local variables. Using runtime handles is good practice in any case.
The initial implementation PR is available [here][1].
### Interesting related issues/bugs
- [DT_RUNPATH produced in binary with rpath = true is wrong (cargo)][5]
- [Disabling MIR Optimization in Rust Compilation (cargo)](https://internals.rust-lang.org/t/disabling-mir-optimization-in-rust-compilation/19066/5)
- [Workspace-level metadata (cargo-deb)](https://github.com/kornelski/cargo-deb/issues/68)
[1]: https://github.com/girlbossceo/conduwuit/pull/387
[2]: https://wiki.musl-libc.org/functional-differences-from-glibc.html#Unloading-libraries
[3]: https://github.com/rust-lang/rust/issues/28794
[4]: https://github.com/rust-lang/rust/issues/28794#issuecomment-368693049
[5]: https://github.com/rust-lang/cargo/issues/12746
[6]: https://crates.io/crates/hot-lib-reloader/
[7]: https://matrix.to/#/#conduwuit:puppygock.gay
[8]: https://crates.io/crates/libloading
+1
View File
@@ -153,6 +153,7 @@ Outgoing typing indicators, outgoing read receipts, **and** outgoing presence!
- Interest in supporting other operating systems such as macOS, BSDs, and Windows, and getting them added into CI and doing builds for them
- Add config option for disabling RocksDB Direct IO if needed
- Add various documentation on maintaining conduwuit, using RocksDB online backups, some troubleshooting, using admin commands, etc
- (Developers): Add support for [hot reloadable/"live" modular development](development/hot_reload.md)
- (Developers): Add support for tokio-console
- (Developers): Add support for tracing flame graphs
- Add `release-debuginfo` Cargo build profile
+1 -1
View File
@@ -59,4 +59,4 @@ conduwuit can ping other servers using `!admin debug ping`. This takes a server
#### Allocator memory stats
If using jemalloc (for now) and built with jemallocator's `stats` feature, you can see conduwuit's jemalloc memory stats by using `!admin debug memory-stats`
When using jemalloc with jemallocator's `stats` feature, you can see conduwuit's jemalloc memory stats by using `!admin debug memory-stats`
+13 -1
View File
@@ -58,7 +58,7 @@ script = "lychee --version"
[[task]]
name = "cargo-audit"
group = "security"
script = "cargo audit -D warnings -D unmaintained -D unsound -D yanked --ignore RUSTSEC-2020-0016"
script = "cargo audit -D warnings -D unmaintained -D unsound -D yanked"
[[task]]
name = "cargo-fmt"
@@ -145,3 +145,15 @@ cargo test \
-- \
--color=always
"""
# Ensure that the flake's default output can build and run without crashing
#
# This is a dynamically-linked jemalloc build, which is a case not covered by
# our other tests. We've had linking problems in the past with dynamic
# jemalloc builds that usually show up as an immediate segfault or "invalid free"
[[task]]
name = "nix-default"
group = "tests"
script = """
nix run .#default -- --help
"""
Generated
+34 -16
View File
@@ -68,11 +68,11 @@
]
},
"locked": {
"lastModified": 1715274763,
"narHash": "sha256-3Iv1PGHJn9sV3HO4FlOVaaztOxa9uGLfOmUWrH7v7+A=",
"lastModified": 1716569590,
"narHash": "sha256-5eDbq8TuXFGGO3mqJFzhUbt5zHVTf5zilQoyW5jnJwo=",
"owner": "ipetkov",
"repo": "crane",
"rev": "27025ab71bdca30e7ed0a16c88fd74c5970fc7f5",
"rev": "109987da061a1bf452f435f1653c47511587d919",
"type": "github"
},
"original": {
@@ -90,11 +90,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1715322226,
"narHash": "sha256-ezoe/FwfJpA7sskLoLP2iwfwkYnscEFCP6Vk5kPwh9k=",
"lastModified": 1716359173,
"narHash": "sha256-pYcjP6Gy7i6jPWrjiWAVV0BCQp+DdmGaI/k65lBb/kM=",
"owner": "nix-community",
"repo": "fenix",
"rev": "297c756ba6249d483c1dafe42378560458842173",
"rev": "b6fc5035b28e36a98370d0eac44f4ef3fd323df6",
"type": "github"
},
"original": {
@@ -171,6 +171,23 @@
"type": "github"
}
},
"liburing": {
"flake": false,
"locked": {
"lastModified": 1716565485,
"narHash": "sha256-4R19aJNQYs6vb0/Hz4bWT56YN1P1DkFL/sxdE4Yj0CE=",
"owner": "axboe",
"repo": "liburing",
"rev": "b90c0e670a93caabbebe2d9e24ff85cece4cfe0e",
"type": "github"
},
"original": {
"owner": "axboe",
"ref": "master",
"repo": "liburing",
"type": "github"
}
},
"nix-filter": {
"locked": {
"lastModified": 1710156097,
@@ -221,11 +238,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1715266358,
"narHash": "sha256-doPgfj+7FFe9rfzWo1siAV2mVCasW+Bh8I1cToAXEE4=",
"lastModified": 1716330097,
"narHash": "sha256-8BO3B7e3BiyIDsaKA0tY8O88rClYRTjvAp66y+VBUeU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f1010e0469db743d14519a1efd37e23f8513d714",
"rev": "5710852ba686cc1fd0d3b8e22b3117d43ba374c2",
"type": "github"
},
"original": {
@@ -238,16 +255,16 @@
"rocksdb": {
"flake": false,
"locked": {
"lastModified": 1714770052,
"narHash": "sha256-NCPYF2wYBsB9OHEkZSOYoPlxjC9BBMhJp8EM5M1o3Mc=",
"lastModified": 1716773462,
"narHash": "sha256-5kUH+XK+2lbFfUgbxuNy3YMLHbp6scfWPdtc8za1wDM=",
"owner": "girlbossceo",
"repo": "rocksdb",
"rev": "db6df0b185774778457dabfcbd822cb81760cade",
"rev": "c8a1450231e9c608edf535538dbe8ca1a8d2f3bc",
"type": "github"
},
"original": {
"owner": "girlbossceo",
"ref": "v9.1.1",
"ref": "v9.2.1",
"repo": "rocksdb",
"type": "github"
}
@@ -260,6 +277,7 @@
"fenix": "fenix",
"flake-compat": "flake-compat_2",
"flake-utils": "flake-utils_2",
"liburing": "liburing",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs_2",
"rocksdb": "rocksdb"
@@ -268,11 +286,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1715255944,
"narHash": "sha256-vLLgYpdtKBaGYTamNLg1rbRo1bPXp4Jgded/gnprPVw=",
"lastModified": 1716107283,
"narHash": "sha256-NJgrwLiLGHDrCia5AeIvZUHUY7xYGVryee0/9D3Ir1I=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "5bf2f85c8054d80424899fa581db1b192230efb5",
"rev": "21ec8f523812b88418b2bfc64240c62b3dd967bd",
"type": "github"
},
"original": {
+89 -77
View File
@@ -9,13 +9,15 @@
nix-filter.url = "github:numtide/nix-filter?ref=main";
nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable";
# https://github.com/girlbossceo/rocksdb/commit/db6df0b185774778457dabfcbd822cb81760cade
rocksdb = { url = "github:girlbossceo/rocksdb?ref=v9.1.1"; flake = false; };
rocksdb = { url = "github:girlbossceo/rocksdb?ref=v9.2.1"; flake = false; };
liburing = { url = "github:axboe/liburing?ref=master"; flake = false; };
};
outputs = inputs:
inputs.flake-utils.lib.eachDefaultSystem (system:
let
pkgsHost = inputs.nixpkgs.legacyPackages.${system};
pkgsHostStatic = pkgsHost.pkgsStatic;
# The Rust toolchain to use
toolchain = inputs.fenix.packages.${system}.fromToolchainFile {
@@ -25,7 +27,8 @@
sha256 = "sha256-+syqAd2kX8KVa8/U2gz3blIQTTsYYt3U63xBWaGOSc8";
};
scope = pkgs: pkgs.lib.makeScope pkgs.newScope (self: {
mkScope = pkgs: pkgs.lib.makeScope pkgs.newScope (self: {
inherit pkgs;
book = self.callPackage ./nix/pkgs/book {};
complement = self.callPackage ./nix/pkgs/complement {};
craneLib = ((inputs.crane.mkLib pkgs).overrideToolchain toolchain);
@@ -39,22 +42,87 @@
(builtins.fromJSON (builtins.readFile ./flake.lock))
.nodes.rocksdb.original.ref;
});
# TODO: remove once https://github.com/NixOS/nixpkgs/pull/314945 is available
liburing = pkgs.liburing.overrideAttrs (old: {
# the configure script doesn't support these, and unconditionally
# builds both static and dynamic libraries.
configureFlags = pkgs.lib.subtractLists
[ "--enable-static" "--disable-shared" ]
old.configureFlags;
postInstall = old.postInstall + ''
# we remove the extra outputs
#
# we need to do this to prevent rocksdb from trying to link the
# static library in a dynamic stdenv
rm $out/lib/liburing*${
if pkgs.stdenv.hostPlatform.isStatic then ".so*" else ".a"
}
'';
});
});
scopeHost = (scope pkgsHost);
scopeHost = mkScope pkgsHost;
scopeHostStatic = mkScope pkgsHostStatic;
mkDevShell = scope: scope.pkgs.mkShell {
env = scope.main.env // {
# Rust Analyzer needs to be able to find the path to default crate
# sources, and it can read this environment variable to do so. The
# `rust-src` component is required in order for this to work.
RUST_SRC_PATH = "${toolchain}/lib/rustlib/src/rust/library";
# Convenient way to access a pinned version of Complement's source
# code.
COMPLEMENT_SRC = inputs.complement.outPath;
# Needed for Complement
CGO_CFLAGS = "-I${scope.pkgs.olm}/include";
CGO_LDFLAGS = "-L${scope.pkgs.olm}/lib";
};
# Development tools
packages = [
# Always use nightly rustfmt because most of its options are unstable
#
# This needs to come before `toolchain` in this list, otherwise
# `$PATH` will have stable rustfmt instead.
inputs.fenix.packages.${system}.latest.rustfmt
toolchain
]
++ (with pkgsHost.pkgs; [
engage
cargo-audit
# Needed for producing Debian packages
cargo-deb
# Needed for Complement
go
# Needed for our script for Complement
jq
# Needed for finding broken markdown links
lychee
# Useful for editing the book locally
mdbook
])
++ scope.main.buildInputs
++ scope.main.propagatedBuildInputs
++ scope.main.nativeBuildInputs;
meta.broken = scope.main.meta.broken;
};
in
{
packages = {
default = scopeHost.main;
jemalloc = scopeHost.main.override { features = ["jemalloc"]; };
hmalloc = scopeHost.main.override { features = ["hardened_malloc"]; };
oci-image = scopeHost.oci-image;
oci-image-jemalloc = scopeHost.oci-image.override {
main = scopeHost.main.override {
features = ["jemalloc"];
};
};
oci-image-hmalloc = scopeHost.oci-image.override {
main = scopeHost.main.override {
features = ["hardened_malloc"];
@@ -64,6 +132,7 @@
book = scopeHost.book;
complement = scopeHost.complement;
static-complement = scopeHostStatic.complement;
}
//
builtins.listToAttrs
@@ -79,7 +148,7 @@
config = crossSystem;
};
}).pkgsStatic;
scopeCrossStatic = scope pkgsCrossStatic;
scopeCrossStatic = mkScope pkgsCrossStatic;
in
[
# An output for a statically-linked binary
@@ -88,14 +157,6 @@
value = scopeCrossStatic.main;
}
# An output for a statically-linked binary with jemalloc
{
name = "${binaryName}-jemalloc";
value = scopeCrossStatic.main.override {
features = ["jemalloc"];
};
}
# An output for a statically-linked binary with hardened_malloc
{
name = "${binaryName}-hmalloc";
@@ -110,16 +171,6 @@
value = scopeCrossStatic.oci-image;
}
# An output for an OCI image based on that binary with jemalloc
{
name = "oci-image-${crossSystem}-jemalloc";
value = scopeCrossStatic.oci-image.override {
main = scopeCrossStatic.main.override {
features = ["jemalloc"];
};
};
}
# An output for an OCI image based on that binary with hardened_malloc
{
name = "oci-image-${crossSystem}-hmalloc";
@@ -138,54 +189,15 @@
)
);
devShells.default = pkgsHost.mkShell {
env = scopeHost.main.env // {
# Rust Analyzer needs to be able to find the path to default crate
# sources, and it can read this environment variable to do so. The
# `rust-src` component is required in order for this to work.
RUST_SRC_PATH = "${toolchain}/lib/rustlib/src/rust/library";
# Convenient way to access a pinned version of Complement's source
# code.
COMPLEMENT_SRC = inputs.complement.outPath;
};
# Development tools
packages = [
# Always use nightly rustfmt because most of its options are unstable
#
# This needs to come before `toolchain` in this list, otherwise
# `$PATH` will have stable rustfmt instead.
inputs.fenix.packages.${system}.latest.rustfmt
toolchain
]
++ (with pkgsHost; [
engage
cargo-audit
# Needed for producing Debian packages
cargo-deb
# Needed for Complement
go
olm
# Needed for our script for Complement
jq
# Needed for finding broken markdown links
lychee
# Useful for editing the book locally
mdbook
])
++ (if !pkgsHost.stdenv.isDarwin then [
# Needed for building with io_uring
pkgsHost.liburing
] else [])
++
scopeHost.main.nativeBuildInputs;
};
devShells.default = mkDevShell scopeHostStatic;
devShells.all-features = mkDevShell
(scopeHostStatic.overrideScope (final: prev: {
main = prev.main.override { all_features = true; };
}));
devShells.no-features = mkDevShell
(scopeHostStatic.overrideScope (final: prev: {
main = prev.main.override { default_features = false; };
}));
devShells.dynamic = mkDevShell scopeHost;
});
}
+1 -3
View File
@@ -44,9 +44,7 @@ let
-sha256
${lib.getExe' coreutils "env"} \
CONDUIT_SERVER_NAME="$SERVER_NAME" \
CONDUIT_WELL_KNOWN_SERVER="$SERVER_NAME:8448" \
CONDUIT_WELL_KNOWN_SERVER="$SERVER_NAME:8008" \
CONDUWUIT_SERVER_NAME="$SERVER_NAME" \
${lib.getExe main'}
'';
in
+87 -14
View File
@@ -1,27 +1,91 @@
# Dependencies (keep sorted)
{ craneLib
, inputs
, jq
, lib
, libiconv
, liburing
, pkgsBuildHost
, rocksdb
, rust
, rust-jemalloc-sys
, stdenv
# Options (keep sorted)
, default_features ? true
, disable_release_max_log_level ? false
, all_features ? false
, disable_features ? []
, features ? []
, profile ? "release"
}:
let
# We perform default-feature unification in nix, because some of the dependencies
# on the nix side depend on feature values.
workspaceMembers = builtins.map (member: "${inputs.self}/src/${member}")
(builtins.attrNames (builtins.readDir "${inputs.self}/src"));
crateFeatures = path:
let manifest = lib.importTOML "${path}/Cargo.toml"; in
lib.remove "default" (lib.attrNames manifest.features) ++
lib.attrNames
(lib.filterAttrs
(_: dependency: dependency.optional or false)
manifest.dependencies);
crateDefaultFeatures = path:
(lib.importTOML "${path}/Cargo.toml").features.default;
allDefaultFeatures = lib.unique
(lib.flatten (builtins.map crateDefaultFeatures workspaceMembers));
allFeatures = lib.unique
(lib.flatten (builtins.map crateFeatures workspaceMembers));
features' = lib.unique
(features ++
lib.optionals default_features allDefaultFeatures ++
lib.optionals all_features allFeatures);
disable_features' = disable_features ++ lib.optionals disable_release_max_log_level ["release_max_log_level"];
features'' = lib.subtractLists disable_features' features';
featureEnabled = feature : builtins.elem feature features'';
enableLiburing = featureEnabled "io_uring" && stdenv.isLinux;
# This derivation will set the JEMALLOC_OVERRIDE variable, causing the
# tikv-jemalloc-sys crate to use the nixpkgs jemalloc instead of building it's
# own. In order for this to work, we need to set flags on the build that match
# whatever flags tikv-jemalloc-sys was going to use. These are dependent on
# which features we enable in tikv-jemalloc-sys.
rust-jemalloc-sys' = (rust-jemalloc-sys.override {
# tikv-jemalloc-sys/unprefixed_malloc_on_supported_platforms feature
unprefixed = true;
}).overrideAttrs (old: {
configureFlags = old.configureFlags ++
# tikv-jemalloc-sys/profiling feature
lib.optional (featureEnabled "jemalloc_prof") "--enable-prof";
});
buildDepsOnlyEnv =
let
rocksdb' = rocksdb.override {
enableJemalloc = builtins.elem "jemalloc" features;
};
rocksdb' = (rocksdb.override {
jemalloc = rust-jemalloc-sys';
# rocksdb fails to build with prefixed jemalloc, which is required on
# darwin due to [1]. In this case, fall back to building rocksdb with
# libc malloc. This should not cause conflicts, because all of the
# jemalloc symbols are prefixed.
#
# [1]: https://github.com/tikv/jemallocator/blob/ab0676d77e81268cd09b059260c75b38dbef2d51/jemalloc-sys/src/env.rs#L17
enableJemalloc = featureEnabled "jemalloc" && !stdenv.isDarwin;
}).overrideAttrs (old: {
# TODO: static rocksdb fails to build on darwin
# build log at <https://girlboss.ceo/~strawberry/pb/JjGH>
meta.broken = stdenv.hostPlatform.isStatic && stdenv.isDarwin;
# TODO: switch to enableUring option once https://github.com/NixOS/nixpkgs/pull/314945 is available
buildInputs = old.buildInputs ++ lib.optional enableLiburing liburing;
});
in
{
# https://crane.dev/faq/rebuilds-bindgen.html
NIX_OUTPATH_USED_AS_RANDOM_SEED = "aaaaaaaaaa";
CARGO_PROFILE = profile;
ROCKSDB_INCLUDE_DIR = "${rocksdb'}/include";
ROCKSDB_LIB_DIR = "${rocksdb'}/lib";
@@ -38,7 +102,14 @@ buildDepsOnlyEnv =
buildPackageEnv = {
CONDUWUIT_VERSION_EXTRA = inputs.self.shortRev or inputs.self.dirtyShortRev or "";
} // buildDepsOnlyEnv;
} // buildDepsOnlyEnv // {
# Only needed in static stdenv because these are transitive dependencies of rocksdb
CARGO_BUILD_RUSTFLAGS = buildDepsOnlyEnv.CARGO_BUILD_RUSTFLAGS
+ lib.optionalString (enableLiburing && stdenv.hostPlatform.isStatic)
" -L${lib.getLib liburing}/lib -luring";
};
commonAttrs = {
inherit
@@ -55,16 +126,24 @@ commonAttrs = {
include = [
"Cargo.lock"
"Cargo.toml"
"hot_lib"
"deps"
"src"
];
};
buildInputs = lib.optional (featureEnabled "jemalloc") rust-jemalloc-sys';
nativeBuildInputs = [
# bindgen needs the build platform's libclang. Apparently due to "splicing
# weirdness", pkgs.rustPlatform.bindgenHook on its own doesn't quite do the
# right thing here.
pkgsBuildHost.rustPlatform.bindgenHook
# We don't actually depend on `jq`, but crane's `buildPackage` does, but
# its `buildDepsOnly` doesn't. This causes those two derivations to have
# differing values for `NIX_CFLAGS_COMPILE`, which contributes to spurious
# rebuilds of bindgen and its depedents.
jq
]
++ lib.optionals stdenv.isDarwin [
# https://github.com/NixOS/nixpkgs/issues/206242
@@ -82,22 +161,16 @@ craneLib.buildPackage ( commonAttrs // {
env = buildDepsOnlyEnv;
});
cargoExtraArgs = ""
cargoExtraArgs = "--no-default-features "
+ lib.optionalString
(!default_features)
"--no-default-features "
+ lib.optionalString
(features != [])
"--features " + (builtins.concatStringsSep "," features);
(features'' != [])
"--features " + (builtins.concatStringsSep "," features'');
# This is redundant with CI
cargoTestCommand = "";
cargoCheckCommand = "";
doCheck = false;
# https://crane.dev/faq/rebuilds-bindgen.html
NIX_OUTPATH_USED_AS_RANDOM_SEED = "aaaaaaaaaa";
env = buildPackageEnv;
passthru = {
+2 -1
View File
@@ -11,5 +11,6 @@
},
"nix": {
"enabled": true
}
},
"labels": ["dependencies", "github_actions"]
}
+63
View File
@@ -0,0 +1,63 @@
[package]
name = "conduit_admin"
version.workspace = true
edition.workspace = true
[lib]
path = "mod.rs"
crate-type = [
"rlib",
# "dylib",
]
[features]
default = [
"rocksdb",
"io_uring",
"jemalloc",
"zstd_compression",
"release_max_log_level",
]
dev_release_log_level = []
release_max_log_level = [
"tracing/max_level_trace",
"tracing/release_max_level_info",
"log/max_level_trace",
"log/release_max_level_info",
]
rocksdb = [
"dep:rust-rocksdb",
]
jemalloc = [
"rust-rocksdb/jemalloc",
]
io_uring = [
"rust-rocksdb/io-uring",
]
zstd_compression = [
"rust-rocksdb/zstd",
]
[dependencies]
clap.workspace = true
conduit-api.workspace = true
conduit-core.workspace = true
conduit-database.workspace = true
conduit-service.workspace = true
futures-util.workspace = true
log.workspace = true
loole.workspace = true
regex.workspace = true
ruma.workspace = true
rust-rocksdb.optional = true
rust-rocksdb.workspace = true
serde_json.workspace = true
serde.workspace = true
serde_yaml.workspace = true
tokio.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
[lints]
workspace = true
@@ -1,6 +1,6 @@
use ruma::{api::appservice::Registration, events::room::message::RoomMessageEventContent};
use crate::{service::admin::escape_html, services, Result};
use crate::{escape_html, services, Result};
pub(crate) async fn register(body: Vec<&str>) -> Result<RoomMessageEventContent> {
if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" {
@@ -1,8 +1,8 @@
use clap::Subcommand;
use conduit::Result;
use ruma::events::room::message::RoomMessageEventContent;
use self::appservice_command::{list, register, show, unregister};
use crate::Result;
pub(crate) mod appservice_command;
@@ -1,18 +1,15 @@
use std::{collections::BTreeMap, sync::Arc, time::Instant};
use conduit::{utils::HtmlEscape, Error, Result};
use ruma::{
api::client::error::ErrorKind, events::room::message::RoomMessageEventContent, CanonicalJsonObject, EventId,
RoomId, RoomVersionId, ServerName,
};
use service::{rooms::event_handler::parse_incoming_pdu, sending::send::resolve_actual_dest, services, PduEvent};
use tokio::sync::RwLock;
use tracing::{debug, info, warn};
use tracing_subscriber::EnvFilter;
use crate::{
api::server_server::parse_incoming_pdu, service::sending::send::resolve_actual_dest, services, utils::HtmlEscape,
Error, PduEvent, Result,
};
pub(crate) async fn get_auth_chain(_body: Vec<&str>, event_id: Box<EventId>) -> Result<RoomMessageEventContent> {
let event_id = Arc::<EventId>::from(event_id);
if let Some(event) = services().rooms.timeline.get_pdu_json(&event_id)? {
@@ -127,7 +124,9 @@ pub(crate) async fn get_remote_pdu_list(
for pdu in list {
if force {
_ = get_remote_pdu(Vec::new(), Box::from(pdu), server.clone()).await;
if let Err(e) = get_remote_pdu(Vec::new(), Box::from(pdu), server.clone()).await {
warn!(%e, "Failed to get remote PDU, ignoring error");
}
} else {
get_remote_pdu(Vec::new(), Box::from(pdu), server.clone()).await?;
}
@@ -332,7 +331,7 @@ pub(crate) async fn change_log_level(
};
match services()
.globals
.server
.tracing_reload_handle
.reload(&old_filter_layer)
{
@@ -361,7 +360,7 @@ pub(crate) async fn change_log_level(
};
match services()
.globals
.server
.tracing_reload_handle
.reload(&new_filter_layer)
{
@@ -447,15 +446,16 @@ pub(crate) async fn resolve_true_destination(
));
}
let (actual_dest, hostname_uri) = resolve_actual_dest(&server_name, no_cache, true).await?;
let (actual_dest, hostname_uri) = resolve_actual_dest(&server_name, no_cache).await?;
Ok(RoomMessageEventContent::text_plain(format!(
"Actual destination: {actual_dest:?} | Hostname URI: {hostname_uri}"
)))
}
#[must_use]
pub(crate) fn memory_stats() -> RoomMessageEventContent {
let html_body = crate::alloc::memory_stats();
let html_body = conduit::alloc::memory_stats();
if html_body.is_empty() {
return RoomMessageEventContent::text_plain("malloc stats are not supported on your compiled malloc.");
@@ -1,13 +1,8 @@
use std::fmt::Write as _;
use std::fmt::Write;
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId, RoomId, ServerName, UserId};
use crate::{
service::admin::{escape_html, get_room_info},
services,
utils::HtmlEscape,
Result,
};
use crate::{escape_html, get_room_info, services, utils::HtmlEscape, Result};
pub(crate) async fn disable_room(_body: Vec<&str>, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
services().rooms.metadata.disable_room(&room_id, true)?;
@@ -25,7 +20,8 @@ pub(crate) async fn incoming_federeation(_body: Vec<&str>) -> Result<RoomMessage
for (r, (e, i)) in map.iter() {
let elapsed = i.elapsed();
let _ = writeln!(msg, "{} {}: {}m{}s", r, e, elapsed.as_secs() / 60, elapsed.as_secs() % 60);
writeln!(msg, "{} {}: {}m{}s", r, e, elapsed.as_secs() / 60, elapsed.as_secs() % 60,)
.expect("should be able to write to string buffer");
}
Ok(RoomMessageEventContent::text_plain(&msg))
}
@@ -125,7 +121,7 @@ pub(crate) async fn remote_user_in_rooms(_body: Vec<&str>, user_id: Box<UserId>)
members,
escape_html(name)
)
.unwrap();
.expect("should be able to write to string buffer");
output
})
);
+305
View File
@@ -0,0 +1,305 @@
use std::sync::Arc;
use clap::Parser;
use regex::Regex;
use ruma::{
events::{
relation::InReplyTo,
room::message::{Relation::Reply, RoomMessageEventContent},
TimelineEventType,
},
OwnedRoomId, OwnedUserId, ServerName, UserId,
};
use serde_json::value::to_raw_value;
use tokio::sync::MutexGuard;
use tracing::error;
extern crate conduit_service as service;
use conduit::{Error, Result};
pub(crate) use service::admin::{AdminRoomEvent, Service};
use service::{admin::HandlerResult, pdu::PduBuilder};
use self::{fsck::FsckCommand, tester::TesterCommands};
use crate::{
appservice, appservice::AppserviceCommand, debug, debug::DebugCommand, escape_html, federation,
federation::FederationCommand, fsck, media, media::MediaCommand, query, query::QueryCommand, room,
room::RoomCommand, server, server::ServerCommand, services, tester, user, user::UserCommand,
};
pub(crate) const PAGE_SIZE: usize = 100;
#[cfg_attr(test, derive(Debug))]
#[derive(Parser)]
#[command(name = "@conduit:server.name:", version = env!("CARGO_PKG_VERSION"))]
pub(crate) enum AdminCommand {
#[command(subcommand)]
/// - Commands for managing appservices
Appservices(AppserviceCommand),
#[command(subcommand)]
/// - Commands for managing local users
Users(UserCommand),
#[command(subcommand)]
/// - Commands for managing rooms
Rooms(RoomCommand),
#[command(subcommand)]
/// - Commands for managing federation
Federation(FederationCommand),
#[command(subcommand)]
/// - Commands for managing the server
Server(ServerCommand),
#[command(subcommand)]
/// - Commands for managing media
Media(MediaCommand),
#[command(subcommand)]
/// - Commands for debugging things
Debug(DebugCommand),
#[command(subcommand)]
/// - Query all the database getters and iterators
Query(QueryCommand),
#[command(subcommand)]
/// - Query all the database getters and iterators
Fsck(FsckCommand),
#[command(subcommand)]
Tester(TesterCommands),
}
#[must_use]
pub fn handle(event: AdminRoomEvent, room: OwnedRoomId, user: OwnedUserId) -> HandlerResult {
Box::pin(handle_event(event, room, user))
}
async fn handle_event(event: AdminRoomEvent, admin_room: OwnedRoomId, server_user: OwnedUserId) -> Result<()> {
let (mut message_content, reply) = match event {
AdminRoomEvent::SendMessage(content) => (content, None),
AdminRoomEvent::ProcessMessage(room_message, reply_id) => {
(process_admin_message(room_message).await, Some(reply_id))
},
};
let mutex_state = Arc::clone(
services()
.globals
.roomid_mutex_state
.write()
.await
.entry(admin_room.clone())
.or_default(),
);
let state_lock = mutex_state.lock().await;
if let Some(reply) = reply {
message_content.relates_to = Some(Reply {
in_reply_to: InReplyTo {
event_id: reply.into(),
},
});
}
let response_pdu = PduBuilder {
event_type: TimelineEventType::RoomMessage,
content: to_raw_value(&message_content).expect("event is valid, we just created it"),
unsigned: None,
state_key: None,
redacts: None,
};
if let Err(e) = services()
.rooms
.timeline
.build_and_append_pdu(response_pdu, &server_user, &admin_room, &state_lock)
.await
{
handle_response_error(&e, &admin_room, &server_user, &state_lock).await?;
}
Ok(())
}
async fn handle_response_error(
e: &Error, admin_room: &OwnedRoomId, server_user: &UserId, state_lock: &MutexGuard<'_, ()>,
) -> Result<()> {
error!("Failed to build and append admin room response PDU: \"{e}\"");
let error_room_message = RoomMessageEventContent::text_plain(format!(
"Failed to build and append admin room PDU: \"{e}\"\n\nThe original admin command may have finished \
successfully, but we could not return the output."
));
let response_pdu = PduBuilder {
event_type: TimelineEventType::RoomMessage,
content: to_raw_value(&error_room_message).expect("event is valid, we just created it"),
unsigned: None,
state_key: None,
redacts: None,
};
services()
.rooms
.timeline
.build_and_append_pdu(response_pdu, server_user, admin_room, state_lock)
.await?;
Ok(())
}
// Parse and process a message from the admin room
async fn process_admin_message(room_message: String) -> RoomMessageEventContent {
let mut lines = room_message.lines().filter(|l| !l.trim().is_empty());
let command_line = lines.next().expect("each string has at least one line");
let body = lines.collect::<Vec<_>>();
let admin_command = match parse_admin_command(command_line) {
Ok(command) => command,
Err(error) => {
let server_name = services().globals.server_name();
let message = error.replace("server.name", server_name.as_str());
let html_message = usage_to_html(&message, server_name);
return RoomMessageEventContent::text_html(message, html_message);
},
};
match process_admin_command(admin_command, body).await {
Ok(reply_message) => reply_message,
Err(error) => {
let markdown_message = format!("Encountered an error while handling the command:\n```\n{error}\n```",);
let html_message = format!("Encountered an error while handling the command:\n<pre>\n{error}\n</pre>",);
RoomMessageEventContent::text_html(markdown_message, html_message)
},
}
}
// Parse chat messages from the admin room into an AdminCommand object
fn parse_admin_command(command_line: &str) -> Result<AdminCommand, String> {
// Note: argv[0] is `@conduit:servername:`, which is treated as the main command
let mut argv = command_line.split_whitespace().collect::<Vec<_>>();
// Replace `help command` with `command --help`
// Clap has a help subcommand, but it omits the long help description.
if argv.len() > 1 && argv[1] == "help" {
argv.remove(1);
argv.push("--help");
}
// Backwards compatibility with `register_appservice`-style commands
let command_with_dashes_argv1;
if argv.len() > 1 && argv[1].contains('_') {
command_with_dashes_argv1 = argv[1].replace('_', "-");
argv[1] = &command_with_dashes_argv1;
}
// Backwards compatibility with `register_appservice`-style commands
let command_with_dashes_argv2;
if argv.len() > 2 && argv[2].contains('_') {
command_with_dashes_argv2 = argv[2].replace('_', "-");
argv[2] = &command_with_dashes_argv2;
}
// if the user is using the `query` command (argv[1]), replace the database
// function/table calls with underscores to match the codebase
let command_with_dashes_argv3;
if argv.len() > 3 && argv[1].eq("query") {
command_with_dashes_argv3 = argv[3].replace('_', "-");
argv[3] = &command_with_dashes_argv3;
}
AdminCommand::try_parse_from(argv).map_err(|error| error.to_string())
}
async fn process_admin_command(command: AdminCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
let reply_message_content = match command {
AdminCommand::Appservices(command) => appservice::process(command, body).await?,
AdminCommand::Media(command) => media::process(command, body).await?,
AdminCommand::Users(command) => user::process(command, body).await?,
AdminCommand::Rooms(command) => room::process(command, body).await?,
AdminCommand::Federation(command) => federation::process(command, body).await?,
AdminCommand::Server(command) => server::process(command, body).await?,
AdminCommand::Debug(command) => debug::process(command, body).await?,
AdminCommand::Query(command) => query::process(command, body).await?,
AdminCommand::Fsck(command) => fsck::process(command, body).await?,
AdminCommand::Tester(command) => tester::process(command, body).await?,
};
Ok(reply_message_content)
}
// Utility to turn clap's `--help` text to HTML.
fn usage_to_html(text: &str, server_name: &ServerName) -> String {
// Replace `@conduit:servername:-subcmdname` with `@conduit:servername:
// subcmdname`
let text = text.replace(&format!("@conduit:{server_name}:-"), &format!("@conduit:{server_name}: "));
// For the conduit admin room, subcommands become main commands
let text = text.replace("SUBCOMMAND", "COMMAND");
let text = text.replace("subcommand", "command");
// Escape option names (e.g. `<element-id>`) since they look like HTML tags
let text = escape_html(&text);
// Italicize the first line (command name and version text)
let re = Regex::new("^(.*?)\n").expect("Regex compilation should not fail");
let text = re.replace_all(&text, "<em>$1</em>\n");
// Unmerge wrapped lines
let text = text.replace("\n ", " ");
// Wrap option names in backticks. The lines look like:
// -V, --version Prints version information
// And are converted to:
// <code>-V, --version</code>: Prints version information
// (?m) enables multi-line mode for ^ and $
let re = Regex::new("(?m)^ {4}(([a-zA-Z_&;-]+(, )?)+) +(.*)$").expect("Regex compilation should not fail");
let text = re.replace_all(&text, "<code>$1</code>: $4");
// Look for a `[commandbody]` tag. If it exists, use all lines below it that
// start with a `#` in the USAGE section.
let mut text_lines = text.lines().collect::<Vec<&str>>();
let mut command_body = String::new();
if let Some(line_index) = text_lines.iter().position(|line| *line == "[commandbody]") {
text_lines.remove(line_index);
while text_lines
.get(line_index)
.is_some_and(|line| line.starts_with('#'))
{
command_body += if text_lines[line_index].starts_with("# ") {
&text_lines[line_index][2..]
} else {
&text_lines[line_index][1..]
};
command_body += "[nobr]\n";
text_lines.remove(line_index);
}
}
let text = text_lines.join("\n");
// Improve the usage section
let text = if command_body.is_empty() {
// Wrap the usage line in code tags
let re = Regex::new("(?m)^USAGE:\n {4}(@conduit:.*)$").expect("Regex compilation should not fail");
re.replace_all(&text, "USAGE:\n<code>$1</code>").to_string()
} else {
// Wrap the usage line in a code block, and add a yaml block example
// This makes the usage of e.g. `register-appservice` more accurate
let re = Regex::new("(?m)^USAGE:\n {4}(.*?)\n\n").expect("Regex compilation should not fail");
re.replace_all(&text, "USAGE:\n<pre>$1[nobr]\n[commandbodyblock]</pre>")
.replace("[commandbodyblock]", &command_body)
};
// Add HTML line-breaks
text.replace("\n\n\n", "\n\n")
.replace('\n', "<br>\n")
.replace("[nobr]<br>", "")
}
@@ -1,7 +1,7 @@
use ruma::{events::room::message::RoomMessageEventContent, EventId};
use ruma::{events::room::message::RoomMessageEventContent, EventId, MxcUri};
use tracing::{debug, info};
use crate::{service::admin::MxcUri, services, Result};
use crate::{services, Result};
pub(crate) async fn delete(
_body: Vec<&str>, mxc: Option<Box<MxcUri>>, event_id: Option<Box<EventId>>,
@@ -1,8 +1,8 @@
use clap::Subcommand;
use ruma::{events::room::message::RoomMessageEventContent, EventId};
use ruma::{events::room::message::RoomMessageEventContent, EventId, MxcUri};
use self::media_commands::{delete, delete_list, delete_past_remote_media};
use crate::{service::admin::MxcUri, Result};
use crate::Result;
pub(crate) mod media_commands;
+55
View File
@@ -0,0 +1,55 @@
pub(crate) mod appservice;
pub(crate) mod debug;
pub(crate) mod federation;
pub(crate) mod fsck;
pub(crate) mod handler;
pub(crate) mod media;
pub(crate) mod query;
pub(crate) mod room;
pub(crate) mod server;
pub(crate) mod tester;
pub(crate) mod user;
pub(crate) mod utils;
extern crate conduit_api as api;
extern crate conduit_core as conduit;
extern crate conduit_service as service;
pub(crate) use conduit::{mod_ctor, mod_dtor, Result};
pub use handler::handle;
pub(crate) use service::{services, user_is_local};
pub(crate) use crate::{
handler::Service,
utils::{escape_html, get_room_info},
};
mod_ctor! {}
mod_dtor! {}
#[cfg(test)]
mod test {
use clap::Parser;
use crate::handler::AdminCommand;
#[test]
fn get_help_short() { get_help_inner("-h"); }
#[test]
fn get_help_long() { get_help_inner("--help"); }
#[test]
fn get_help_subcommand() { get_help_inner("help"); }
fn get_help_inner(input: &str) {
let error = AdminCommand::try_parse_from(["argv[0] doesn't matter", input])
.unwrap_err()
.to_string();
// Search for a handful of keywords that suggest the help printed properly
assert!(error.contains("Usage:"));
assert!(error.contains("Commands:"));
assert!(error.contains("Options:"));
}
}
@@ -46,7 +46,7 @@ pub(crate) enum RoomAliasCommand {
room_alias_localpart: String,
},
/// - Remove an alias
/// - Remove a local alias
Remove {
/// The alias localpart to remove (`alias`, not `#alias:servername.tld`)
room_alias_localpart: String,
@@ -1,9 +1,9 @@
use std::fmt::Write as _;
use std::fmt::Write;
use ruma::{events::room::message::RoomMessageEventContent, RoomAliasId};
use super::RoomAliasCommand;
use crate::{service::admin::escape_html, services, Result};
use crate::{escape_html, services, Result};
pub(crate) async fn process(command: RoomAliasCommand, _body: Vec<&str>) -> Result<RoomMessageEventContent> {
match command {
@@ -79,12 +79,13 @@ pub(crate) async fn process(command: RoomAliasCommand, _body: Vec<&str>) -> Resu
match aliases {
Ok(aliases) => {
let plain_list = aliases.iter().fold(String::new(), |mut output, alias| {
writeln!(output, "- {alias}").unwrap();
writeln!(output, "- {alias}").expect("should be able to write to string buffer");
output
});
let html_list = aliases.iter().fold(String::new(), |mut output, alias| {
writeln!(output, "<li>{}</li>", escape_html(alias.as_ref())).unwrap();
writeln!(output, "<li>{}</li>", escape_html(alias.as_ref()))
.expect("should be able to write to string buffer");
output
});
@@ -106,7 +107,8 @@ pub(crate) async fn process(command: RoomAliasCommand, _body: Vec<&str>) -> Resu
let plain_list = aliases
.iter()
.fold(String::new(), |mut output, (alias, id)| {
writeln!(output, "- `{alias}` -> #{id}:{server_name}").unwrap();
writeln!(output, "- `{alias}` -> #{id}:{server_name}")
.expect("should be able to write to string buffer");
output
});
@@ -120,7 +122,7 @@ pub(crate) async fn process(command: RoomAliasCommand, _body: Vec<&str>) -> Resu
escape_html(id.as_ref()),
server_name
)
.unwrap();
.expect("should be able to write to string buffer");
output
});
@@ -1,11 +1,8 @@
use std::fmt::Write as _;
use std::fmt::Write;
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId};
use crate::{
service::admin::{escape_html, get_room_info, PAGE_SIZE},
services, Result,
};
use crate::{escape_html, get_room_info, handler::PAGE_SIZE, services, Result};
pub(crate) async fn list(_body: Vec<&str>, page: Option<usize>) -> Result<RoomMessageEventContent> {
// TODO: i know there's a way to do this with clap, but i can't seem to find it
@@ -51,7 +48,7 @@ pub(crate) async fn list(_body: Vec<&str>, page: Option<usize>) -> Result<RoomMe
members,
escape_html(name)
)
.unwrap();
.expect("should be able to write to string buffer");
output
})
);
@@ -1,12 +1,9 @@
use std::fmt::Write as _;
use std::fmt::Write;
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId};
use super::RoomDirectoryCommand;
use crate::{
service::admin::{escape_html, get_room_info, PAGE_SIZE},
services, Result,
};
use crate::{escape_html, get_room_info, handler::PAGE_SIZE, services, Result};
pub(crate) async fn process(command: RoomDirectoryCommand, _body: Vec<&str>) -> Result<RoomMessageEventContent> {
match command {
@@ -68,7 +65,7 @@ pub(crate) async fn process(command: RoomDirectoryCommand, _body: Vec<&str>) ->
members,
escape_html(name.as_ref())
)
.unwrap();
.expect("should be able to write to string buffer");
output
})
);
@@ -1,18 +1,16 @@
use std::fmt::Write as _;
use std::fmt::Write;
use api::client_server::{get_alias_helper, leave_room};
use ruma::{
events::room::message::RoomMessageEventContent, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomOrAliasId,
};
use tracing::{debug, error, info, warn};
use super::RoomModerationCommand;
use crate::{
api::client_server::{get_alias_helper, leave_room},
service::admin::{escape_html, Service},
services,
utils::user_id::user_is_local,
Result,
use super::{
super::{escape_html, Service},
RoomModerationCommand,
};
use crate::{services, user_is_local, Result};
pub(crate) async fn process(command: RoomModerationCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
match command {
@@ -105,16 +103,16 @@ pub(crate) async fn process(command: RoomModerationCommand, body: Vec<&str>) ->
.filter_map(|user| {
user.ok().filter(|local_user| {
user_is_local(local_user)
// additional wrapped check here is to avoid adding remote users
// who are in the admin room to the list of local users (would fail auth check)
&& (user_is_local(local_user)
&& services()
.users
.is_admin(local_user)
.unwrap_or(true)) // since this is a force
// operation, assume user
// is an admin if somehow
// this fails
// additional wrapped check here is to avoid adding remote users
// who are in the admin room to the list of local users (would fail auth check)
&& (user_is_local(local_user)
&& services()
.users
.is_admin(local_user)
.unwrap_or(true)) // since this is a force
// operation, assume user
// is an admin if somehow
// this fails
})
})
.collect::<Vec<OwnedUserId>>()
@@ -124,7 +122,9 @@ pub(crate) async fn process(command: RoomModerationCommand, body: Vec<&str>) ->
&local_user, &room_id
);
_ = leave_room(&local_user, &room_id, None).await;
if let Err(e) = leave_room(&local_user, &room_id, None).await {
warn!(%e, "Failed to leave room");
}
}
} else {
for local_user in services()
@@ -134,14 +134,14 @@ pub(crate) async fn process(command: RoomModerationCommand, body: Vec<&str>) ->
.filter_map(|user| {
user.ok().filter(|local_user| {
local_user.server_name() == services().globals.server_name()
// additional wrapped check here is to avoid adding remote users
// who are in the admin room to the list of local users (would fail auth check)
&& (local_user.server_name()
== services().globals.server_name()
&& !services()
.users
.is_admin(local_user)
.unwrap_or(false))
// additional wrapped check here is to avoid adding remote users
// who are in the admin room to the list of local users (would fail auth check)
&& (local_user.server_name()
== services().globals.server_name()
&& !services()
.users
.is_admin(local_user)
.unwrap_or(false))
})
})
.collect::<Vec<OwnedUserId>>()
@@ -170,8 +170,8 @@ pub(crate) async fn process(command: RoomModerationCommand, body: Vec<&str>) ->
}
Ok(RoomMessageEventContent::text_plain(
"Room banned and removed all our local users, use disable-room to stop receiving new inbound \
federation events as well if needed.",
"Room banned and removed all our local users, use `!admin federation disable-room` to stop receiving \
new inbound federation events as well if needed.",
))
},
RoomModerationCommand::BanListOfRooms {
@@ -309,19 +309,19 @@ pub(crate) async fn process(command: RoomModerationCommand, body: Vec<&str>) ->
.filter_map(|user| {
user.ok().filter(|local_user| {
local_user.server_name() == services().globals.server_name()
// additional wrapped check here is to avoid adding remote users
// who are in the admin room to the list of local users (would fail auth check)
&& (local_user.server_name()
== services().globals.server_name()
&& services()
.users
.is_admin(local_user)
.unwrap_or(true)) // since this is a
// force operation,
// assume user is
// an admin if
// somehow this
// fails
// additional wrapped check here is to avoid adding remote users
// who are in the admin room to the list of local users (would fail auth check)
&& (local_user.server_name()
== services().globals.server_name()
&& services()
.users
.is_admin(local_user)
.unwrap_or(true)) // since this is a
// force operation,
// assume user is
// an admin if
// somehow this
// fails
})
})
.collect::<Vec<OwnedUserId>>()
@@ -331,7 +331,9 @@ pub(crate) async fn process(command: RoomModerationCommand, body: Vec<&str>) ->
admins too)",
&local_user, room_id
);
_ = leave_room(&local_user, &room_id, None).await;
if let Err(e) = leave_room(&local_user, &room_id, None).await {
warn!(%e, "Failed to leave room");
}
}
} else {
for local_user in services()
@@ -341,14 +343,14 @@ pub(crate) async fn process(command: RoomModerationCommand, body: Vec<&str>) ->
.filter_map(|user| {
user.ok().filter(|local_user| {
local_user.server_name() == services().globals.server_name()
// additional wrapped check here is to avoid adding remote users
// who are in the admin room to the list of local users (would fail auth check)
&& (local_user.server_name()
== services().globals.server_name()
&& !services()
.users
.is_admin(local_user)
.unwrap_or(false))
// additional wrapped check here is to avoid adding remote users
// who are in the admin room to the list of local users (would fail auth check)
&& (local_user.server_name()
== services().globals.server_name()
&& !services()
.users
.is_admin(local_user)
.unwrap_or(false))
})
})
.collect::<Vec<OwnedUserId>>()
@@ -4,7 +4,7 @@ use crate::{services, Result};
pub(crate) async fn uptime(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
let seconds = services()
.globals
.server
.started
.elapsed()
.expect("standard duration")
@@ -28,7 +28,7 @@ pub(crate) async fn show_config(_body: Vec<&str>) -> Result<RoomMessageEventCont
pub(crate) async fn memory_usage(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
let response0 = services().memory_usage().await;
let response1 = services().globals.db.memory_usage();
let response2 = crate::alloc::memory_usage();
let response2 = conduit::alloc::memory_usage();
Ok(RoomMessageEventContent::text_plain(format!(
"Services:\n{response0}\n\nDatabase:\n{response1}\n{}",
@@ -69,12 +69,15 @@ pub(crate) async fn backup_database(_body: Vec<&str>) -> Result<RoomMessageEvent
));
}
let mut result = tokio::task::spawn_blocking(move || match services().globals.db.backup() {
Ok(()) => String::new(),
Err(e) => (*e).to_string(),
})
.await
.unwrap();
let mut result = services()
.server
.runtime()
.spawn_blocking(move || match services().globals.db.backup() {
Ok(()) => String::new(),
Err(e) => (*e).to_string(),
})
.await
.unwrap();
if result.is_empty() {
result = services().globals.db.backup_list()?;
@@ -9,6 +9,6 @@ pub(crate) enum TesterCommands {
}
pub(crate) async fn process(command: TesterCommands, _body: Vec<&str>) -> Result<RoomMessageEventContent> {
Ok(match command {
TesterCommands::Tester => RoomMessageEventContent::notice_plain(String::from("complete")),
TesterCommands::Tester => RoomMessageEventContent::notice_plain(String::from("completed")),
})
}
@@ -1,15 +1,13 @@
use std::{fmt::Write as _, sync::Arc};
use api::client_server::{join_room_by_id_helper, leave_all_rooms};
use conduit::utils;
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId, UserId};
use tracing::{error, info, warn};
use crate::{
api::client_server::{join_room_by_id_helper, leave_all_rooms, AUTO_GEN_PASSWORD_LENGTH},
service::admin::{escape_html, get_room_info},
services,
utils::{self, user_id::user_is_local},
Result,
};
use crate::{escape_html, get_room_info, services, user_is_local, Result};
const AUTO_GEN_PASSWORD_LENGTH: usize = 25;
pub(crate) async fn list(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
match services().users.list_local_users() {
@@ -67,7 +65,8 @@ pub(crate) async fn create(
.new_user_displayname_suffix
.is_empty()
{
_ = write!(displayname, " {}", services().globals.config.new_user_displayname_suffix);
write!(displayname, " {}", services().globals.config.new_user_displayname_suffix)
.expect("should be able to write to string buffer");
}
services()
@@ -111,7 +110,7 @@ pub(crate) async fn create(
)
.await
{
Ok(_) => {
Ok(_response) => {
info!("Automatically joined room {room} for user {user_id}");
},
Err(e) => {
+30
View File
@@ -0,0 +1,30 @@
pub(crate) use conduit::utils::HtmlEscape;
use ruma::OwnedRoomId;
use crate::services;
pub(crate) fn escape_html(s: &str) -> String {
s.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
}
pub(crate) fn get_room_info(id: &OwnedRoomId) -> (OwnedRoomId, u64, String) {
(
id.clone(),
services()
.rooms
.state_cache
.room_joined_count(id)
.ok()
.flatten()
.unwrap_or(0),
services()
.rooms
.state_accessor
.get_name(id)
.ok()
.flatten()
.unwrap_or_else(|| id.to_string()),
)
}
-7
View File
@@ -1,7 +0,0 @@
//! Default allocator with no special features
/// Always returns the empty string
pub(crate) fn memory_stats() -> String { Default::default() }
/// Always returns the empty string
pub(crate) fn memory_usage() -> String { Default::default() }
-8
View File
@@ -1,8 +0,0 @@
#[global_allocator]
static HMALLOC: hardened_malloc_rs::HardenedMalloc = hardened_malloc_rs::HardenedMalloc;
pub(crate) fn memory_usage() -> String {
String::default() //TODO: get usage
}
pub(crate) fn memory_stats() -> String { "Extended statistics are not available from hardened_malloc.".to_owned() }
+66
View File
@@ -0,0 +1,66 @@
[package]
name = "conduit_api"
version.workspace = true
edition.workspace = true
[lib]
path = "mod.rs"
crate-type = [
"rlib",
# "dylib",
]
[features]
default = [
"element_hacks",
"gzip_compression",
"brotli_compression",
"release_max_log_level",
]
element_hacks = []
dev_release_log_level = []
release_max_log_level = [
"tracing/max_level_trace",
"tracing/release_max_level_info",
"log/max_level_trace",
"log/release_max_level_info",
]
gzip_compression = [
"reqwest/gzip",
]
brotli_compression = [
"reqwest/brotli",
]
[dependencies]
argon2.workspace = true
axum-extra.workspace = true
axum.workspace = true
base64.workspace = true
bytes.workspace = true
conduit-core.workspace = true
conduit-database.workspace = true
conduit-service.workspace = true
futures-util.workspace = true
hmac.workspace = true
http.workspace = true
hyper.workspace = true
image.workspace = true
ipaddress.workspace = true
jsonwebtoken.workspace = true
log.workspace = true
rand.workspace = true
reqwest.workspace = true
ruma.workspace = true
serde_html_form.workspace = true
serde_json.workspace = true
serde.workspace = true
sha-1.workspace = true
thiserror.workspace = true
tokio.workspace = true
tracing.workspace = true
webpage.workspace = true
[lints]
workspace = true
+34 -7
View File
@@ -1,10 +1,11 @@
use std::fmt::Write as _;
use std::fmt::Write;
use conduit::debug_info;
use register::RegistrationKind;
use ruma::{
api::client::{
account::{
change_password, deactivate, get_3pids, get_username_availability,
change_password, check_registration_token_validity, deactivate, get_3pids, get_username_availability,
register::{self, LoginType},
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn, whoami,
ThirdPartyIdRemovalStatus,
@@ -19,9 +20,10 @@ use tracing::{error, info, warn};
use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH};
use crate::{
api::client_server::{self, join_room_by_id_helper},
service, services,
utils::{self, user_id::user_is_local},
client_server::{self, join_room_by_id_helper},
service::user_is_local,
services,
utils::{self},
Error, Result, Ruma,
};
@@ -240,7 +242,8 @@ pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result<
// If `new_user_displayname_suffix` is set, registration will push whatever
// content is set to the user's display name with a space before it
if !services().globals.new_user_displayname_suffix().is_empty() {
_ = write!(displayname, " {}", services().globals.config.new_user_displayname_suffix);
write!(displayname, " {}", services().globals.config.new_user_displayname_suffix)
.expect("should be able to write to string buffer");
}
services()
@@ -288,10 +291,11 @@ pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result<
.users
.create_device(&user_id, &device_id, &token, body.initial_device_display_name.clone())?;
info!("New user \"{}\" registered on this server.", user_id);
debug_info!(%user_id, %device_id, "User account was created");
// log in conduit admin channel if a non-guest user registered
if body.appservice_info.is_none() && !is_guest {
info!("New user \"{user_id}\" registered on this server.");
services()
.admin
.send_message(RoomMessageEventContent::notice_plain(format!(
@@ -302,6 +306,8 @@ pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result<
// log in conduit admin channel if a guest registered
if body.appservice_info.is_none() && is_guest && services().globals.log_guest_registrations() {
info!("New guest user \"{user_id}\" registered on this server.");
if let Some(device_display_name) = &body.initial_device_display_name {
if body
.initial_device_display_name
@@ -593,3 +599,24 @@ pub(crate) async fn request_3pid_management_token_via_msisdn_route(
"Third party identifier is not allowed",
))
}
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
///
/// Checks if the provided registration token is valid at the time of checking
///
/// Currently does not have any ratelimiting, and this isn't very practical as
/// there is only one registration token allowed.
pub(crate) async fn check_registration_token_validity(
body: Ruma<check_registration_token_validity::v1::Request>,
) -> Result<check_registration_token_validity::v1::Response> {
let Some(reg_token) = services().globals.config.registration_token.clone() else {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Server does not allow token registration.",
));
};
Ok(check_registration_token_validity::v1::Response {
valid: reg_token == body.token,
})
}
+4 -4
View File
@@ -13,8 +13,9 @@ use ruma::{
use tracing::debug;
use crate::{
debug_info, debug_warn, service::appservice::RegistrationInfo, services, utils::server_name::server_is_ours, Error,
Result, Ruma,
debug_info, debug_warn,
service::{appservice::RegistrationInfo, server_is_ours},
services, Error, Result, Ruma,
};
/// # `PUT /_matrix/client/v3/directory/room/{roomAlias}`
@@ -65,7 +66,6 @@ pub(crate) async fn create_alias_route(body: Ruma<create_alias::v3::Request>) ->
/// - TODO: Update canonical alias event
pub(crate) async fn delete_alias_route(body: Ruma<delete_alias::v3::Request>) -> Result<delete_alias::v3::Response> {
alias_checks(&body.room_alias, &body.appservice_info).await?;
if services()
.rooms
.alias
@@ -99,7 +99,7 @@ pub(crate) async fn get_alias_route(body: Ruma<get_alias::v3::Request>) -> Resul
get_alias_helper(body.body.room_alias, None).await
}
pub(crate) async fn get_alias_helper(
pub async fn get_alias_helper(
room_alias: OwnedRoomAliasId, servers: Option<Vec<OwnedServerName>>,
) -> Result<get_alias::v3::Response> {
debug!("get_alias_helper servers: {servers:?}");
+1 -1
View File
@@ -163,7 +163,7 @@ pub(crate) async fn get_context_route(body: Ruma<get_context::v3::Request>) -> R
.map(|(_, pdu)| pdu.to_room_event())
.collect();
let mut state = Vec::new();
let mut state = Vec::with_capacity(state_ids.len());
for (shortstatekey, id) in state_ids {
let (event_type, state_key) = services()
+1 -1
View File
@@ -24,7 +24,7 @@ use ruma::{
};
use tracing::{error, info, warn};
use crate::{services, utils::server_name::server_is_ours, Error, Result, Ruma};
use crate::{service::server_is_ours, services, Error, Result, Ruma};
/// # `POST /_matrix/client/v3/publicRooms`
///
+2 -1
View File
@@ -22,8 +22,9 @@ use tracing::debug;
use super::SESSION_ID_LENGTH;
use crate::{
service::user_is_local,
services,
utils::{self, user_id::user_is_local},
utils::{self},
Error, Result, Ruma,
};
+16 -5
View File
@@ -15,14 +15,16 @@ use webpage::HTML;
use crate::{
debug_warn,
service::media::{FileMeta, UrlPreviewData},
service::{
media::{FileMeta, UrlPreviewData},
server_is_ours,
},
services,
utils::{
self,
content_disposition::{
content_disposition_type, make_content_disposition, make_content_type, sanitise_filename,
},
server_name::server_is_ours,
},
Error, Result, Ruma, RumaResponse,
};
@@ -190,7 +192,7 @@ pub(crate) async fn get_content_route(body: Ruma<get_content::v3::Request>) -> R
content_disposition,
}) = services().media.get(mxc.clone()).await?
{
let content_disposition = Some(make_content_disposition(&file, &content_type, content_disposition));
let content_disposition = Some(make_content_disposition(&file, &content_type, content_disposition, None));
let content_type = Some(make_content_type(&file, &content_type).to_owned());
Ok(get_content::v3::Response {
@@ -218,6 +220,7 @@ pub(crate) async fn get_content_route(body: Ruma<get_content::v3::Request>) -> R
&response.file,
&response.content_type,
response.content_disposition,
None,
));
let content_type = Some(make_content_type(&response.file, &response.content_type).to_owned());
@@ -270,7 +273,12 @@ pub(crate) async fn get_content_as_filename_route(
content_disposition,
}) = services().media.get(mxc.clone()).await?
{
let content_disposition = Some(make_content_disposition(&file, &content_type, content_disposition));
let content_disposition = Some(make_content_disposition(
&file,
&content_type,
content_disposition,
Some(body.filename.clone()),
));
let content_type = Some(make_content_type(&file, &content_type).to_owned());
Ok(get_content_as_filename::v3::Response {
@@ -295,6 +303,7 @@ pub(crate) async fn get_content_as_filename_route(
&remote_content_response.file,
&remote_content_response.content_type,
remote_content_response.content_disposition,
None,
));
let content_type = Some(
make_content_type(&remote_content_response.file, &remote_content_response.content_type).to_owned(),
@@ -366,7 +375,7 @@ pub(crate) async fn get_content_thumbnail_route(
)
.await?
{
let content_disposition = Some(make_content_disposition(&file, &content_type, content_disposition));
let content_disposition = Some(make_content_disposition(&file, &content_type, content_disposition, None));
let content_type = Some(make_content_type(&file, &content_type).to_owned());
Ok(get_content_thumbnail::v3::Response {
@@ -423,6 +432,7 @@ pub(crate) async fn get_content_thumbnail_route(
&get_thumbnail_response.file,
&get_thumbnail_response.content_type,
get_thumbnail_response.content_disposition,
None,
));
let content_type = Some(
make_content_type(&get_thumbnail_response.file, &get_thumbnail_response.content_type).to_owned(),
@@ -496,6 +506,7 @@ async fn get_remote_content(
&content_response.file,
&content_response.content_type,
content_response.content_disposition,
None,
));
let content_type = Some(make_content_type(&content_response.file, &content_response.content_type).to_owned());
+20 -9
View File
@@ -35,9 +35,12 @@ use tracing::{debug, error, info, trace, warn};
use super::get_alias_helper;
use crate::{
service::pdu::{gen_event_id_canonical_json, PduBuilder},
service::{
pdu::{gen_event_id_canonical_json, PduBuilder},
server_is_ours, user_is_local,
},
services,
utils::{self, server_name::server_is_ours, user_id::user_is_local},
utils::{self},
Error, PduEvent, Result, Ruma,
};
@@ -77,7 +80,9 @@ async fn banned_room_check(user_id: &UserId, room_id: Option<&RoomId>, server_na
// ignore errors
leave_all_rooms(user_id).await;
_ = services().users.deactivate_account(user_id);
if let Err(e) = services().users.deactivate_account(user_id) {
warn!(%e, "Failed to deactivate account");
}
}
return Err(Error::BadRequest(
@@ -112,7 +117,9 @@ async fn banned_room_check(user_id: &UserId, room_id: Option<&RoomId>, server_na
// ignore errors
leave_all_rooms(user_id).await;
_ = services().users.deactivate_account(user_id);
if let Err(e) = services().users.deactivate_account(user_id) {
warn!(%e, "Failed to deactivate account");
}
}
return Err(Error::BadRequest(
@@ -607,7 +614,7 @@ pub(crate) async fn joined_members_route(
})
}
pub(crate) async fn join_room_by_id_helper(
pub async fn join_room_by_id_helper(
sender_user: Option<&UserId>, room_id: &RoomId, reason: Option<String>, servers: &[OwnedServerName],
_third_party_signed: Option<&ThirdPartySigned>,
) -> Result<join_room_by_id::v3::Response> {
@@ -1525,7 +1532,7 @@ pub(crate) async fn invite_helper(
// Make a user leave all their joined rooms, forgets all rooms, and ignores
// errors
pub(crate) async fn leave_all_rooms(user_id: &UserId) {
pub async fn leave_all_rooms(user_id: &UserId) {
let all_rooms = services()
.rooms
.state_cache
@@ -1545,12 +1552,16 @@ pub(crate) async fn leave_all_rooms(user_id: &UserId) {
};
// ignore errors
_ = services().rooms.state_cache.forget(&room_id, user_id);
_ = leave_room(user_id, &room_id, None).await;
if let Err(e) = services().rooms.state_cache.forget(&room_id, user_id) {
warn!(%e, "Failed to forget room");
}
if let Err(e) = leave_room(user_id, &room_id, None).await {
warn!(%e, "Failed to leave room");
}
}
}
pub(crate) async fn leave_room(user_id: &UserId, room_id: &RoomId, reason: Option<String>) -> Result<()> {
pub async fn leave_room(user_id: &UserId, room_id: &RoomId, reason: Option<String>) -> Result<()> {
// Ask a remote server if we don't have this room
if !services()
.rooms
+2 -4
View File
@@ -3,6 +3,7 @@ use std::{
sync::Arc,
};
use conduit::PduCount;
use ruma::{
api::client::{
error::ErrorKind,
@@ -14,10 +15,7 @@ use ruma::{
};
use serde_json::{from_str, Value};
use crate::{
service::{pdu::PduBuilder, rooms::timeline::PduCount},
services, utils, Error, PduEvent, Result, Ruma,
};
use crate::{service::pdu::PduBuilder, services, utils, Error, PduEvent, Result, Ruma};
/// # `PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}`
///
+38 -39
View File
@@ -1,40 +1,41 @@
mod account;
mod alias;
mod backup;
mod capabilities;
mod config;
mod context;
mod device;
mod directory;
mod filter;
mod keys;
mod media;
mod membership;
mod message;
mod presence;
mod profile;
mod push;
mod read_marker;
mod redact;
mod relations;
mod report;
mod room;
mod search;
mod session;
mod space;
mod state;
mod sync;
mod tag;
mod thirdparty;
mod threads;
mod to_device;
mod typing;
mod unstable;
mod unversioned;
mod user_directory;
mod voip;
pub(crate) mod account;
pub(crate) mod alias;
pub(crate) mod backup;
pub(crate) mod capabilities;
pub(crate) mod config;
pub(crate) mod context;
pub(crate) mod device;
pub(crate) mod directory;
pub(crate) mod filter;
pub(crate) mod keys;
pub(crate) mod media;
pub(crate) mod membership;
pub(crate) mod message;
pub(crate) mod presence;
pub(crate) mod profile;
pub(crate) mod push;
pub(crate) mod read_marker;
pub(crate) mod redact;
pub(crate) mod relations;
pub(crate) mod report;
pub(crate) mod room;
pub(crate) mod search;
pub(crate) mod session;
pub(crate) mod space;
pub(crate) mod state;
pub(crate) mod sync;
pub(crate) mod tag;
pub(crate) mod thirdparty;
pub(crate) mod threads;
pub(crate) mod to_device;
pub(crate) mod typing;
pub(crate) mod unstable;
pub(crate) mod unversioned;
pub(crate) mod user_directory;
pub(crate) mod voip;
pub(crate) use account::*;
pub use alias::get_alias_helper;
pub(crate) use alias::*;
pub(crate) use backup::*;
pub(crate) use capabilities::*;
@@ -46,6 +47,7 @@ pub(crate) use filter::*;
pub(crate) use keys::*;
pub(crate) use media::*;
pub(crate) use membership::*;
pub use membership::{join_room_by_id_helper, leave_all_rooms, leave_room};
pub(crate) use message::*;
pub(crate) use presence::*;
pub(crate) use profile::*;
@@ -77,7 +79,4 @@ const DEVICE_ID_LENGTH: usize = 10;
const TOKEN_LENGTH: usize = 32;
/// generated user session ID length
pub(crate) const SESSION_ID_LENGTH: usize = 32;
/// auto-generated password length
pub(crate) const AUTO_GEN_PASSWORD_LENGTH: usize = 25;
const SESSION_ID_LENGTH: usize = service::uiaa::SESSION_ID_LENGTH;
+15 -5
View File
@@ -12,8 +12,12 @@ use ruma::{
presence::PresenceState,
};
use serde_json::value::to_raw_value;
use tracing::warn;
use crate::{service::pdu::PduBuilder, services, utils::user_id::user_is_local, Error, Result, Ruma};
use crate::{
service::{pdu::PduBuilder, user_is_local},
services, Error, Result, Ruma,
};
/// # `PUT /_matrix/client/r0/profile/{userId}/displayname`
///
@@ -79,11 +83,14 @@ pub(crate) async fn set_displayname_route(
);
let state_lock = mutex_state.lock().await;
_ = services()
if let Err(e) = services()
.rooms
.timeline
.build_and_append_pdu(pdu_builder, sender_user, &room_id, &state_lock)
.await;
.await
{
warn!(%e, "Failed to update/send new display name in room");
}
}
if services().globals.allow_local_presence() {
@@ -221,11 +228,14 @@ pub(crate) async fn set_avatar_url_route(
);
let state_lock = mutex_state.lock().await;
_ = services()
if let Err(e) = services()
.rooms
.timeline
.build_and_append_pdu(pdu_builder, sender_user, &room_id, &state_lock)
.await;
.await
{
warn!(%e, "Failed to set/update room with new avatar URL / pfp");
}
}
if services().globals.allow_local_presence() {
+2 -1
View File
@@ -1,5 +1,6 @@
use std::collections::BTreeMap;
use conduit::PduCount;
use ruma::{
api::client::{error::ErrorKind, read_marker::set_read_marker, receipt::create_receipt},
events::{
@@ -9,7 +10,7 @@ use ruma::{
MilliSecondsSinceUnixEpoch,
};
use crate::{service::rooms::timeline::PduCount, services, Error, Result, Ruma};
use crate::{services, Error, Result, Ruma};
/// # `POST /_matrix/client/r0/rooms/{roomId}/read_markers`
///
+7 -5
View File
@@ -1,5 +1,6 @@
use std::{cmp::max, collections::BTreeMap, sync::Arc};
use conduit::{debug_info, debug_warn};
use ruma::{
api::client::{
error::ErrorKind,
@@ -28,8 +29,7 @@ use serde_json::{json, value::to_raw_value};
use tracing::{error, info, warn};
use crate::{
api::client_server::invite_helper,
debug_info, debug_warn,
client_server::invite_helper,
service::{appservice::RegistrationInfo, pdu::PduBuilder},
services, Error, Result, Ruma,
};
@@ -388,7 +388,7 @@ pub(crate) async fn create_room_route(body: Ruma<create_room::v3::Request>) -> R
Error::BadRequest(ErrorKind::InvalidParam, "Invalid initial state event.")
})?;
debug_warn!("initial state event: {event:?}");
debug_info!("Room creation initial state event: {event:?}");
// client/appservice workaround: if a user sends an initial_state event with a
// state event in there with the content of literally `{}` (not null or empty
@@ -460,7 +460,9 @@ pub(crate) async fn create_room_route(body: Ruma<create_room::v3::Request>) -> R
// 8. Events implied by invite (and TODO: invite_3pid)
drop(state_lock);
for user_id in &body.invite {
_ = invite_helper(sender_user, user_id, &room_id, None, body.is_direct).await;
if let Err(e) = invite_helper(sender_user, user_id, &room_id, None, body.is_direct).await {
warn!(%e, "Failed to send invite");
}
}
// Homeserver specific stuff
@@ -815,7 +817,7 @@ pub(crate) async fn upgrade_room_route(body: Ruma<upgrade_room::v3::Request>) ->
// Modify the power levels in the old room to prevent sending of events and
// inviting new users
_ = services()
services()
.rooms
.timeline
.build_and_append_pdu(
+2 -4
View File
@@ -19,10 +19,8 @@ use ruma::{
use tracing::{error, log::warn};
use crate::{
service::{self, pdu::PduBuilder},
services,
utils::server_name::server_is_ours,
Error, Result, Ruma, RumaResponse,
service::{pdu::PduBuilder, server_is_ours},
services, Error, Result, Ruma, RumaResponse,
};
/// # `PUT /_matrix/client/*/rooms/{roomId}/state/{eventType}/{stateKey}`
+71 -154
View File
@@ -5,6 +5,7 @@ use std::{
time::Duration,
};
use conduit::PduCount;
use ruma::{
api::client::{
filter::{FilterDefinition, LazyLoadOptions},
@@ -25,15 +26,11 @@ use ruma::{
StateEventType, TimelineEventType,
},
serde::Raw,
uint, DeviceId, EventId, OwnedDeviceId, OwnedUserId, RoomId, UInt, UserId,
uint, DeviceId, EventId, OwnedUserId, RoomId, UInt, UserId,
};
use tokio::sync::watch::Sender;
use tracing::{debug, error, Instrument as _, Span};
use tracing::{error, Instrument as _, Span};
use crate::{
service::{pdu::EventHash, rooms::timeline::PduCount},
services, utils, Error, PduEvent, Result, Ruma, RumaResponse,
};
use crate::{service::pdu::EventHash, services, utils, Error, PduEvent, Result, Ruma, RumaResponse};
/// # `GET /_matrix/client/r0/sync`
///
@@ -73,10 +70,6 @@ use crate::{
/// For left rooms:
/// - If the user left after `since`: `prev_batch` token, empty state (TODO:
/// subset of the state at the point of the leave)
///
/// - Sync is handled in an async task, multiple requests from the same device
/// with the same
/// `since` will be cached
pub(crate) async fn sync_events_route(
body: Ruma<sync_events::v3::Request>,
) -> Result<sync_events::v3::Response, RumaResponse<UiaaResponse>> {
@@ -84,95 +77,6 @@ pub(crate) async fn sync_events_route(
let sender_device = body.sender_device.expect("user is authenticated");
let body = body.body;
let mut rx = match services()
.globals
.sync_receivers
.write()
.await
.entry((sender_user.clone(), sender_device.clone()))
{
Entry::Vacant(v) => {
let (tx, rx) = tokio::sync::watch::channel(None);
v.insert((body.since.clone(), rx.clone()));
tokio::spawn(sync_helper_wrapper(sender_user.clone(), sender_device.clone(), body, tx));
rx
},
Entry::Occupied(mut o) => {
if o.get().0 != body.since {
let (tx, rx) = tokio::sync::watch::channel(None);
o.insert((body.since.clone(), rx.clone()));
debug!("Sync started for {sender_user}");
tokio::spawn(sync_helper_wrapper(sender_user.clone(), sender_device.clone(), body, tx));
rx
} else {
o.get().1.clone()
}
},
};
let we_have_to_wait = rx.borrow().is_none();
if we_have_to_wait {
if let Err(e) = rx.changed().await {
error!("Error waiting for sync: {}", e);
}
}
let result = match rx
.borrow()
.as_ref()
.expect("When sync channel changes it's always set to some")
{
Ok(response) => Ok(response.clone()),
Err(error) => Err(error.to_response()),
};
result
}
async fn sync_helper_wrapper(
sender_user: OwnedUserId, sender_device: OwnedDeviceId, body: sync_events::v3::Request,
tx: Sender<Option<Result<sync_events::v3::Response>>>,
) {
let since = body.since.clone();
let r = sync_helper(sender_user.clone(), sender_device.clone(), body).await;
if let Ok((_, caching_allowed)) = r {
if !caching_allowed {
match services()
.globals
.sync_receivers
.write()
.await
.entry((sender_user, sender_device))
{
Entry::Occupied(o) => {
// Only remove if the device didn't start a different /sync already
if o.get().0 == since {
o.remove();
}
},
Entry::Vacant(_) => {},
}
}
}
_ = tx.send(Some(r.map(|(r, _)| r)));
}
async fn sync_helper(
sender_user: OwnedUserId,
sender_device: OwnedDeviceId,
body: sync_events::v3::Request,
// bool = caching allowed
) -> Result<(sync_events::v3::Response, bool), Error> {
// Presence update
if services().globals.allow_local_presence() {
services()
@@ -238,7 +142,7 @@ async fn sync_helper(
.collect::<Vec<_>>();
// Coalesce database writes for the remainder of this scope.
let _cork = services().globals.db.cork_and_flush()?;
let _cork = services().globals.cork_and_flush()?;
for room_id in all_joined_rooms {
let room_id = room_id?;
@@ -413,11 +317,14 @@ async fn sync_helper(
if duration.as_secs() > 30 {
duration = Duration::from_secs(30);
}
_ = tokio::time::timeout(duration, watcher).await;
Ok((response, false))
} else {
Ok((response, since != next_batch)) // Only cache if we made progress
#[allow(clippy::let_underscore_must_use)]
{
_ = tokio::time::timeout(duration, watcher).await;
}
}
Ok(response)
}
#[tracing::instrument(skip_all, fields(user_id = %sender_user, room_id = %room_id))]
@@ -845,8 +752,7 @@ async fn load_joined_room(
// Incremental /sync
let since_shortstatehash = since_shortstatehash.unwrap();
let mut state_events = Vec::new();
let mut lazy_loaded = HashSet::new();
let mut delta_state_events = Vec::new();
if since_shortstatehash != current_shortstatehash {
let current_state_ids = services()
@@ -867,55 +773,12 @@ async fn load_joined_room(
continue;
};
if pdu.kind == TimelineEventType::RoomMember {
match UserId::parse(
pdu.state_key
.as_ref()
.expect("State event has state key")
.clone(),
) {
Ok(state_key_userid) => {
lazy_loaded.insert(state_key_userid);
},
Err(e) => error!("Invalid state key for member event: {}", e),
}
}
state_events.push(pdu);
delta_state_events.push(pdu);
tokio::task::yield_now().await;
}
}
}
for (_, event) in &timeline_pdus {
if lazy_loaded.contains(&event.sender) {
continue;
}
if !services().rooms.lazy_loading.lazy_load_was_sent_before(
sender_user,
sender_device,
room_id,
&event.sender,
)? || lazy_load_send_redundant
{
if let Some(member_event) = services().rooms.state_accessor.room_state_get(
room_id,
&StateEventType::RoomMember,
event.sender.as_str(),
)? {
lazy_loaded.insert(event.sender.clone());
state_events.push(member_event);
}
}
}
services()
.rooms
.lazy_loading
.lazy_load_mark_sent(sender_user, sender_device, room_id, lazy_loaded, next_batchcount)
.await;
let encrypted_room = services()
.rooms
.state_accessor
@@ -931,12 +794,12 @@ async fn load_joined_room(
// Calculations:
let new_encrypted_room = encrypted_room && since_encryption.is_none();
let send_member_count = state_events
let send_member_count = delta_state_events
.iter()
.any(|event| event.kind == TimelineEventType::RoomMember);
if encrypted_room {
for state_event in &state_events {
for state_event in &delta_state_events {
if state_event.kind != TimelineEventType::RoomMember {
continue;
}
@@ -997,6 +860,57 @@ async fn load_joined_room(
(None, None, Vec::new())
};
let mut state_events = delta_state_events;
let mut lazy_loaded = HashSet::new();
// Mark all member events we're returning as lazy-loaded
for pdu in &state_events {
if pdu.kind == TimelineEventType::RoomMember {
match UserId::parse(
pdu.state_key
.as_ref()
.expect("State event has state key")
.clone(),
) {
Ok(state_key_userid) => {
lazy_loaded.insert(state_key_userid);
},
Err(e) => error!("Invalid state key for member event: {}", e),
}
}
}
// Fetch contextual member state events for events from the timeline, and
// mark them as lazy-loaded as well.
for (_, event) in &timeline_pdus {
if lazy_loaded.contains(&event.sender) {
continue;
}
if !services().rooms.lazy_loading.lazy_load_was_sent_before(
sender_user,
sender_device,
room_id,
&event.sender,
)? || lazy_load_send_redundant
{
if let Some(member_event) = services().rooms.state_accessor.room_state_get(
room_id,
&StateEventType::RoomMember,
event.sender.as_str(),
)? {
lazy_loaded.insert(event.sender.clone());
state_events.push(member_event);
}
}
}
services()
.rooms
.lazy_loading
.lazy_load_mark_sent(sender_user, sender_device, room_id, lazy_loaded, next_batchcount)
.await;
(
heroes,
joined_member_count,
@@ -1684,7 +1598,10 @@ pub(crate) async fn sync_events_v4_route(
if duration.as_secs() > 30 {
duration = Duration::from_secs(30);
}
_ = tokio::time::timeout(duration, watcher).await;
#[allow(clippy::let_underscore_must_use)]
{
_ = tokio::time::timeout(duration, watcher).await;
}
}
Ok(sync_events::v4::Response {
+1 -1
View File
@@ -8,7 +8,7 @@ use ruma::{
to_device::DeviceIdOrAllDevices,
};
use crate::{services, utils::user_id::user_is_local, Error, Result, Ruma};
use crate::{services, user_is_local, Error, Result, Ruma};
/// # `PUT /_matrix/client/r0/sendToDevice/{eventType}/{txnId}`
///
+21 -7
View File
@@ -45,12 +45,13 @@ pub(crate) async fn get_supported_versions_route(
],
unstable_features: BTreeMap::from_iter([
("org.matrix.e2e_cross_signing".to_owned(), true),
("org.matrix.msc2285.stable".to_owned(), true),
("uk.half-shot.msc2666.query_mutual_rooms".to_owned(), true),
("org.matrix.msc2836".to_owned(), true),
("org.matrix.msc2946".to_owned(), true),
("org.matrix.msc3026.busy_presence".to_owned(), true),
("org.matrix.msc3827".to_owned(), true),
("org.matrix.msc2285.stable".to_owned(), true), /* private read receipts (https://github.com/matrix-org/matrix-spec-proposals/pull/2285) */
("uk.half-shot.msc2666.query_mutual_rooms".to_owned(), true), /* query mutual rooms (https://github.com/matrix-org/matrix-spec-proposals/pull/2666) */
("org.matrix.msc2836".to_owned(), true), /* threading/threads (https://github.com/matrix-org/matrix-spec-proposals/pull/2836) */
("org.matrix.msc2946".to_owned(), true), /* spaces/hierarchy summaries (https://github.com/matrix-org/matrix-spec-proposals/pull/2946) */
("org.matrix.msc3026.busy_presence".to_owned(), true), /* busy presence status (https://github.com/matrix-org/matrix-spec-proposals/pull/3026) */
("org.matrix.msc3827".to_owned(), true), /* filtering of /publicRooms by room type (https://github.com/matrix-org/matrix-spec-proposals/pull/3827) */
("org.matrix.msc3575".to_owned(), true), /* sliding sync (https://github.com/matrix-org/matrix-spec-proposals/pull/3575/files#r1588877046) */
]),
};
@@ -154,7 +155,20 @@ pub(crate) async fn syncv3_client_server_json() -> Result<impl IntoResponse> {
/// `/_matrix/federation/v1/version`
pub(crate) async fn conduwuit_server_version() -> Result<impl IntoResponse> {
Ok(Json(serde_json::json!({
"name": "Conduwuit",
"name": "conduwuit",
"version": conduwuit_version(),
})))
}
/// # `GET /_conduwuit/local_user_count`
///
/// conduwuit-specific API to return the amount of users registered on this
/// homeserver. Endpoint is disabled if federation is disabled for privacy. This
/// only includes active users (not deactivated, no guests, etc)
pub(crate) async fn conduwuit_local_user_count() -> Result<impl IntoResponse> {
let user_count = services().users.list_local_users()?.len();
Ok(Json(serde_json::json!({
"count": user_count
})))
}
+15 -3
View File
@@ -1,3 +1,15 @@
pub(crate) mod client_server;
pub(crate) mod ruma_wrapper;
pub(crate) mod server_server;
pub mod client_server;
pub mod router;
mod ruma_wrapper;
pub mod server_server;
extern crate conduit_core as conduit;
extern crate conduit_service as service;
pub use client_server::membership::{join_room_by_id_helper, leave_all_rooms};
pub(crate) use conduit::{debug_error, debug_info, debug_warn, error::RumaResponse, utils, Error, Result};
pub(crate) use ruma_wrapper::Ruma;
pub(crate) use service::{pdu::PduEvent, services, user_is_local};
conduit::mod_ctor! {}
conduit::mod_dtor! {}
+37 -60
View File
@@ -1,21 +1,19 @@
use std::future::Future;
use axum::{
extract::FromRequestParts,
response::IntoResponse,
routing::{any, get, on, post, MethodFilter},
Router,
};
use conduit::{Error, Result, Server};
use http::{Method, Uri};
use ruma::api::{client::error::ErrorKind, IncomingRequest};
use crate::{
api::{client_server, server_server},
Config, Error, Result, Ruma, RumaResponse,
};
use crate::{client_server, server_server, Ruma, RumaResponse};
pub(crate) fn routes(config: &Config) -> Router {
let router = Router::new()
pub fn build(router: Router, server: &Server) -> Router {
let config = &server.config;
let router = router
.ruma_route(client_server::get_supported_versions_route)
.ruma_route(client_server::get_register_available_route)
.ruma_route(client_server::register_route)
@@ -29,6 +27,7 @@ pub(crate) fn routes(config: &Config) -> Router {
.ruma_route(client_server::third_party_route)
.ruma_route(client_server::request_3pid_management_token_via_email_route)
.ruma_route(client_server::request_3pid_management_token_via_msisdn_route)
.ruma_route(client_server::check_registration_token_validity)
.ruma_route(client_server::get_capabilities_route)
.ruma_route(client_server::get_pushrules_all_route)
.ruma_route(client_server::set_pushrule_route)
@@ -187,9 +186,7 @@ pub(crate) fn routes(config: &Config) -> Router {
.route("/_conduwuit/server_version", get(client_server::conduwuit_server_version))
.route("/_matrix/client/r0/rooms/:room_id/initialSync", get(initial_sync))
.route("/_matrix/client/v3/rooms/:room_id/initialSync", get(initial_sync))
.route("/client/server.json", get(client_server::syncv3_client_server_json))
.route("/", get(it_works))
.fallback(not_found);
.route("/client/server.json", get(client_server::syncv3_client_server_json));
if config.allow_federation {
router
@@ -222,24 +219,20 @@ pub(crate) fn routes(config: &Config) -> Router {
.ruma_route(server_server::claim_keys_route)
.ruma_route(server_server::get_hierarchy_route)
.ruma_route(server_server::well_known_server)
.route("/_conduwuit/local_user_count", get(client_server::conduwuit_local_user_count))
} else {
router
.route("/_matrix/federation/*path", any(federation_disabled))
.route("/.well-known/matrix/server", any(federation_disabled))
.route("/_matrix/key/*path", any(federation_disabled))
.route("/_conduwuit/local_user_count", any(federation_disabled))
}
}
async fn not_found(_uri: Uri) -> impl IntoResponse {
Error::BadRequest(ErrorKind::Unrecognized, "Unrecognized request")
}
async fn initial_sync(_uri: Uri) -> impl IntoResponse {
Error::BadRequest(ErrorKind::GuestAccessForbidden, "Guest access not implemented")
}
async fn it_works() -> &'static str { "hewwo from conduwuit woof!" }
async fn federation_disabled() -> impl IntoResponse { Error::bad_config("Federation is disabled.") }
trait RouterExt {
@@ -250,63 +243,47 @@ trait RouterExt {
}
impl RouterExt for Router {
#[inline(always)]
fn ruma_route<H, T>(self, handler: H) -> Self
where
H: RumaHandler<T>,
T: 'static,
{
handler.add_to_router(self)
handler.add_routes(self)
}
}
pub(crate) trait RumaHandler<T> {
// Can't transform to a handler without boxing or relying on the nightly-only
// impl-trait-in-traits feature. Moving a small amount of extra logic into the
// trait allows bypassing both.
fn add_to_router(self, router: Router) -> Router;
trait RumaHandler<T> {
fn add_routes(&self, router: Router) -> Router;
fn add_route(&self, router: Router, path: &str) -> Router;
}
macro_rules! impl_ruma_handler {
( $($ty:ident),* $(,)? ) => {
#[axum::async_trait]
#[allow(non_snake_case)]
impl<Req, E, F, Fut, $($ty,)*> RumaHandler<($($ty,)* Ruma<Req>,)> for F
where
Req: IncomingRequest + Send + 'static,
F: FnOnce($($ty,)* Ruma<Req>) -> Fut + Clone + Send + 'static,
Fut: Future<Output = Result<Req::OutgoingResponse, E>>
+ Send,
E: IntoResponse,
$( $ty: FromRequestParts<()> + Send + 'static, )*
{
fn add_to_router(self, mut router: Router) -> Router {
let meta = Req::METADATA;
let method_filter = method_to_filter(meta.method);
impl<Req, E, F, Fut> RumaHandler<Ruma<Req>> for F
where
Req: IncomingRequest + Send + 'static,
F: FnOnce(Ruma<Req>) -> Fut + Clone + Send + Sync + 'static,
Fut: Future<Output = Result<Req::OutgoingResponse, E>> + Send,
E: IntoResponse,
{
#[inline(always)]
fn add_routes(&self, router: Router) -> Router {
Req::METADATA
.history
.all_paths()
.fold(router, |router, path| self.add_route(router, path))
}
for path in meta.history.all_paths() {
let handler = self.clone();
router = router.route(path, on(method_filter, |$( $ty: $ty, )* req| async move {
handler($($ty,)* req).await.map(RumaResponse)
}))
}
router
}
}
};
#[inline(always)]
fn add_route(&self, router: Router, path: &str) -> Router {
let handle = self.clone();
let method = method_to_filter(Req::METADATA.method);
let action = |req| async { handle(req).await.map(RumaResponse) };
router.route(path, on(method, action))
}
}
impl_ruma_handler!();
impl_ruma_handler!(T1);
impl_ruma_handler!(T1, T2);
impl_ruma_handler!(T1, T2, T3);
impl_ruma_handler!(T1, T2, T3, T4);
impl_ruma_handler!(T1, T2, T3, T4, T5);
impl_ruma_handler!(T1, T2, T3, T4, T5, T6);
impl_ruma_handler!(T1, T2, T3, T4, T5, T6, T7);
impl_ruma_handler!(T1, T2, T3, T4, T5, T6, T7, T8);
#[inline]
fn method_to_filter(method: Method) -> MethodFilter {
match method {
Method::DELETE => MethodFilter::DELETE,
+252
View File
@@ -0,0 +1,252 @@
use std::collections::BTreeMap;
use axum::RequestPartsExt;
use axum_extra::{headers::Authorization, typed_header::TypedHeaderRejectionReason, TypedHeader};
use http::uri::PathAndQuery;
use ruma::{
api::{client::error::ErrorKind, AuthScheme, IncomingRequest},
CanonicalJsonValue, OwnedDeviceId, OwnedServerName, OwnedUserId, UserId,
};
use tracing::warn;
use super::{request::Request, xmatrix::XMatrix};
use crate::{service::appservice::RegistrationInfo, services, Error, Result};
enum Token {
Appservice(Box<RegistrationInfo>),
User((OwnedUserId, OwnedDeviceId)),
Invalid,
None,
}
pub(super) struct Auth {
pub(super) sender_user: Option<OwnedUserId>,
pub(super) sender_device: Option<OwnedDeviceId>,
pub(super) origin: Option<OwnedServerName>,
pub(super) appservice_info: Option<RegistrationInfo>,
}
pub(super) async fn auth<T>(request: &mut Request) -> Result<Auth>
where
T: IncomingRequest,
{
let metadata = T::METADATA;
let token = match &request.auth {
Some(TypedHeader(Authorization(bearer))) => Some(bearer.token()),
None => request.query.access_token.as_deref(),
};
let token = if let Some(token) = token {
if let Some(reg_info) = services().appservice.find_from_token(token).await {
Token::Appservice(Box::new(reg_info))
} else if let Some((user_id, device_id)) = services().users.find_from_token(token)? {
Token::User((user_id, OwnedDeviceId::from(device_id)))
} else {
Token::Invalid
}
} else {
Token::None
};
if metadata.authentication == AuthScheme::None {
match request.parts.uri.path() {
// TODO: can we check this better?
"/_matrix/client/v3/publicRooms" | "/_matrix/client/r0/publicRooms" => {
if !services()
.globals
.config
.allow_public_room_directory_without_auth
{
match token {
Token::Appservice(_) | Token::User(_) => {
// we should have validated the token above
// already
},
Token::None | Token::Invalid => {
return Err(Error::BadRequest(ErrorKind::MissingToken, "Missing or invalid access token."));
},
}
}
},
_ => {},
};
}
match (metadata.authentication, token) {
(_, Token::Invalid) => Err(Error::BadRequest(
ErrorKind::UnknownToken {
soft_logout: false,
},
"Unknown access token.",
)),
(AuthScheme::AccessToken, Token::Appservice(info)) => Ok(auth_appservice(request, info)?),
(AuthScheme::None | AuthScheme::AccessTokenOptional | AuthScheme::AppserviceToken, Token::Appservice(info)) => {
Ok(Auth {
sender_user: None,
sender_device: None,
origin: None,
appservice_info: Some(*info),
})
},
(AuthScheme::AccessToken, Token::None) => {
Err(Error::BadRequest(ErrorKind::MissingToken, "Missing access token."))
},
(
AuthScheme::AccessToken | AuthScheme::AccessTokenOptional | AuthScheme::None,
Token::User((user_id, device_id)),
) => Ok(Auth {
sender_user: Some(user_id),
sender_device: Some(device_id),
origin: None,
appservice_info: None,
}),
(AuthScheme::ServerSignatures, Token::None) => Ok(auth_server(request).await?),
(AuthScheme::None | AuthScheme::AppserviceToken | AuthScheme::AccessTokenOptional, Token::None) => Ok(Auth {
sender_user: None,
sender_device: None,
origin: None,
appservice_info: None,
}),
(AuthScheme::ServerSignatures, Token::Appservice(_) | Token::User(_)) => Err(Error::BadRequest(
ErrorKind::Unauthorized,
"Only server signatures should be used on this endpoint.",
)),
(AuthScheme::AppserviceToken, Token::User(_)) => Err(Error::BadRequest(
ErrorKind::Unauthorized,
"Only appservice access tokens should be used on this endpoint.",
)),
}
}
fn auth_appservice(request: &mut Request, info: Box<RegistrationInfo>) -> Result<Auth> {
let user_id = request
.query
.user_id
.clone()
.map_or_else(
|| {
UserId::parse_with_server_name(
info.registration.sender_localpart.as_str(),
services().globals.server_name(),
)
},
UserId::parse,
)
.map_err(|_| Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."))?;
if !info.is_user_match(&user_id) {
return Err(Error::BadRequest(ErrorKind::Exclusive, "User is not in namespace."));
}
if !services().users.exists(&user_id)? {
return Err(Error::BadRequest(ErrorKind::forbidden(), "User does not exist."));
}
Ok(Auth {
sender_user: Some(user_id),
sender_device: None,
origin: None,
appservice_info: Some(*info),
})
}
async fn auth_server(request: &mut Request) -> Result<Auth> {
if !services().globals.allow_federation() {
return Err(Error::bad_config("Federation is disabled."));
}
let TypedHeader(Authorization(x_matrix)) = request
.parts
.extract::<TypedHeader<Authorization<XMatrix>>>()
.await
.map_err(|e| {
warn!("Missing or invalid Authorization header: {e}");
let msg = match e.reason() {
TypedHeaderRejectionReason::Missing => "Missing Authorization header.",
TypedHeaderRejectionReason::Error(_) => "Invalid X-Matrix signatures.",
_ => "Unknown header-related error",
};
Error::BadRequest(ErrorKind::forbidden(), msg)
})?;
let origin_signatures = BTreeMap::from_iter([(x_matrix.key.clone(), CanonicalJsonValue::String(x_matrix.sig))]);
let signatures = BTreeMap::from_iter([(
x_matrix.origin.as_str().to_owned(),
CanonicalJsonValue::Object(origin_signatures),
)]);
let server_destination = services().globals.server_name().as_str().to_owned();
if let Some(destination) = x_matrix.destination.as_ref() {
if destination != &server_destination {
return Err(Error::BadRequest(ErrorKind::forbidden(), "Invalid authorization."));
}
}
let signature_uri = CanonicalJsonValue::String(
request
.parts
.uri
.path_and_query()
.unwrap_or(&PathAndQuery::from_static("/"))
.to_string(),
);
let mut request_map = BTreeMap::from_iter([
(
"method".to_owned(),
CanonicalJsonValue::String(request.parts.method.to_string()),
),
("uri".to_owned(), signature_uri),
(
"origin".to_owned(),
CanonicalJsonValue::String(x_matrix.origin.as_str().to_owned()),
),
("destination".to_owned(), CanonicalJsonValue::String(server_destination)),
("signatures".to_owned(), CanonicalJsonValue::Object(signatures)),
]);
if let Some(json_body) = &request.json {
request_map.insert("content".to_owned(), json_body.clone());
};
let keys_result = services()
.rooms
.event_handler
.fetch_signing_keys_for_server(&x_matrix.origin, vec![x_matrix.key.clone()])
.await;
let keys = keys_result.map_err(|e| {
warn!("Failed to fetch signing keys: {e}");
Error::BadRequest(ErrorKind::forbidden(), "Failed to fetch signing keys.")
})?;
let pub_key_map = BTreeMap::from_iter([(x_matrix.origin.as_str().to_owned(), keys)]);
match ruma::signatures::verify_json(&pub_key_map, &request_map) {
Ok(()) => Ok(Auth {
sender_user: None,
sender_device: None,
origin: Some(x_matrix.origin),
appservice_info: None,
}),
Err(e) => {
warn!("Failed to verify json request from {}: {e}\n{request_map:?}", x_matrix.origin);
if request.parts.uri.to_string().contains('@') {
warn!(
"Request uri contained '@' character. Make sure your reverse proxy gives Conduit the raw uri \
(apache: use nocanon)"
);
}
Err(Error::BadRequest(
ErrorKind::forbidden(),
"Failed to verify X-Matrix signatures.",
))
},
}
}
-399
View File
@@ -1,399 +0,0 @@
use std::{collections::BTreeMap, str};
use axum::{
async_trait,
extract::{FromRequest, Path},
response::{IntoResponse, Response},
RequestExt, RequestPartsExt,
};
use axum_extra::{
headers::{
authorization::{Bearer, Credentials},
Authorization,
},
typed_header::TypedHeaderRejectionReason,
TypedHeader,
};
use bytes::{BufMut, BytesMut};
use http::{uri::PathAndQuery, StatusCode};
use http_body_util::Full;
use hyper::Request;
use ruma::{
api::{client::error::ErrorKind, AuthScheme, IncomingRequest, OutgoingResponse},
CanonicalJsonValue, OwnedDeviceId, OwnedServerName, OwnedUserId, UserId,
};
use serde::Deserialize;
use tracing::{debug, error, trace, warn};
use super::{Ruma, RumaResponse};
use crate::{debug_warn, service::appservice::RegistrationInfo, services, Error, Result};
enum Token {
Appservice(Box<RegistrationInfo>),
User((OwnedUserId, OwnedDeviceId)),
Invalid,
None,
}
#[derive(Deserialize)]
struct QueryParams {
access_token: Option<String>,
user_id: Option<String>,
}
#[async_trait]
impl<T, S> FromRequest<S, axum::body::Body> for Ruma<T>
where
T: IncomingRequest,
{
type Rejection = Error;
#[allow(unused_qualifications)] // async traits
async fn from_request(req: Request<axum::body::Body>, _state: &S) -> Result<Self, Self::Rejection> {
let limited = req.with_limited_body();
let (mut parts, body) = limited.into_parts();
let mut body = axum::body::to_bytes(
body,
services()
.globals
.config
.max_request_size
.try_into()
.expect("failed to convert max request size"),
)
.await
.map_err(|_| Error::BadRequest(ErrorKind::MissingToken, "Missing token."))?;
let metadata = T::METADATA;
let auth_header: Option<TypedHeader<Authorization<Bearer>>> = parts.extract().await?;
let path_params: Path<Vec<String>> = parts.extract().await?;
let query = parts.uri.query().unwrap_or_default();
let query_params: QueryParams = match serde_html_form::from_str(query) {
Ok(params) => params,
Err(e) => {
error!(%query, "Failed to deserialize query parameters: {e}");
return Err(Error::BadRequest(ErrorKind::Unknown, "Failed to read query parameters"));
},
};
let token = match &auth_header {
Some(TypedHeader(Authorization(bearer))) => Some(bearer.token()),
None => query_params.access_token.as_deref(),
};
let token = if let Some(token) = token {
if let Some(reg_info) = services().appservice.find_from_token(token).await {
Token::Appservice(Box::new(reg_info))
} else if let Some((user_id, device_id)) = services().users.find_from_token(token)? {
Token::User((user_id, OwnedDeviceId::from(device_id)))
} else {
Token::Invalid
}
} else {
Token::None
};
if metadata.authentication == AuthScheme::None {
match parts.uri.path() {
// TODO: can we check this better?
"/_matrix/client/v3/publicRooms" | "/_matrix/client/r0/publicRooms" => {
if !services()
.globals
.config
.allow_public_room_directory_without_auth
{
match token {
Token::Appservice(_) | Token::User(_) => {
// we should have validated the token above
// already
},
Token::None | Token::Invalid => {
return Err(Error::BadRequest(
ErrorKind::MissingToken,
"Missing or invalid access token.",
));
},
}
}
},
_ => {},
};
}
let mut json_body = serde_json::from_slice::<CanonicalJsonValue>(&body).ok();
let (sender_user, sender_device, sender_servername, appservice_info) = match (metadata.authentication, token) {
(_, Token::Invalid) => {
return Err(Error::BadRequest(
ErrorKind::UnknownToken {
soft_logout: false,
},
"Unknown access token.",
))
},
(AuthScheme::AccessToken, Token::Appservice(info)) => {
let user_id = query_params
.user_id
.map_or_else(
|| {
UserId::parse_with_server_name(
info.registration.sender_localpart.as_str(),
services().globals.server_name(),
)
},
UserId::parse,
)
.map_err(|_| Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."))?;
if !info.is_user_match(&user_id) {
return Err(Error::BadRequest(ErrorKind::Exclusive, "User is not in namespace."));
}
if !services().users.exists(&user_id)? {
return Err(Error::BadRequest(ErrorKind::forbidden(), "User does not exist."));
}
(Some(user_id), None, None, Some(*info))
},
(
AuthScheme::None | AuthScheme::AccessTokenOptional | AuthScheme::AppserviceToken,
Token::Appservice(info),
) => (None, None, None, Some(*info)),
(AuthScheme::AccessToken, Token::None) => {
return Err(Error::BadRequest(ErrorKind::MissingToken, "Missing access token."));
},
(
AuthScheme::AccessToken | AuthScheme::AccessTokenOptional | AuthScheme::None,
Token::User((user_id, device_id)),
) => (Some(user_id), Some(device_id), None, None),
(AuthScheme::ServerSignatures, Token::None) => {
if !services().globals.allow_federation() {
return Err(Error::bad_config("Federation is disabled."));
}
let TypedHeader(Authorization(x_matrix)) = parts
.extract::<TypedHeader<Authorization<XMatrix>>>()
.await
.map_err(|e| {
warn!("Missing or invalid Authorization header: {e}");
let msg = match e.reason() {
TypedHeaderRejectionReason::Missing => "Missing Authorization header.",
TypedHeaderRejectionReason::Error(_) => "Invalid X-Matrix signatures.",
_ => "Unknown header-related error",
};
Error::BadRequest(ErrorKind::forbidden(), msg)
})?;
let origin_signatures =
BTreeMap::from_iter([(x_matrix.key.clone(), CanonicalJsonValue::String(x_matrix.sig))]);
let signatures = BTreeMap::from_iter([(
x_matrix.origin.as_str().to_owned(),
CanonicalJsonValue::Object(origin_signatures),
)]);
let server_destination = services().globals.server_name().as_str().to_owned();
if let Some(destination) = x_matrix.destination.as_ref() {
if destination != &server_destination {
return Err(Error::BadRequest(ErrorKind::forbidden(), "Invalid authorization."));
}
}
let signature_uri = CanonicalJsonValue::String(
parts
.uri
.path_and_query()
.unwrap_or(&PathAndQuery::from_static("/"))
.to_string(),
);
let mut request_map = BTreeMap::from_iter([
("method".to_owned(), CanonicalJsonValue::String(parts.method.to_string())),
("uri".to_owned(), signature_uri),
(
"origin".to_owned(),
CanonicalJsonValue::String(x_matrix.origin.as_str().to_owned()),
),
("destination".to_owned(), CanonicalJsonValue::String(server_destination)),
("signatures".to_owned(), CanonicalJsonValue::Object(signatures)),
]);
if let Some(json_body) = &json_body {
request_map.insert("content".to_owned(), json_body.clone());
};
let keys_result = services()
.rooms
.event_handler
.fetch_signing_keys_for_server(&x_matrix.origin, vec![x_matrix.key.clone()])
.await;
let keys = keys_result.map_err(|e| {
warn!("Failed to fetch signing keys: {e}");
Error::BadRequest(ErrorKind::forbidden(), "Failed to fetch signing keys.")
})?;
let pub_key_map = BTreeMap::from_iter([(x_matrix.origin.as_str().to_owned(), keys)]);
match ruma::signatures::verify_json(&pub_key_map, &request_map) {
Ok(()) => (None, None, Some(x_matrix.origin), None),
Err(e) => {
warn!("Failed to verify json request from {}: {e}\n{request_map:?}", x_matrix.origin);
if parts.uri.to_string().contains('@') {
warn!(
"Request uri contained '@' character. Make sure your reverse proxy gives Conduit the \
raw uri (apache: use nocanon)"
);
}
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Failed to verify X-Matrix signatures.",
));
},
}
},
(AuthScheme::None | AuthScheme::AppserviceToken | AuthScheme::AccessTokenOptional, Token::None) => {
(None, None, None, None)
},
(AuthScheme::ServerSignatures, Token::Appservice(_) | Token::User(_)) => {
return Err(Error::BadRequest(
ErrorKind::Unauthorized,
"Only server signatures should be used on this endpoint.",
));
},
(AuthScheme::AppserviceToken, Token::User(_)) => {
return Err(Error::BadRequest(
ErrorKind::Unauthorized,
"Only appservice access tokens should be used on this endpoint.",
));
},
};
let mut http_request = Request::builder().uri(parts.uri).method(parts.method);
*http_request.headers_mut().unwrap() = parts.headers;
if let Some(CanonicalJsonValue::Object(json_body)) = &mut json_body {
let user_id = sender_user.clone().unwrap_or_else(|| {
UserId::parse_with_server_name("", services().globals.server_name()).expect("we know this is valid")
});
let uiaa_request = json_body
.get("auth")
.and_then(|auth| auth.as_object())
.and_then(|auth| auth.get("session"))
.and_then(|session| session.as_str())
.and_then(|session| {
services().uiaa.get_uiaa_request(
&user_id,
&sender_device.clone().unwrap_or_else(|| "".into()),
session,
)
});
if let Some(CanonicalJsonValue::Object(initial_request)) = uiaa_request {
for (key, value) in initial_request {
json_body.entry(key).or_insert(value);
}
}
let mut buf = BytesMut::new().writer();
serde_json::to_writer(&mut buf, json_body).expect("value serialization can't fail");
body = buf.into_inner().freeze();
}
let http_request = http_request.body(&*body).unwrap();
debug!(
"{:?} {:?} {:?}",
http_request.method(),
http_request.uri(),
http_request.headers()
);
trace!("{:?} {:?} {:?}", http_request.method(), http_request.uri(), json_body);
let body = T::try_from_http_request(http_request, &path_params).map_err(|e| {
warn!("try_from_http_request failed: {e:?}",);
debug_warn!("JSON body: {:?}", json_body);
Error::BadRequest(ErrorKind::BadJson, "Failed to deserialize request.")
})?;
Ok(Ruma {
body,
sender_user,
sender_device,
sender_servername,
json_body,
appservice_info,
})
}
}
struct XMatrix {
origin: OwnedServerName,
destination: Option<String>,
key: String, // KeyName?
sig: String,
}
impl Credentials for XMatrix {
const SCHEME: &'static str = "X-Matrix";
fn decode(value: &http::HeaderValue) -> Option<Self> {
debug_assert!(
value.as_bytes().starts_with(b"X-Matrix "),
"HeaderValue to decode should start with \"X-Matrix ..\", received = {value:?}",
);
let parameters = str::from_utf8(&value.as_bytes()["X-Matrix ".len()..])
.ok()?
.trim_start();
let mut origin = None;
let mut destination = None;
let mut key = None;
let mut sig = None;
for entry in parameters.split_terminator(',') {
let (name, value) = entry.split_once('=')?;
// It's not at all clear why some fields are quoted and others not in the spec,
// let's simply accept either form for every field.
let value = value
.strip_prefix('"')
.and_then(|rest| rest.strip_suffix('"'))
.unwrap_or(value);
// FIXME: Catch multiple fields of the same name
match name {
"origin" => origin = Some(value.try_into().ok()?),
"key" => key = Some(value.to_owned()),
"sig" => sig = Some(value.to_owned()),
"destination" => destination = Some(value.to_owned()),
_ => debug!("Unexpected field `{name}` in X-Matrix Authorization header"),
}
}
Some(Self {
origin: origin?,
key: key?,
sig: sig?,
destination,
})
}
fn encode(&self) -> http::HeaderValue { todo!() }
}
impl<T: OutgoingResponse> IntoResponse for RumaResponse<T> {
fn into_response(self) -> Response {
match self.0.try_into_http_response::<BytesMut>() {
Ok(res) => res.map(BytesMut::freeze).map(Full::new).into_response(),
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}
}
+9 -16
View File
@@ -1,17 +1,21 @@
mod auth;
mod request;
mod xmatrix;
use std::ops::Deref;
use ruma::{api::client::uiaa::UiaaResponse, CanonicalJsonValue, OwnedDeviceId, OwnedServerName, OwnedUserId};
use ruma::{CanonicalJsonValue, OwnedDeviceId, OwnedServerName, OwnedUserId};
use crate::{service::appservice::RegistrationInfo, Error};
mod axum;
use crate::service::appservice::RegistrationInfo;
/// Extractor for Ruma request structs
pub(crate) struct Ruma<T> {
/// Request struct body
pub(crate) body: T,
pub(crate) sender_user: Option<OwnedUserId>,
pub(crate) sender_device: Option<OwnedDeviceId>,
pub(crate) sender_servername: Option<OwnedServerName>,
/// X-Matrix origin/server
pub(crate) origin: Option<OwnedServerName>,
pub(crate) json_body: Option<CanonicalJsonValue>, // This is None when body is not a valid string
pub(crate) appservice_info: Option<RegistrationInfo>,
}
@@ -21,14 +25,3 @@ impl<T> Deref for Ruma<T> {
fn deref(&self) -> &Self::Target { &self.body }
}
#[derive(Clone)]
pub(crate) struct RumaResponse<T>(pub(crate) T);
impl<T> From<T> for RumaResponse<T> {
fn from(t: T) -> Self { Self(t) }
}
impl From<Error> for RumaResponse<UiaaResponse> {
fn from(t: Error) -> Self { t.to_response() }
}
+149
View File
@@ -0,0 +1,149 @@
use std::{mem, str};
use axum::{
async_trait,
extract::{FromRequest, Path},
RequestExt, RequestPartsExt,
};
use axum_extra::{
headers::{authorization::Bearer, Authorization},
TypedHeader,
};
use bytes::{BufMut, Bytes, BytesMut};
use conduit::debug_warn;
use http::request::Parts;
use ruma::{
api::{client::error::ErrorKind, IncomingRequest},
CanonicalJsonValue, UserId,
};
use serde::Deserialize;
use tracing::{debug, trace, warn};
use super::{auth, auth::Auth, Ruma};
use crate::{services, Error, Result};
#[derive(Deserialize)]
pub(super) struct QueryParams {
pub(super) access_token: Option<String>,
pub(super) user_id: Option<String>,
}
pub(super) struct Request {
pub(super) auth: Option<TypedHeader<Authorization<Bearer>>>,
pub(super) path: Path<Vec<String>>,
pub(super) query: QueryParams,
pub(super) json: Option<CanonicalJsonValue>,
pub(super) body: Bytes,
pub(super) parts: Parts,
}
#[async_trait]
impl<T, S> FromRequest<S, axum::body::Body> for Ruma<T>
where
T: IncomingRequest,
{
type Rejection = Error;
async fn from_request(request: hyper::Request<axum::body::Body>, _state: &S) -> Result<Self, Self::Rejection> {
let mut request: Request = extract(request).await?;
let auth: Auth = auth::auth::<T>(&mut request).await?;
let body = make_body::<T>(&mut request, &auth)?;
Ok(Ruma {
body,
sender_user: auth.sender_user,
sender_device: auth.sender_device,
origin: auth.origin,
json_body: request.json,
appservice_info: auth.appservice_info,
})
}
}
fn make_body<T>(request: &mut Request, auth: &Auth) -> Result<T>
where
T: IncomingRequest,
{
let body = if let Some(CanonicalJsonValue::Object(json_body)) = &mut request.json {
let user_id = auth.sender_user.clone().unwrap_or_else(|| {
UserId::parse_with_server_name("", services().globals.server_name()).expect("we know this is valid")
});
let uiaa_request = json_body
.get("auth")
.and_then(|auth| auth.as_object())
.and_then(|auth| auth.get("session"))
.and_then(|session| session.as_str())
.and_then(|session| {
services().uiaa.get_uiaa_request(
&user_id,
&auth.sender_device.clone().unwrap_or_else(|| "".into()),
session,
)
});
if let Some(CanonicalJsonValue::Object(initial_request)) = uiaa_request {
for (key, value) in initial_request {
json_body.entry(key).or_insert(value);
}
}
let mut buf = BytesMut::new().writer();
serde_json::to_writer(&mut buf, &request.json).expect("value serialization can't fail");
buf.into_inner().freeze()
} else {
mem::take(&mut request.body)
};
let mut http_request = hyper::Request::builder()
.uri(request.parts.uri.clone())
.method(request.parts.method.clone());
*http_request.headers_mut().unwrap() = request.parts.headers.clone();
let http_request = http_request.body(body).unwrap();
debug!(
"{:?} {:?} {:?}",
http_request.method(),
http_request.uri(),
http_request.headers()
);
trace!("{:?} {:?} {:?}", http_request.method(), http_request.uri(), request.json);
let body = T::try_from_http_request(http_request, &request.path).map_err(|e| {
warn!("try_from_http_request failed: {e:?}",);
debug_warn!("JSON body: {:?}", request.json);
Error::BadRequest(ErrorKind::BadJson, "Failed to deserialize request.")
})?;
Ok(body)
}
async fn extract(request: hyper::Request<axum::body::Body>) -> Result<Request> {
let limited = request.with_limited_body();
let (mut parts, body) = limited.into_parts();
let auth = parts.extract().await?;
let path = parts.extract().await?;
let query = serde_html_form::from_str(parts.uri.query().unwrap_or_default())
.map_err(|_| Error::BadRequest(ErrorKind::Unknown, "Failed to read query parameters"))?;
let max_body_size = services()
.globals
.config
.max_request_size
.try_into()
.expect("failed to convert max request size");
let body = axum::body::to_bytes(body, max_body_size)
.await
.map_err(|_| Error::BadRequest(ErrorKind::TooLarge, "Request body too large"))?;
let json = serde_json::from_slice::<CanonicalJsonValue>(&body).ok();
Ok(Request {
auth,
path,
query,
json,
body,
parts,
})
}
+61
View File
@@ -0,0 +1,61 @@
use std::str;
use axum_extra::headers::authorization::Credentials;
use ruma::OwnedServerName;
use tracing::debug;
pub(crate) struct XMatrix {
pub(crate) origin: OwnedServerName,
pub(crate) destination: Option<String>,
pub(crate) key: String, // KeyName?
pub(crate) sig: String,
}
impl Credentials for XMatrix {
const SCHEME: &'static str = "X-Matrix";
fn decode(value: &http::HeaderValue) -> Option<Self> {
debug_assert!(
value.as_bytes().starts_with(b"X-Matrix "),
"HeaderValue to decode should start with \"X-Matrix ..\", received = {value:?}",
);
let parameters = str::from_utf8(&value.as_bytes()["X-Matrix ".len()..])
.ok()?
.trim_start();
let mut origin = None;
let mut destination = None;
let mut key = None;
let mut sig = None;
for entry in parameters.split_terminator(',') {
let (name, value) = entry.split_once('=')?;
// It's not at all clear why some fields are quoted and others not in the spec,
// let's simply accept either form for every field.
let value = value
.strip_prefix('"')
.and_then(|rest| rest.strip_suffix('"'))
.unwrap_or(value);
// FIXME: Catch multiple fields of the same name
match name {
"origin" => origin = Some(value.try_into().ok()?),
"key" => key = Some(value.to_owned()),
"sig" => sig = Some(value.to_owned()),
"destination" => destination = Some(value.to_owned()),
_ => debug!("Unexpected field `{name}` in X-Matrix Authorization header"),
}
}
Some(Self {
origin: origin?,
key: key?,
sig: sig?,
destination,
})
}
fn encode(&self) -> http::HeaderValue { todo!() }
}
+412 -222
View File
File diff suppressed because it is too large Load Diff
+128
View File
@@ -0,0 +1,128 @@
[package]
name = "conduit_core"
version.workspace = true
edition.workspace = true
[lib]
path = "mod.rs"
crate-type = [
"rlib",
# "dylib",
]
[features]
default = [
"rocksdb",
"io_uring",
"jemalloc",
"gzip_compression",
"zstd_compression",
"brotli_compression",
"sentry_telemetry",
"release_max_log_level",
]
dev_release_log_level = []
release_max_log_level = [
"tracing/max_level_trace",
"tracing/release_max_level_info",
"log/max_level_trace",
"log/release_max_level_info",
]
sqlite = [
"dep:rusqlite",
"dep:parking_lot",
"dep:thread_local",
]
rocksdb = [
"dep:rust-rocksdb",
]
jemalloc = [
"dep:tikv-jemalloc-sys",
"dep:tikv-jemalloc-ctl",
"dep:tikv-jemallocator",
"rust-rocksdb/jemalloc",
]
jemalloc_prof = [
"tikv-jemalloc-sys/profiling",
]
hardened_malloc = [
"dep:hardened_malloc-rs"
]
io_uring = [
"rust-rocksdb/io-uring",
]
zstd_compression = [
"rust-rocksdb/zstd",
]
gzip_compression = [
"reqwest/gzip",
]
brotli_compression = [
"reqwest/brotli",
]
perf_measurements = []
sentry_telemetry = []
[dependencies]
async-trait.workspace = true
axum-server.workspace = true
axum.workspace = true
base64.workspace = true
bytes.workspace = true
clap.workspace = true
cyborgtime.workspace = true
either.workspace = true
figment.workspace = true
futures-util.workspace = true
http-body-util.workspace = true
http.workspace = true
image.workspace = true
infer.workspace = true
ipaddress.workspace = true
itertools.workspace = true
libloading.workspace = true
log.workspace = true
lru-cache.workspace = true
parking_lot.optional = true
parking_lot.workspace = true
rand.workspace = true
regex.workspace = true
reqwest.workspace = true
ring.workspace = true
ruma.workspace = true
rusqlite.optional = true
rusqlite.workspace = true
rust-rocksdb.optional = true
rust-rocksdb.workspace = true
sanitize-filename.workspace = true
serde_json.workspace = true
serde_regex.workspace = true
serde.workspace = true
serde_yaml.workspace = true
sha-1.workspace = true
thiserror.workspace = true
thread_local.optional = true
thread_local.workspace = true
tikv-jemallocator.optional = true
tikv-jemallocator.workspace = true
tikv-jemalloc-ctl.optional = true
tikv-jemalloc-ctl.workspace = true
tikv-jemalloc-sys.optional = true
tikv-jemalloc-sys.workspace = true
tokio.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
url.workspace = true
zstd.optional = true
zstd.workspace = true
[target.'cfg(unix)'.dependencies]
nix.workspace = true
[target.'cfg(all(not(target_env = "msvc"), target_os = "linux"))'.dependencies]
hardened_malloc-rs.workspace = true
hardened_malloc-rs.optional = true
[lints]
workspace = true
+9
View File
@@ -0,0 +1,9 @@
//! Default allocator with no special features
/// Always returns the empty string
#[must_use]
pub fn memory_stats() -> String { Default::default() }
/// Always returns the empty string
#[must_use]
pub fn memory_usage() -> String { Default::default() }
+10
View File
@@ -0,0 +1,10 @@
#[global_allocator]
static HMALLOC: hardened_malloc_rs::HardenedMalloc = hardened_malloc_rs::HardenedMalloc;
#[must_use]
pub fn memory_usage() -> String {
String::default() //TODO: get usage
}
#[must_use]
pub fn memory_stats() -> String { "Extended statistics are not available from hardened_malloc.".to_owned() }
+4 -2
View File
@@ -7,7 +7,8 @@ use tikv_jemallocator as jemalloc;
#[global_allocator]
static JEMALLOC: jemalloc::Jemalloc = jemalloc::Jemalloc;
pub(crate) fn memory_usage() -> String {
#[must_use]
pub fn memory_usage() -> String {
use mallctl::stats;
let allocated = stats::allocated::read().unwrap_or_default() as f64 / 1024.0 / 1024.0;
let active = stats::active::read().unwrap_or_default() as f64 / 1024.0 / 1024.0;
@@ -21,7 +22,8 @@ pub(crate) fn memory_usage() -> String {
)
}
pub(crate) fn memory_stats() -> String {
#[must_use]
pub fn memory_stats() -> String {
const MAX_LENGTH: usize = 65536 - 4096;
let opts_s = "d";
+6 -6
View File
@@ -2,24 +2,24 @@
// jemalloc
#[cfg(all(not(target_env = "msvc"), feature = "jemalloc", not(feature = "hardened_malloc")))]
mod je;
pub mod je;
#[cfg(all(not(target_env = "msvc"), feature = "jemalloc", not(feature = "hardened_malloc")))]
pub(crate) use je::{memory_stats, memory_usage};
pub use je::{memory_stats, memory_usage};
// hardened_malloc
#[cfg(all(not(target_env = "msvc"), feature = "hardened_malloc", target_os = "linux", not(feature = "jemalloc")))]
mod hardened;
pub mod hardened;
#[cfg(all(not(target_env = "msvc"), feature = "hardened_malloc", target_os = "linux", not(feature = "jemalloc")))]
pub(crate) use hardened::{memory_stats, memory_usage};
pub use hardened::{memory_stats, memory_usage};
// default, enabled when none or multiple of the above are enabled
#[cfg(any(
not(any(feature = "jemalloc", feature = "hardened_malloc")),
all(feature = "jemalloc", feature = "hardened_malloc"),
))]
mod default;
pub mod default;
#[cfg(any(
not(any(feature = "jemalloc", feature = "hardened_malloc")),
all(feature = "jemalloc", feature = "hardened_malloc"),
))]
pub(crate) use default::{memory_stats, memory_usage};
pub use default::{memory_stats, memory_usage};
@@ -3,9 +3,9 @@ use std::path::Path; // not unix specific, just only for UNIX sockets stuff and
use tracing::{debug, error, info, warn};
use crate::{utils::error::Error, Config};
use crate::{error::Error, Config};
pub(crate) fn check(config: &Config) -> Result<(), Error> {
pub fn check(config: &Config) -> Result<(), Error> {
config.warn_deprecated();
config.warn_unknown_key();
+164 -162
View File
@@ -1,7 +1,7 @@
use std::{
collections::BTreeMap,
fmt::{self, Write as _},
net::{IpAddr, Ipv4Addr, SocketAddr},
net::{IpAddr, Ipv6Addr, SocketAddr},
path::PathBuf,
};
@@ -22,11 +22,12 @@ use serde::{de::IgnoredAny, Deserialize};
use tracing::{debug, error, warn};
use url::Url;
use self::{check::check, proxy::ProxyConfig};
use crate::utils::error::Error;
pub use self::check::check;
use self::proxy::ProxyConfig;
use crate::error::Error;
pub(crate) mod check;
mod proxy;
pub mod check;
pub mod proxy;
#[derive(Deserialize, Clone, Debug)]
#[serde(transparent)]
@@ -38,310 +39,310 @@ struct ListeningPort {
/// all the config options for conduwuit
#[derive(Clone, Debug, Deserialize)]
#[allow(clippy::struct_excessive_bools)]
pub(crate) struct Config {
pub struct Config {
/// [`IpAddr`] conduwuit will listen on (can be IPv4 or IPv6)
#[serde(default = "default_address")]
pub(crate) address: IpAddr,
pub address: IpAddr,
/// default TCP port(s) conduwuit will listen on
#[serde(default = "default_port")]
port: ListeningPort,
pub(crate) tls: Option<TlsConfig>,
pub(crate) unix_socket_path: Option<PathBuf>,
pub tls: Option<TlsConfig>,
pub unix_socket_path: Option<PathBuf>,
#[serde(default = "default_unix_socket_perms")]
pub(crate) unix_socket_perms: u32,
pub(crate) server_name: OwnedServerName,
pub unix_socket_perms: u32,
pub server_name: OwnedServerName,
#[serde(default = "default_database_backend")]
pub(crate) database_backend: String,
pub(crate) database_path: PathBuf,
pub(crate) database_backup_path: Option<PathBuf>,
pub database_backend: String,
pub database_path: PathBuf,
pub database_backup_path: Option<PathBuf>,
#[serde(default = "default_database_backups_to_keep")]
pub(crate) database_backups_to_keep: i16,
pub database_backups_to_keep: i16,
#[serde(default = "default_db_cache_capacity_mb")]
pub(crate) db_cache_capacity_mb: f64,
pub db_cache_capacity_mb: f64,
#[serde(default = "default_new_user_displayname_suffix")]
pub(crate) new_user_displayname_suffix: String,
pub new_user_displayname_suffix: String,
#[serde(default)]
pub(crate) allow_check_for_updates: bool,
pub allow_check_for_updates: bool,
#[serde(default = "default_pdu_cache_capacity")]
pub(crate) pdu_cache_capacity: u32,
pub pdu_cache_capacity: u32,
#[serde(default = "default_conduit_cache_capacity_modifier")]
pub(crate) conduit_cache_capacity_modifier: f64,
pub conduit_cache_capacity_modifier: f64,
#[serde(default = "default_auth_chain_cache_capacity")]
pub(crate) auth_chain_cache_capacity: u32,
pub auth_chain_cache_capacity: u32,
#[serde(default = "default_shorteventid_cache_capacity")]
pub(crate) shorteventid_cache_capacity: u32,
pub shorteventid_cache_capacity: u32,
#[serde(default = "default_eventidshort_cache_capacity")]
pub(crate) eventidshort_cache_capacity: u32,
pub eventidshort_cache_capacity: u32,
#[serde(default = "default_shortstatekey_cache_capacity")]
pub(crate) shortstatekey_cache_capacity: u32,
pub shortstatekey_cache_capacity: u32,
#[serde(default = "default_statekeyshort_cache_capacity")]
pub(crate) statekeyshort_cache_capacity: u32,
pub statekeyshort_cache_capacity: u32,
#[serde(default = "default_server_visibility_cache_capacity")]
pub(crate) server_visibility_cache_capacity: u32,
pub server_visibility_cache_capacity: u32,
#[serde(default = "default_user_visibility_cache_capacity")]
pub(crate) user_visibility_cache_capacity: u32,
pub user_visibility_cache_capacity: u32,
#[serde(default = "default_stateinfo_cache_capacity")]
pub(crate) stateinfo_cache_capacity: u32,
pub stateinfo_cache_capacity: u32,
#[serde(default = "default_roomid_spacehierarchy_cache_capacity")]
pub(crate) roomid_spacehierarchy_cache_capacity: u32,
pub roomid_spacehierarchy_cache_capacity: u32,
#[serde(default = "default_cleanup_second_interval")]
pub(crate) cleanup_second_interval: u32,
pub cleanup_second_interval: u32,
#[serde(default = "default_dns_cache_entries")]
pub(crate) dns_cache_entries: u32,
pub dns_cache_entries: u32,
#[serde(default = "default_dns_min_ttl")]
pub(crate) dns_min_ttl: u64,
pub dns_min_ttl: u64,
#[serde(default = "default_dns_min_ttl_nxdomain")]
pub(crate) dns_min_ttl_nxdomain: u64,
pub dns_min_ttl_nxdomain: u64,
#[serde(default = "default_dns_attempts")]
pub(crate) dns_attempts: u16,
pub dns_attempts: u16,
#[serde(default = "default_dns_timeout")]
pub(crate) dns_timeout: u64,
pub dns_timeout: u64,
#[serde(default = "true_fn")]
pub(crate) dns_tcp_fallback: bool,
pub dns_tcp_fallback: bool,
#[serde(default = "true_fn")]
pub(crate) query_all_nameservers: bool,
pub query_all_nameservers: bool,
#[serde(default)]
pub(crate) query_over_tcp_only: bool,
pub query_over_tcp_only: bool,
#[serde(default = "default_ip_lookup_strategy")]
pub(crate) ip_lookup_strategy: u8,
pub ip_lookup_strategy: u8,
#[serde(default = "default_max_request_size")]
pub(crate) max_request_size: u32,
pub max_request_size: u32,
#[serde(default = "default_max_fetch_prev_events")]
pub(crate) max_fetch_prev_events: u16,
pub max_fetch_prev_events: u16,
#[serde(default = "default_request_conn_timeout")]
pub(crate) request_conn_timeout: u64,
pub request_conn_timeout: u64,
#[serde(default = "default_request_timeout")]
pub(crate) request_timeout: u64,
pub request_timeout: u64,
#[serde(default = "default_request_total_timeout")]
pub(crate) request_total_timeout: u64,
pub request_total_timeout: u64,
#[serde(default = "default_request_idle_timeout")]
pub(crate) request_idle_timeout: u64,
pub request_idle_timeout: u64,
#[serde(default = "default_request_idle_per_host")]
pub(crate) request_idle_per_host: u16,
pub request_idle_per_host: u16,
#[serde(default = "default_well_known_conn_timeout")]
pub(crate) well_known_conn_timeout: u64,
pub well_known_conn_timeout: u64,
#[serde(default = "default_well_known_timeout")]
pub(crate) well_known_timeout: u64,
pub well_known_timeout: u64,
#[serde(default = "default_federation_timeout")]
pub(crate) federation_timeout: u64,
pub federation_timeout: u64,
#[serde(default = "default_federation_idle_timeout")]
pub(crate) federation_idle_timeout: u64,
pub federation_idle_timeout: u64,
#[serde(default = "default_federation_idle_per_host")]
pub(crate) federation_idle_per_host: u16,
pub federation_idle_per_host: u16,
#[serde(default = "default_sender_timeout")]
pub(crate) sender_timeout: u64,
pub sender_timeout: u64,
#[serde(default = "default_sender_idle_timeout")]
pub(crate) sender_idle_timeout: u64,
pub sender_idle_timeout: u64,
#[serde(default = "default_sender_retry_backoff_limit")]
pub(crate) sender_retry_backoff_limit: u64,
pub sender_retry_backoff_limit: u64,
#[serde(default = "default_appservice_timeout")]
pub(crate) appservice_timeout: u64,
pub appservice_timeout: u64,
#[serde(default = "default_appservice_idle_timeout")]
pub(crate) appservice_idle_timeout: u64,
pub appservice_idle_timeout: u64,
#[serde(default = "default_pusher_idle_timeout")]
pub(crate) pusher_idle_timeout: u64,
pub pusher_idle_timeout: u64,
#[serde(default)]
pub(crate) allow_registration: bool,
pub allow_registration: bool,
#[serde(default)]
pub(crate) yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse: bool,
pub(crate) registration_token: Option<String>,
pub yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse: bool,
pub registration_token: Option<String>,
#[serde(default = "true_fn")]
pub(crate) allow_encryption: bool,
pub allow_encryption: bool,
#[serde(default = "true_fn")]
pub(crate) allow_federation: bool,
pub allow_federation: bool,
#[serde(default)]
pub(crate) allow_public_room_directory_over_federation: bool,
pub allow_public_room_directory_over_federation: bool,
#[serde(default)]
pub(crate) allow_public_room_directory_without_auth: bool,
pub allow_public_room_directory_without_auth: bool,
#[serde(default)]
pub(crate) lockdown_public_room_directory: bool,
pub lockdown_public_room_directory: bool,
#[serde(default)]
pub(crate) allow_device_name_federation: bool,
pub allow_device_name_federation: bool,
#[serde(default = "true_fn")]
pub(crate) allow_profile_lookup_federation_requests: bool,
pub allow_profile_lookup_federation_requests: bool,
#[serde(default = "true_fn")]
pub(crate) allow_room_creation: bool,
pub allow_room_creation: bool,
#[serde(default = "true_fn")]
pub(crate) allow_unstable_room_versions: bool,
pub allow_unstable_room_versions: bool,
#[serde(default = "default_default_room_version")]
pub(crate) default_room_version: RoomVersionId,
pub default_room_version: RoomVersionId,
#[serde(default)]
pub(crate) well_known: WellKnownConfig,
pub well_known: WellKnownConfig,
#[serde(default)]
#[cfg(feature = "perf_measurements")]
pub(crate) allow_jaeger: bool,
pub allow_jaeger: bool,
#[serde(default)]
#[cfg(feature = "perf_measurements")]
pub(crate) tracing_flame: bool,
pub tracing_flame: bool,
#[serde(default = "default_tracing_flame_filter")]
#[cfg(feature = "perf_measurements")]
pub(crate) tracing_flame_filter: String,
pub tracing_flame_filter: String,
#[serde(default = "default_tracing_flame_output_path")]
#[cfg(feature = "perf_measurements")]
pub(crate) tracing_flame_output_path: String,
pub tracing_flame_output_path: String,
#[serde(default)]
pub(crate) proxy: ProxyConfig,
pub(crate) jwt_secret: Option<String>,
pub proxy: ProxyConfig,
pub jwt_secret: Option<String>,
#[serde(default = "default_trusted_servers")]
pub(crate) trusted_servers: Vec<OwnedServerName>,
pub trusted_servers: Vec<OwnedServerName>,
#[serde(default = "true_fn")]
pub(crate) query_trusted_key_servers_first: bool,
pub query_trusted_key_servers_first: bool,
#[serde(default = "default_log")]
pub(crate) log: String,
pub log: String,
#[serde(default)]
pub(crate) turn_username: String,
pub turn_username: String,
#[serde(default)]
pub(crate) turn_password: String,
pub turn_password: String,
#[serde(default = "Vec::new")]
pub(crate) turn_uris: Vec<String>,
pub turn_uris: Vec<String>,
#[serde(default)]
pub(crate) turn_secret: String,
pub turn_secret: String,
#[serde(default = "default_turn_ttl")]
pub(crate) turn_ttl: u64,
pub turn_ttl: u64,
#[serde(default = "Vec::new")]
pub(crate) auto_join_rooms: Vec<OwnedRoomId>,
pub auto_join_rooms: Vec<OwnedRoomId>,
#[serde(default)]
pub(crate) auto_deactivate_banned_room_attempts: bool,
pub auto_deactivate_banned_room_attempts: bool,
#[serde(default = "default_rocksdb_log_level")]
pub(crate) rocksdb_log_level: String,
pub rocksdb_log_level: String,
#[serde(default)]
pub(crate) rocksdb_log_stderr: bool,
pub rocksdb_log_stderr: bool,
#[serde(default = "default_rocksdb_max_log_file_size")]
pub(crate) rocksdb_max_log_file_size: usize,
pub rocksdb_max_log_file_size: usize,
#[serde(default = "default_rocksdb_log_time_to_roll")]
pub(crate) rocksdb_log_time_to_roll: usize,
pub rocksdb_log_time_to_roll: usize,
#[serde(default)]
pub(crate) rocksdb_optimize_for_spinning_disks: bool,
pub rocksdb_optimize_for_spinning_disks: bool,
#[serde(default = "true_fn")]
pub(crate) rocksdb_direct_io: bool,
pub rocksdb_direct_io: bool,
#[serde(default = "default_rocksdb_parallelism_threads")]
pub(crate) rocksdb_parallelism_threads: usize,
pub rocksdb_parallelism_threads: usize,
#[serde(default = "default_rocksdb_max_log_files")]
pub(crate) rocksdb_max_log_files: usize,
pub rocksdb_max_log_files: usize,
#[serde(default = "default_rocksdb_compression_algo")]
pub(crate) rocksdb_compression_algo: String,
pub rocksdb_compression_algo: String,
#[serde(default = "default_rocksdb_compression_level")]
pub(crate) rocksdb_compression_level: i32,
pub rocksdb_compression_level: i32,
#[serde(default = "default_rocksdb_bottommost_compression_level")]
pub(crate) rocksdb_bottommost_compression_level: i32,
pub rocksdb_bottommost_compression_level: i32,
#[serde(default)]
pub(crate) rocksdb_bottommost_compression: bool,
pub rocksdb_bottommost_compression: bool,
#[serde(default = "default_rocksdb_recovery_mode")]
pub(crate) rocksdb_recovery_mode: u8,
pub rocksdb_recovery_mode: u8,
#[serde(default)]
pub(crate) rocksdb_repair: bool,
pub rocksdb_repair: bool,
#[serde(default)]
pub(crate) rocksdb_read_only: bool,
pub rocksdb_read_only: bool,
#[serde(default)]
pub(crate) rocksdb_periodic_cleanup: bool,
pub rocksdb_periodic_cleanup: bool,
#[serde(default)]
pub(crate) rocksdb_compaction_prio_idle: bool,
pub rocksdb_compaction_prio_idle: bool,
#[serde(default = "true_fn")]
pub(crate) rocksdb_compaction_ioprio_idle: bool,
pub rocksdb_compaction_ioprio_idle: bool,
pub(crate) emergency_password: Option<String>,
pub emergency_password: Option<String>,
#[serde(default = "default_notification_push_path")]
pub(crate) notification_push_path: String,
pub notification_push_path: String,
#[serde(default = "true_fn")]
pub(crate) allow_local_presence: bool,
pub allow_local_presence: bool,
#[serde(default = "true_fn")]
pub(crate) allow_incoming_presence: bool,
pub allow_incoming_presence: bool,
#[serde(default = "true_fn")]
pub(crate) allow_outgoing_presence: bool,
pub allow_outgoing_presence: bool,
#[serde(default = "default_presence_idle_timeout_s")]
pub(crate) presence_idle_timeout_s: u64,
pub presence_idle_timeout_s: u64,
#[serde(default = "default_presence_offline_timeout_s")]
pub(crate) presence_offline_timeout_s: u64,
pub presence_offline_timeout_s: u64,
#[serde(default = "true_fn")]
pub(crate) presence_timeout_remote_users: bool,
pub presence_timeout_remote_users: bool,
#[serde(default = "true_fn")]
pub(crate) allow_incoming_read_receipts: bool,
pub allow_incoming_read_receipts: bool,
#[serde(default = "true_fn")]
pub(crate) allow_outgoing_read_receipts: bool,
pub allow_outgoing_read_receipts: bool,
#[serde(default = "true_fn")]
pub(crate) allow_outgoing_typing: bool,
pub allow_outgoing_typing: bool,
#[serde(default = "true_fn")]
pub(crate) allow_incoming_typing: bool,
pub allow_incoming_typing: bool,
#[serde(default = "default_typing_federation_timeout_s")]
pub(crate) typing_federation_timeout_s: u64,
pub typing_federation_timeout_s: u64,
#[serde(default = "default_typing_client_timeout_min_s")]
pub(crate) typing_client_timeout_min_s: u64,
pub typing_client_timeout_min_s: u64,
#[serde(default = "default_typing_client_timeout_max_s")]
pub(crate) typing_client_timeout_max_s: u64,
pub typing_client_timeout_max_s: u64,
#[serde(default)]
pub(crate) zstd_compression: bool,
pub zstd_compression: bool,
#[serde(default)]
pub(crate) gzip_compression: bool,
pub gzip_compression: bool,
#[serde(default)]
pub(crate) brotli_compression: bool,
pub brotli_compression: bool,
#[serde(default)]
pub(crate) allow_guest_registration: bool,
pub allow_guest_registration: bool,
#[serde(default)]
pub(crate) log_guest_registrations: bool,
pub log_guest_registrations: bool,
#[serde(default)]
pub(crate) allow_guests_auto_join_rooms: bool,
pub allow_guests_auto_join_rooms: bool,
#[serde(default = "Vec::new")]
pub(crate) prevent_media_downloads_from: Vec<OwnedServerName>,
pub prevent_media_downloads_from: Vec<OwnedServerName>,
#[serde(default = "Vec::new")]
pub(crate) forbidden_remote_server_names: Vec<OwnedServerName>,
pub forbidden_remote_server_names: Vec<OwnedServerName>,
#[serde(default = "Vec::new")]
pub(crate) forbidden_remote_room_directory_server_names: Vec<OwnedServerName>,
pub forbidden_remote_room_directory_server_names: Vec<OwnedServerName>,
#[serde(default = "default_ip_range_denylist")]
pub(crate) ip_range_denylist: Vec<String>,
pub ip_range_denylist: Vec<String>,
#[serde(default = "Vec::new")]
pub(crate) url_preview_domain_contains_allowlist: Vec<String>,
pub url_preview_domain_contains_allowlist: Vec<String>,
#[serde(default = "Vec::new")]
pub(crate) url_preview_domain_explicit_allowlist: Vec<String>,
pub url_preview_domain_explicit_allowlist: Vec<String>,
#[serde(default = "Vec::new")]
pub(crate) url_preview_domain_explicit_denylist: Vec<String>,
pub url_preview_domain_explicit_denylist: Vec<String>,
#[serde(default = "Vec::new")]
pub(crate) url_preview_url_contains_allowlist: Vec<String>,
pub url_preview_url_contains_allowlist: Vec<String>,
#[serde(default = "default_url_preview_max_spider_size")]
pub(crate) url_preview_max_spider_size: usize,
pub url_preview_max_spider_size: usize,
#[serde(default)]
pub(crate) url_preview_check_root_domain: bool,
pub url_preview_check_root_domain: bool,
#[serde(default = "RegexSet::empty")]
#[serde(with = "serde_regex")]
pub(crate) forbidden_alias_names: RegexSet,
pub forbidden_alias_names: RegexSet,
#[serde(default = "RegexSet::empty")]
#[serde(with = "serde_regex")]
pub(crate) forbidden_usernames: RegexSet,
pub forbidden_usernames: RegexSet,
#[serde(default = "true_fn")]
pub(crate) startup_netburst: bool,
pub startup_netburst: bool,
#[serde(default = "default_startup_netburst_keep")]
pub(crate) startup_netburst_keep: i64,
pub startup_netburst_keep: i64,
#[serde(default)]
pub(crate) block_non_admin_invites: bool,
pub block_non_admin_invites: bool,
#[serde(default)]
pub(crate) sentry: bool,
pub sentry: bool,
#[serde(default = "default_sentry_endpoint")]
pub(crate) sentry_endpoint: Option<Url>,
pub sentry_endpoint: Option<Url>,
#[serde(default)]
pub(crate) sentry_send_server_name: bool,
pub sentry_send_server_name: bool,
#[serde(default = "default_sentry_traces_sample_rate")]
pub(crate) sentry_traces_sample_rate: f32,
pub sentry_traces_sample_rate: f32,
#[serde(flatten)]
#[allow(clippy::zero_sized_map_values)] // this is a catchall, the map shouldn't be zero at runtime
@@ -349,24 +350,24 @@ pub(crate) struct Config {
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct TlsConfig {
pub(crate) certs: String,
pub(crate) key: String,
pub struct TlsConfig {
pub certs: String,
pub key: String,
#[serde(default)]
/// Whether to listen and allow for HTTP and HTTPS connections (insecure!)
/// Only works / does something if the `axum_dual_protocol` feature flag was
/// built
pub(crate) dual_protocol: bool,
pub dual_protocol: bool,
}
#[derive(Clone, Debug, Deserialize, Default)]
pub(crate) struct WellKnownConfig {
pub(crate) client: Option<Url>,
pub(crate) server: Option<OwnedServerName>,
pub(crate) support_page: Option<Url>,
pub(crate) support_role: Option<ContactRole>,
pub(crate) support_email: Option<String>,
pub(crate) support_mxid: Option<OwnedUserId>,
pub struct WellKnownConfig {
pub client: Option<Url>,
pub server: Option<OwnedServerName>,
pub support_page: Option<Url>,
pub support_role: Option<ContactRole>,
pub support_email: Option<String>,
pub support_mxid: Option<OwnedUserId>,
}
const DEPRECATED_KEYS: &[&str] = &[
@@ -382,7 +383,7 @@ const DEPRECATED_KEYS: &[&str] = &[
impl Config {
/// Initialize config
pub(crate) fn new(path: Option<PathBuf>) -> Result<Self, Error> {
pub fn new(path: Option<PathBuf>) -> Result<Self, Error> {
let raw_config = if let Some(config_file_env) = Env::var("CONDUIT_CONFIG") {
Figment::new()
.merge(Toml::file(config_file_env).nested())
@@ -469,7 +470,7 @@ impl Config {
}
#[must_use]
pub(crate) fn get_bind_addrs(&self) -> Vec<SocketAddr> {
pub fn get_bind_addrs(&self) -> Vec<SocketAddr> {
match &self.port.ports {
Left(port) => {
// Left is only 1 value, so make a vec with 1 value only
@@ -489,7 +490,7 @@ impl Config {
}
}
pub(crate) fn check(&self) -> Result<(), Error> { check(self) }
pub fn check(&self) -> Result<(), Error> { check(self) }
}
impl fmt::Display for Config {
@@ -865,7 +866,7 @@ impl fmt::Display for Config {
let mut msg: String = "Active config values:\n\n".to_owned();
for line in lines.into_iter().enumerate() {
let _ = writeln!(msg, "{}: {}", line.1 .0, line.1 .1);
writeln!(msg, "{}: {}", line.1 .0, line.1 .1).expect("should be able to write to string buffer");
}
write!(f, "{msg}")
@@ -874,7 +875,7 @@ impl fmt::Display for Config {
fn true_fn() -> bool { true }
fn default_address() -> IpAddr { Ipv4Addr::LOCALHOST.into() }
fn default_address() -> IpAddr { Ipv6Addr::LOCALHOST.into() }
fn default_port() -> ListeningPort {
ListeningPort {
@@ -1027,7 +1028,8 @@ fn default_rocksdb_compression_level() -> i32 { 32767 }
fn default_rocksdb_bottommost_compression_level() -> i32 { 32767 }
// I know, it's a great name
pub(crate) fn default_default_room_version() -> RoomVersionId { RoomVersionId::V10 }
#[must_use]
pub fn default_default_room_version() -> RoomVersionId { RoomVersionId::V10 }
fn default_ip_range_denylist() -> Vec<String> {
vec![
@@ -30,7 +30,7 @@ use crate::Result;
/// `ordinary.onion`, `matrix.myspecial.onion`, but not `hello.myspecial.onion`.
#[derive(Clone, Default, Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ProxyConfig {
pub enum ProxyConfig {
#[default]
None,
Global {
@@ -40,7 +40,7 @@ pub(crate) enum ProxyConfig {
ByDomain(Vec<PartialProxyConfig>),
}
impl ProxyConfig {
pub(crate) fn to_proxy(&self) -> Result<Option<Proxy>> {
pub fn to_proxy(&self) -> Result<Option<Proxy>> {
Ok(match self.clone() {
ProxyConfig::None => None,
ProxyConfig::Global {
@@ -55,7 +55,7 @@ impl ProxyConfig {
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct PartialProxyConfig {
pub struct PartialProxyConfig {
#[serde(deserialize_with = "crate::utils::deserialize_from_str")]
url: Url,
#[serde(default)]
@@ -64,7 +64,8 @@ pub(crate) struct PartialProxyConfig {
exclude: Vec<WildCardedDomain>,
}
impl PartialProxyConfig {
pub(crate) fn for_url(&self, url: &Url) -> Option<&Url> {
#[must_use]
pub fn for_url(&self, url: &Url) -> Option<&Url> {
let domain = url.domain()?;
let mut included_because = None; // most specific reason it was included
let mut excluded_because = None; // most specific reason it was excluded
+32
View File
@@ -1,3 +1,7 @@
#![allow(dead_code)] // this is a developer's toolbox
use std::{panic, panic::PanicInfo};
/// Log event at given level in debug-mode (when debug-assertions are enabled).
/// In release-mode it becomes DEBUG level, and possibly subject to elision.
///
@@ -43,3 +47,31 @@ macro_rules! debug_info {
$crate::debug_event!(tracing::Level::INFO, $($x)+ );
}
}
pub fn set_panic_trap() {
let next = panic::take_hook();
panic::set_hook(Box::new(move |info| {
panic_handler(info, &next);
}));
}
#[inline(always)]
fn panic_handler(info: &PanicInfo<'_>, next: &dyn Fn(&PanicInfo<'_>)) {
trap();
next(info);
}
#[inline(always)]
pub fn trap() {
#[cfg(core_intrinsics)]
//SAFETY: embeds llvm intrinsic for hardware breakpoint
unsafe {
std::intrinsics::breakpoint();
}
#[cfg(all(not(core_intrinsics), target_arch = "x86_64"))]
//SAFETY: embeds instruction for hardware breakpoint
unsafe {
std::arch::asm!("int3");
}
}
+79 -53
View File
@@ -1,10 +1,16 @@
use std::{convert::Infallible, fmt};
use axum::response::{IntoResponse, Response};
use bytes::BytesMut;
use http::StatusCode;
use http_body_util::Full;
use ruma::{
api::client::{
error::{Error as RumaError, ErrorBody, ErrorKind},
uiaa::{UiaaInfo, UiaaResponse},
api::{
client::{
error::{Error as RumaError, ErrorBody, ErrorKind},
uiaa::{UiaaInfo, UiaaResponse},
},
OutgoingResponse,
},
OwnedServerName,
};
@@ -15,12 +21,10 @@ use ErrorKind::{
TooLarge, Unauthorized, Unknown, UnknownToken, Unrecognized, UserDeactivated, WrongRoomKeysVersion,
};
use crate::RumaResponse;
pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Error)]
pub(crate) enum Error {
pub enum Error {
#[cfg(feature = "sqlite")]
#[error("There was a problem with the connection to the sqlite database: {source}")]
Sqlite {
@@ -33,7 +37,7 @@ pub(crate) enum Error {
#[from]
source: rust_rocksdb::Error,
},
#[error("Could not generate an image.")]
#[error("Could not generate an image: {source}")]
Image {
#[from]
source: image::error::ImageError,
@@ -48,14 +52,14 @@ pub(crate) enum Error {
#[from]
source: regex::Error,
},
#[error("{0}")]
#[error("Remote server {0} responded with: {1}")]
Federation(OwnedServerName, RumaError),
#[error("Could not do this io: {source}")]
Io {
#[from]
source: std::io::Error,
},
#[error("There was a problem with your configuration file: {0}")]
#[error("There was a problem with your configuration: {0}")]
BadConfig(String),
#[error("{0}")]
BadServerResponse(&'static str),
@@ -83,17 +87,70 @@ pub(crate) enum Error {
}
impl Error {
pub(crate) fn bad_database(message: &'static str) -> Self {
pub fn bad_database(message: &'static str) -> Self {
error!("BadDatabase: {}", message);
Self::BadDatabase(message)
}
pub(crate) fn bad_config(message: &str) -> Self {
pub fn bad_config(message: &str) -> Self {
error!("BadConfig: {}", message);
Self::BadConfig(message.to_owned())
}
pub(crate) fn to_response(&self) -> RumaResponse<UiaaResponse> {
/// Returns the Matrix error code / error kind
pub fn error_code(&self) -> ErrorKind {
if let Self::Federation(_, error) = self {
return error.error_kind().unwrap_or_else(|| &Unknown).clone();
}
match self {
Self::BadRequest(kind, _) => kind.clone(),
_ => Unknown,
}
}
/// Sanitizes public-facing errors that can leak sensitive information.
pub fn sanitized_error(&self) -> String {
let db_error = String::from("Database or I/O error occurred.");
match self {
#[cfg(feature = "sqlite")]
Self::Sqlite {
..
} => db_error,
#[cfg(feature = "rocksdb")]
Self::RocksDb {
..
} => db_error,
Self::Io {
..
} => db_error,
_ => self.to_string(),
}
}
}
impl From<Infallible> for Error {
fn from(i: Infallible) -> Self { match i {} }
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self) }
}
#[derive(Clone)]
pub struct RumaResponse<T>(pub T);
impl<T> From<T> for RumaResponse<T> {
fn from(t: T) -> Self { Self(t) }
}
impl From<Error> for RumaResponse<UiaaResponse> {
fn from(t: Error) -> Self { t.to_response() }
}
impl Error {
pub fn to_response(&self) -> RumaResponse<UiaaResponse> {
if let Self::Uiaa(uiaainfo) = self {
return RumaResponse(UiaaResponse::AuthResponse(uiaainfo.clone()));
}
@@ -147,48 +204,17 @@ impl Error {
status_code,
}))
}
}
/// Returns the Matrix error code / error kind
pub(crate) fn error_code(&self) -> ErrorKind {
if let Self::Federation(_, error) = self {
return error.error_kind().unwrap_or_else(|| &Unknown).clone();
}
impl ::axum::response::IntoResponse for Error {
fn into_response(self) -> ::axum::response::Response { self.to_response().into_response() }
}
match self {
Self::BadRequest(kind, _) => kind.clone(),
_ => Unknown,
}
}
/// Sanitizes public-facing errors that can leak sensitive information.
pub(crate) fn sanitized_error(&self) -> String {
let db_error = String::from("Database or I/O error occurred.");
match self {
#[cfg(feature = "sqlite")]
Self::Sqlite {
..
} => db_error,
#[cfg(feature = "rocksdb")]
Self::RocksDb {
..
} => db_error,
Self::Io {
..
} => db_error,
_ => self.to_string(),
impl<T: OutgoingResponse> IntoResponse for RumaResponse<T> {
fn into_response(self) -> Response {
match self.0.try_into_http_response::<BytesMut>() {
Ok(res) => res.map(BytesMut::freeze).map(Full::new).into_response(),
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}
}
impl From<Infallible> for Error {
fn from(i: Infallible) -> Self { match i {} }
}
impl axum::response::IntoResponse for Error {
fn into_response(self) -> axum::response::Response { self.to_response().into_response() }
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self) }
}
+79
View File
@@ -0,0 +1,79 @@
use std::sync::Arc;
use tracing_subscriber::{reload, EnvFilter};
/// We need to store a reload::Handle value, but can't name it's type explicitly
/// because the S type parameter depends on the subscriber's previous layers. In
/// our case, this includes unnameable 'impl Trait' types.
///
/// This is fixed[1] in the unreleased tracing-subscriber from the master
/// branch, which removes the S parameter. Unfortunately can't use it without
/// pulling in a version of tracing that's incompatible with the rest of our
/// deps.
///
/// To work around this, we define an trait without the S paramter that forwards
/// to the reload::Handle::reload method, and then store the handle as a trait
/// object.
///
/// [1]: <https://github.com/tokio-rs/tracing/pull/1035/commits/8a87ea52425098d3ef8f56d92358c2f6c144a28f>
pub trait ReloadHandle<L> {
fn reload(&self, new_value: L) -> Result<(), reload::Error>;
}
impl<L, S> ReloadHandle<L> for reload::Handle<L, S> {
fn reload(&self, new_value: L) -> Result<(), reload::Error> { reload::Handle::reload(self, new_value) }
}
struct LogLevelReloadHandlesInner {
handles: Vec<Box<dyn ReloadHandle<EnvFilter> + Send + Sync>>,
}
/// Wrapper to allow reloading the filter on several several
/// [`tracing_subscriber::reload::Handle`]s at once, with the same value.
#[derive(Clone)]
pub struct LogLevelReloadHandles {
inner: Arc<LogLevelReloadHandlesInner>,
}
impl LogLevelReloadHandles {
#[must_use]
pub fn new(handles: Vec<Box<dyn ReloadHandle<EnvFilter> + Send + Sync>>) -> LogLevelReloadHandles {
LogLevelReloadHandles {
inner: Arc::new(LogLevelReloadHandlesInner {
handles,
}),
}
}
pub fn reload(&self, new_value: &EnvFilter) -> Result<(), reload::Error> {
for handle in &self.inner.handles {
handle.reload(new_value.clone())?;
}
Ok(())
}
}
#[macro_export]
macro_rules! error {
( $($x:tt)+ ) => { tracing::error!( $($x)+ ); }
}
#[macro_export]
macro_rules! warn {
( $($x:tt)+ ) => { tracing::warn!( $($x)+ ); }
}
#[macro_export]
macro_rules! info {
( $($x:tt)+ ) => { tracing::info!( $($x)+ ); }
}
#[macro_export]
macro_rules! debug {
( $($x:tt)+ ) => { tracing::debug!( $($x)+ ); }
}
#[macro_export]
macro_rules! trace {
( $($x:tt)+ ) => { tracing::trace!( $($x)+ ); }
}
+27
View File
@@ -0,0 +1,27 @@
pub mod alloc;
pub mod config;
pub mod debug;
pub mod error;
pub mod log;
pub mod mods;
pub mod pducount;
pub mod server;
pub mod utils;
pub use config::Config;
pub use error::{Error, Result, RumaResponse};
pub use pducount::PduCount;
pub use server::Server;
pub use utils::conduwuit_version;
#[cfg(not(conduit_mods))]
pub mod mods {
#[macro_export]
macro_rules! mod_ctor {
() => {};
}
#[macro_export]
macro_rules! mod_dtor {
() => {};
}
}
+28
View File
@@ -0,0 +1,28 @@
use std::sync::atomic::{AtomicI32, Ordering};
const ORDERING: Ordering = Ordering::Relaxed;
static STATIC_DTORS: AtomicI32 = AtomicI32::new(0);
/// Called by Module::unload() to indicate module is about to be unloaded and
/// static destruction is intended. This will allow verifying it actually took
/// place.
pub(crate) fn prepare() {
let count = STATIC_DTORS.fetch_sub(1, ORDERING);
debug_assert!(count <= 0, "STATIC_DTORS should not be greater than zero.");
}
/// Called by static destructor of a module. This call should only be found
/// inside a mod_fini! macro. Do not call from anywhere else.
#[inline(always)]
pub fn report() { let _count = STATIC_DTORS.fetch_add(1, ORDERING); }
/// Called by Module::unload() (see check()) with action in case a check()
/// failed. This can allow a stuck module to be noted while allowing for other
/// independent modules to be diagnosed.
pub(crate) fn check_and_reset() -> bool { STATIC_DTORS.swap(0, ORDERING) == 0 }
/// Called by Module::unload() after unload to verify static destruction took
/// place. A call to prepare() must be made prior to Module::unload() and making
/// this call.
#[allow(dead_code)]
pub(crate) fn check() -> bool { STATIC_DTORS.load(ORDERING) == 0 }
+44
View File
@@ -0,0 +1,44 @@
#[macro_export]
macro_rules! mod_ctor {
( $($body:block)? ) => {
$crate::mod_init! {{
$crate::debug_info!("Module loaded");
$($body)?
}}
}
}
#[macro_export]
macro_rules! mod_dtor {
( $($body:block)? ) => {
$crate::mod_fini! {{
$crate::debug_info!("Module unloading");
$($body)?
$crate::mods::canary::report();
}}
}
}
#[macro_export]
macro_rules! mod_init {
($body:block) => {
#[used]
#[cfg_attr(target_family = "unix", link_section = ".init_array")]
static MOD_INIT: extern "C" fn() = { _mod_init };
#[cfg_attr(target_family = "unix", link_section = ".text.startup")]
extern "C" fn _mod_init() -> () $body
};
}
#[macro_export]
macro_rules! mod_fini {
($body:block) => {
#[used]
#[cfg_attr(target_family = "unix", link_section = ".fini_array")]
static MOD_FINI: extern "C" fn() = { _mod_fini };
#[cfg_attr(target_family = "unix", link_section = ".text.startup")]
extern "C" fn _mod_fini() -> () $body
};
}

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