Compare commits

..

2 Commits

Author SHA1 Message Date
timedout dc574d0f77 chore: Add news fragment 2026-01-06 00:07:47 +00:00
timedout 3992071980 fix: Automatically remove corrupted appservice registrations 2026-01-06 00:07:47 +00:00
46 changed files with 204 additions and 754 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
name: Renovate
runs-on: ubuntu-latest
container:
image: ghcr.io/renovatebot/renovate:42.70.2@sha256:3c2ac1b94fa92ef2fa4d1a0493f2c3ba564454720a32fdbcac2db2846ff1ee47
image: ghcr.io/renovatebot/renovate:42.11.0@sha256:656c1e5b808279eac16c37b89562fb4c699e02fc7e219244f4a1fc2f0a7ce367
options: --tmpfs /tmp:exec
steps:
- name: Checkout
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
persist-credentials: true
token: ${{ secrets.FORGEJO_TOKEN }}
- uses: https://github.com/cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
- uses: https://github.com/cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0
with:
nix_path: nixpkgs=channel:nixos-unstable
+11 -4
View File
@@ -1,17 +1,26 @@
#cargo-features = ["profile-rustflags"]
[workspace]
resolver = "2"
members = ["src/*", "xtask/*"]
default-members = ["src/*"]
[workspace.package]
authors = ["Continuwuity Team and contributors <team@continuwuity.org>"]
description = "A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver."
authors = [
"June Clementine Strawberry <june@girlboss.ceo>",
"strawberry <strawberry@puppygock.gay>", # woof
"Jason Volk <jason@zemos.net>",
]
categories = ["network-programming"]
description = "a very cool Matrix chat homeserver written in Rust"
edition = "2024"
homepage = "https://continuwuity.org/"
keywords = ["chat", "matrix", "networking", "server", "uwu"]
license = "Apache-2.0"
# See also `rust-toolchain.toml`
readme = "README.md"
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
rust-version = "1.86.0"
version = "0.5.1"
[workspace.metadata.crane]
@@ -839,8 +848,6 @@ unknown_lints = "allow"
###################
cargo = { level = "warn", priority = -1 }
# Nobody except for us should be consuming these crates, they don't need metadata
cargo_common_metadata = { level = "allow"}
## some sadness
multiple_crate_versions = { level = "allow", priority = 1 }
+1
View File
@@ -0,0 +1 @@
Fixed corrupted appservice registrations causing the server to enter a crash loop. Contributed by @nex.
-1
View File
@@ -1 +0,0 @@
Added admin command to forcefully log out all of a user's existing sessions. Contributed by @nex.
-1
View File
@@ -1 +0,0 @@
Implemented toggling the ability for an account to log in without mutating any of its data. Contributed by @nex.
-1
View File
@@ -1 +0,0 @@
Added support for issuing additional registration tokens, stored in the database, which supplement the existing registration token hardcoded in the config file. These tokens may optionally expire after a certain number of uses or after a certain amount of time has passed. Additionally, the `registration_token_file` configuration option is superseded by this feature and **has been removed**.
+12 -3
View File
@@ -421,7 +421,7 @@
# `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
#
# If you would like registration only via token reg, please configure
# `registration_token`.
# `registration_token` or `registration_token_file`.
#
#allow_registration = false
@@ -452,13 +452,22 @@
# `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
# to true to allow open registration without any conditions.
#
# If you do not want to set a static token, the `!admin token` commands
# may also be used to manage registration tokens.
# YOU NEED TO EDIT THIS OR USE registration_token_file.
#
# example: "o&^uCtes4HPf0Vu@F20jQeeWE7"
#
#registration_token =
# Path to a file on the system that gets read for additional registration
# tokens. Multiple tokens can be added if you separate them with
# whitespace
#
# continuwuity must be able to access the file, and it must not be empty
#
# example: "/etc/continuwuity/.reg_token"
#
#registration_token_file =
# The public site key for reCaptcha. If this is provided, reCaptcha
# becomes required during registration. If both captcha *and*
# registration token are enabled, both will be required during
+1 -1
View File
@@ -52,7 +52,7 @@ ENV BINSTALL_VERSION=1.16.6
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree
ENV LDDTREE_VERSION=0.4.0
ENV LDDTREE_VERSION=0.3.7
# renovate: datasource=crate depName=timelord-cli
ENV TIMELORD_VERSION=3.0.1
+1 -1
View File
@@ -22,7 +22,7 @@ ENV BINSTALL_VERSION=1.16.6
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree
ENV LDDTREE_VERSION=0.4.0
ENV LDDTREE_VERSION=0.3.7
# Install unpackaged tools
RUN <<EOF
+9 -27
View File
@@ -11,10 +11,10 @@ OCI images for Continuwuity are available in the registries listed below.
| Registry | Image | Notes |
| --------------- | --------------------------------------------------------------- | -----------------------|
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:latest](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/latest) | Latest tagged image. |
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:main](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/main) | Main branch image. |
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:latest-maxperf](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/latest-maxperf) | Performance optimised version. |
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:main-maxperf](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/main-maxperf) | Performance optimised version. |
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:latest][fj] | Latest tagged image. |
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:main][fj] | Main branch image. |
[fj]: https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity
Use
@@ -24,15 +24,6 @@ docker image pull $LINK
to pull it to your machine.
#### Mirrors
Images are mirrored to multiple locations automatically, on a schedule:
- `ghcr.io/continuwuity/continuwuity`
- `docker.io/jadedblueeyes/continuwuity`
- `registry.gitlab.com/continuwuity/continuwuity`
- `git.nexy7574.co.uk/mirrored/continuwuity` (releases only, no `main`)
### Run
When you have the image, you can simply run it with
@@ -58,7 +49,7 @@ If you just want to test Continuwuity for a short time, you can use the `--rm`
flag, which cleans up everything related to your container after you stop
it.
### Docker Compose
### Docker-compose
If the `docker run` command is not suitable for you or your setup, you can also use one
of the provided `docker-compose` files.
@@ -167,19 +158,8 @@ docker buildx build --load --tag continuwuity:latest -f docker/Dockerfile .
# Example: Build for specific platforms and push to a registry.
# docker buildx build --platform linux/amd64,linux/arm64 --tag registry.io/org/continuwuity:latest -f docker/Dockerfile . --push
# Example: Build binary optimised for the current CPU (standard release profile)
# docker buildx build --load \
# --tag continuwuity:latest \
# --build-arg TARGET_CPU=native \
# -f docker/Dockerfile .
# Example: Build maxperf variant (release-max-perf profile with LTO)
# Optimised for runtime performance and smaller binary size, but requires longer build time
# docker buildx build --load \
# --tag continuwuity:latest-maxperf \
# --build-arg TARGET_CPU=native \
# --build-arg RUST_PROFILE=release-max-perf \
# -f docker/Dockerfile .
# Example: Build binary optimized for the current CPU
# docker buildx build --load --tag continuwuity:latest --build-arg TARGET_CPU=native -f docker/Dockerfile .
```
Refer to the Docker Buildx documentation for more advanced build options.
@@ -218,3 +198,5 @@ Alternatively, you can use Continuwuity's built-in delegation file capability. S
## Voice communication
See the [TURN](../turn.md) page.
[nix-buildlayeredimage]: https://ryantm.github.io/nixpkgs/builders/images/dockertools/#ssec-pkgs-dockerTools-buildLayeredImage
+18 -28
View File
@@ -8,39 +8,29 @@
## Installing Continuwuity
### Prebuilt binary
### Static prebuilt binary
Download the binary for your architecture (x86_64 or aarch64) -
run the `uname -m` to check which you need.
You may simply download the binary that fits your machine architecture (x86_64
or aarch64). Run `uname -m` to see what you need.
Prebuilt binaries are available from:
- **Tagged releases**: [Latest release page](https://forgejo.ellis.link/continuwuation/continuwuity/releases/latest)
- **Development builds**: CI artifacts from the `main` branch
(includes Debian/Ubuntu packages)
You can download prebuilt fully static musl binaries from the latest tagged
release [here](https://forgejo.ellis.link/continuwuation/continuwuity/releases/latest) or
from the `main` CI branch workflow artifact output. These also include Debian/Ubuntu
packages.
When browsing CI artifacts, `ci-bins` contains binaries organised
by commit hash, while `releases` contains tagged versions. Sort
by last modified date to find the most recent builds.
You can download these directly using curl. The `ci-bins` are CI workflow binaries organized by commit
hash/revision, and `releases` are tagged releases. Sort by descending last
modified date to find the latest.
The binaries require jemalloc and io_uring on the host system. Currently
we can't cross-build static binaries - contributions are welcome here.
These binaries have jemalloc and io_uring statically linked and included with
them, so no additional dynamic dependencies need to be installed.
#### Performance-optimised builds
For x86_64 systems with CPUs from the last ~15 years, use the
`-haswell-` optimised binaries for best performance. These
binaries enable hardware-accelerated CRC32 checksumming in
RocksDB, which significantly improves database performance.
The haswell instruction set provides an excellent balance of
compatibility and speed.
If you're using Docker instead, equivalent performance-optimised
images are available with the `-maxperf` suffix (e.g.
`forgejo.ellis.link/continuwuation/continuwuity:latest-maxperf`).
These images use the `release-max-perf`
build profile with
[link-time optimisation (LTO)](https://doc.rust-lang.org/cargo/reference/profiles.html#lto)
and, for amd64, target the haswell CPU architecture.
For the **best** performance: if you are using an `x86_64` CPU made in the last ~15 years,
we recommend using the `-haswell-` optimized binaries. These set
`-march=haswell`, which provides the most compatible and highest performance with
optimized binaries. The database backend, RocksDB, benefits most from this as it
uses hardware-accelerated CRC32 hashing/checksumming, which is critical
for performance.
### Compiling
+1 -1
View File
@@ -8,7 +8,7 @@ This document contains the help content for the `continuwuity` command-line prog
## `continuwuity`
A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver.
a very cool Matrix chat homeserver written in Rust
**Usage:** `continuwuity [OPTIONS]`
+2 -2
View File
@@ -4,7 +4,7 @@
Name: continuwuity
Version: {{{ git_repo_version }}}
Release: 1%{?dist}
Summary: A Matrix homeserver written in Rust.
Summary: Very cool Matrix chat homeserver written in Rust
License: Apache-2.0 AND MIT
@@ -23,7 +23,7 @@ Requires: glibc
Requires: libstdc++
%global _description %{expand:
A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver.}
A cool hard fork of Conduit, a Matrix homeserver written in Rust}
%description %{_description}
+2
View File
@@ -1,7 +1,9 @@
[package]
name = "conduwuit_admin"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
+4 -20
View File
@@ -2,17 +2,10 @@ use clap::Parser;
use conduwuit::Result;
use crate::{
appservice::{self, AppserviceCommand},
check::{self, CheckCommand},
context::Context,
debug::{self, DebugCommand},
federation::{self, FederationCommand},
media::{self, MediaCommand},
query::{self, QueryCommand},
room::{self, RoomCommand},
server::{self, ServerCommand},
token::{self, TokenCommand},
user::{self, UserCommand},
appservice, appservice::AppserviceCommand, check, check::CheckCommand, context::Context,
debug, debug::DebugCommand, federation, federation::FederationCommand, media,
media::MediaCommand, query, query::QueryCommand, room, room::RoomCommand, server,
server::ServerCommand, user, user::UserCommand,
};
#[derive(Debug, Parser)]
@@ -26,10 +19,6 @@ pub enum AdminCommand {
/// - Commands for managing local users
Users(UserCommand),
#[command(subcommand)]
/// - Commands for managing registration tokens
Token(TokenCommand),
#[command(subcommand)]
/// - Commands for managing rooms
Rooms(RoomCommand),
@@ -75,11 +64,6 @@ pub(super) async fn process(command: AdminCommand, context: &Context<'_>) -> Res
context.bail_restricted()?;
user::process(command, context).await
},
| Token(command) => {
// token commands are all restricted
context.bail_restricted()?;
token::process(command, context).await
},
| Rooms(command) => room::process(command, context).await,
| Federation(command) => federation::process(command, context).await,
| Server(command) => server::process(command, context).await,
-1
View File
@@ -17,7 +17,6 @@ pub(crate) mod media;
pub(crate) mod query;
pub(crate) mod room;
pub(crate) mod server;
pub(crate) mod token;
pub(crate) mod user;
extern crate conduwuit_api as api;
-76
View File
@@ -1,76 +0,0 @@
use conduwuit::{Err, Result, utils};
use conduwuit_macros::admin_command;
use futures::StreamExt;
use service::registration_tokens::TokenExpires;
#[admin_command]
pub(super) async fn issue_token(&self, expires: super::TokenExpires) -> Result {
let expires = {
if expires.immortal {
None
} else if let Some(max_uses) = expires.max_uses {
Some(TokenExpires::AfterUses(max_uses))
} else if expires.once {
Some(TokenExpires::AfterUses(1))
} else if let Some(max_age) = expires
.max_age
.as_deref()
.map(|max_age| utils::time::timepoint_from_now(utils::time::parse_duration(max_age)?))
.transpose()?
{
Some(TokenExpires::AfterTime(max_age))
} else {
unreachable!();
}
};
let (token, info) = self
.services
.registration_tokens
.issue_token(self.sender_or_service_user().into(), expires);
self.write_str(&format!(
"New registration token issued: `{token}`. {}.",
if let Some(expires) = info.expires {
format!("{expires}")
} else {
"Never expires".to_owned()
}
))
.await
}
#[admin_command]
pub(super) async fn revoke_token(&self, token: String) -> Result {
let Some(token) = self
.services
.registration_tokens
.validate_token(token)
.await
else {
return Err!("This token does not exist or has already expired.");
};
self.services.registration_tokens.revoke_token(token)?;
self.write_str("Token revoked successfully.").await
}
#[admin_command]
pub(super) async fn list_tokens(&self) -> Result {
let tokens: Vec<_> = self
.services
.registration_tokens
.iterate_tokens()
.collect()
.await;
self.write_str(&format!("Found {} registration tokens:\n", tokens.len()))
.await?;
for token in tokens {
self.write_str(&format!("- {token}\n")).await?;
}
Ok(())
}
-51
View File
@@ -1,51 +0,0 @@
mod commands;
use clap::{Args, Subcommand};
use conduwuit::Result;
use crate::admin_command_dispatch;
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
pub enum TokenCommand {
/// - Issue a new registration token
#[clap(name = "issue")]
IssueToken {
/// When this token will expire.
#[command(flatten)]
expires: TokenExpires,
},
/// - Revoke a registration token
#[clap(name = "revoke")]
RevokeToken {
/// The token to revoke.
token: String,
},
/// - List all registration tokens
#[clap(name = "list")]
ListTokens,
}
#[derive(Debug, Args)]
#[group(required = true, multiple = false)]
pub struct TokenExpires {
/// The maximum number of times this token is allowed to be used before it
/// expires.
#[arg(long)]
max_uses: Option<u64>,
/// The maximum age of this token (e.g. 30s, 5m, 7d). It will expire after
/// this much time has passed.
#[arg(long)]
max_age: Option<String>,
/// This token will never expire.
#[arg(long)]
immortal: bool,
/// A shortcut for `--max-uses 1`.
#[arg(long)]
once: bool,
}
+2 -87
View File
@@ -280,12 +280,7 @@ pub(super) async fn unsuspend(&self, user_id: String) -> Result {
}
#[admin_command]
pub(super) async fn reset_password(
&self,
logout: bool,
username: String,
password: Option<String>,
) -> Result {
pub(super) async fn reset_password(&self, username: String, password: Option<String>) -> Result {
let user_id = parse_local_user_id(self.services, &username)?;
if user_id == self.services.globals.server_user {
@@ -308,18 +303,7 @@ pub(super) async fn reset_password(
write!(self, "Successfully reset the password for user {user_id}: `{new_password}`")
},
}
.await?;
if logout {
self.services
.users
.all_device_ids(&user_id)
.for_each(|device_id| self.services.users.remove_device(&user_id, device_id))
.await;
write!(self, "\nAll existing sessions have been logged out.").await?;
}
Ok(())
.await
}
#[admin_command]
@@ -1033,72 +1017,3 @@ pub(super) async fn unlock(&self, user_id: String) -> Result {
self.write_str(&format!("User {user_id} has been unlocked."))
.await
}
#[admin_command]
pub(super) async fn logout(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
if user_id == self.services.globals.server_user {
return Err!("Not allowed to log out the server service account.",);
}
if !self.services.users.exists(&user_id).await {
return Err!("User {user_id} does not exist.");
}
if self.services.users.is_admin(&user_id).await {
return Err!("You cannot forcefully log out admin users.");
}
self.services
.users
.all_device_ids(&user_id)
.for_each(|device_id| self.services.users.remove_device(&user_id, device_id))
.await;
self.write_str(&format!("User {user_id} has been logged out from all devices."))
.await
}
#[admin_command]
pub(super) async fn disable_login(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
if user_id == self.services.globals.server_user {
return Err!("Not allowed to disable login for the server service account.",);
}
if !self.services.users.exists(&user_id).await {
return Err!("User {user_id} does not exist.");
}
if self.services.users.is_admin(&user_id).await {
return Err!("Admin users cannot have their login disallowed.");
}
self.services.users.disable_login(&user_id);
self.write_str(&format!(
"{user_id} can no longer log in. Their existing sessions remain unaffected."
))
.await
}
#[admin_command]
pub(super) async fn enable_login(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
if !self.services.users.exists(&user_id).await {
return Err!("User {user_id} does not exist.");
}
self.services.users.enable_login(&user_id);
self.write_str(&format!("{user_id} can now log in.")).await
}
-31
View File
@@ -20,9 +20,6 @@ pub enum UserCommand {
/// - Reset user password
ResetPassword {
/// Log out existing sessions
#[arg(short, long)]
logout: bool,
/// Username of the user for whom the password should be reset
username: String,
/// New password for the user, if unspecified one is generated
@@ -62,18 +59,6 @@ pub enum UserCommand {
force: bool,
},
/// - Forcefully log a user out of all of their devices.
///
/// This will invalidate all access tokens for the specified user,
/// effectively logging them out from all sessions.
/// Note that this is destructive and may result in data loss for the user,
/// such as encryption keys. Use with caution. Can only be used in the admin
/// room.
Logout {
/// Username of the user to log out
user_id: String,
},
/// - Suspend a user
///
/// Suspended users are able to log in, sync, and read messages, but are not
@@ -116,22 +101,6 @@ pub enum UserCommand {
user_id: String,
},
/// - Enable login for a user
EnableLogin {
/// Username of the user to enable login for
user_id: String,
},
/// - Disable login for a user
///
/// Disables login for the specified user without deactivating or locking
/// their account. This prevents the user from obtaining new access tokens,
/// but does not invalidate existing sessions.
DisableLogin {
/// Username of the user to disable login for
user_id: String,
},
/// - List local users in the database
#[clap(alias = "list")]
ListUsers,
+2
View File
@@ -1,7 +1,9 @@
[package]
name = "conduwuit_api"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
+13 -39
View File
@@ -179,18 +179,13 @@ pub(crate) async fn register_route(
},
}
return Err!(Request(Forbidden(
"This server is not accepting registrations at this time."
)));
return Err!(Request(Forbidden("Registration has been disabled.")));
}
if is_guest
&& (!services.config.allow_guest_registration
|| (services.config.allow_registration
&& services
.registration_tokens
.get_config_file_token()
.is_some()))
&& services.globals.registration_token.is_some()))
{
info!(
"Guest registration disabled / registration enabled with token configured, \
@@ -208,9 +203,7 @@ pub(crate) async fn register_route(
rejecting registration. Guest's initial device name: \"{}\"",
body.initial_device_display_name.as_deref().unwrap_or("")
);
return Err!(Request(Forbidden(
"This server is not accepting registrations at this time."
)));
return Err!(Request(Forbidden("Registration is temporarily disabled.")));
}
let user_id = match (body.username.as_ref(), is_guest) {
@@ -308,13 +301,7 @@ pub(crate) async fn register_route(
let skip_auth = body.appservice_info.is_some() || is_guest;
// Populate required UIAA flows
if services
.registration_tokens
.iterate_tokens()
.next()
.await
.is_some()
{
if services.globals.registration_token.is_some() {
// Registration token required
uiaainfo.flows.push(AuthFlow {
stages: vec![AuthType::RegistrationToken],
@@ -336,19 +323,7 @@ pub(crate) async fn register_route(
}
if uiaainfo.flows.is_empty() && !skip_auth {
// Registration isn't _disabled_, but there's no captcha configured and no
// registration tokens currently set. Bail out by default unless open
// registration was explicitly enabled.
if !services
.config
.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
{
return Err!(Request(Forbidden(
"This server is not accepting registrations at this time."
)));
}
// We have open registration enabled (😧), provide a dummy stage
// No registration token necessary, but clients must still go through the flow
uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Dummy] }],
completed: Vec::new(),
@@ -871,20 +846,19 @@ pub(crate) async fn request_3pid_management_token_via_msisdn_route(
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
///
/// Checks if the provided registration token is valid at the time of checking.
/// 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(
State(services): State<crate::State>,
body: Ruma<check_registration_token_validity::v1::Request>,
) -> Result<check_registration_token_validity::v1::Response> {
// TODO: ratelimit this pretty heavily
let Some(reg_token) = services.globals.registration_token.clone() else {
return Err!(Request(Forbidden("Server does not allow token registration")));
};
let valid = services
.registration_tokens
.validate_token(body.token.clone())
.await
.is_some();
Ok(check_registration_token_validity::v1::Response { valid })
Ok(check_registration_token_validity::v1::Response { valid: reg_token == body.token })
}
/// Runs through all the deactivation steps:
+1 -14
View File
@@ -178,20 +178,7 @@ pub async fn leave_room(
.rooms
.state_cache
.left_state(user_id, room_id)
.await
.inspect_err(|err| {
// `left_state` may return an Err if the user _is_ in the room they're
// trying to leave, but the membership cache is incorrect and
// they're cached as being joined. In this situation
// we save a `None` to the `roomuserid_leftcount` table, which generates
// and sends a dummy leave to the client.
warn!(
?err,
"Trying to leave room not cached as leave, sending dummy leave \
event to client"
);
})
.unwrap_or_default()
.await?
},
}
};
-11
View File
@@ -5,7 +5,6 @@ use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Error, Result, debug, err, info,
utils::{self, ReadyExt, hash},
warn,
};
use conduwuit_core::{debug_error, debug_warn};
use conduwuit_service::{Services, uiaa::SESSION_ID_LENGTH};
@@ -13,7 +12,6 @@ use futures::StreamExt;
use ruma::{
OwnedUserId, UserId,
api::client::{
error::ErrorKind,
session::{
get_login_token,
get_login_types::{
@@ -186,15 +184,6 @@ pub(crate) async fn handle_login(
return Err!(Request(Unknown("User ID does not belong to this homeserver")));
}
if services.users.is_locked(&user_id).await? {
return Err(Error::BadRequest(ErrorKind::UserLocked, "This account has been locked."));
}
if services.users.is_login_disabled(&user_id).await {
warn!(%user_id, "user attempted to log in with a login-disabled account");
return Err!(Request(Forbidden("This account is not permitted to log in.")));
}
if cfg!(feature = "ldap") && services.config.ldap.enable {
match Box::pin(ldap_login(services, &user_id, &lowercased_user_id, password)).await {
| Ok(user_id) => Ok(user_id),
+2
View File
@@ -1,7 +1,9 @@
[package]
name = "conduwuit_build_metadata"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
+2
View File
@@ -1,7 +1,9 @@
[package]
name = "conduwuit_core"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
+36
View File
@@ -146,6 +146,22 @@ pub fn check(config: &Config) -> Result {
));
}
// check if we can read the token file path, and check if the file is empty
if config.registration_token_file.as_ref().is_some_and(|path| {
let Ok(token) = std::fs::read_to_string(path).inspect_err(|e| {
error!("Failed to read the registration token file: {e}");
}) else {
return true;
};
token == String::new()
}) {
return Err!(Config(
"registration_token_file",
"Registration token file was specified but is empty or failed to be read"
));
}
if config.max_request_size < 10_000_000 {
return Err!(Config(
"max_request_size",
@@ -171,9 +187,29 @@ pub fn check(config: &Config) -> Result {
));
}
if config.allow_registration
&& !config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
&& config.registration_token.is_none()
&& config.registration_token_file.is_none()
&& config.recaptcha_site_key.is_none()
{
return Err!(Config(
"registration_token",
"!! You have `allow_registration` enabled without a token or captcha configured \
which means you are allowing ANYONE to register on your continuwuity instance \
without any 2nd-step (e.g. registration token, captcha), which is FREQUENTLY \
abused by malicious actors. If this is not the intended behaviour, please set a \
registration token. For security and safety reasons, continuwuity will shut down. \
If you are extra sure this is the desired behaviour you want, please set the \
following config option to true:
`yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`"
));
}
if config.allow_registration
&& config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
&& config.registration_token.is_none()
&& config.registration_token_file.is_none()
{
warn!(
"Open registration is enabled via setting \
+12 -5
View File
@@ -545,7 +545,7 @@ pub struct Config {
/// `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
///
/// If you would like registration only via token reg, please configure
/// `registration_token`.
/// `registration_token` or `registration_token_file`.
#[serde(default)]
pub allow_registration: bool,
@@ -576,14 +576,22 @@ pub struct Config {
/// `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
/// to true to allow open registration without any conditions.
///
/// If you do not want to set a static token, the `!admin token` commands
/// may also be used to manage registration tokens.
/// YOU NEED TO EDIT THIS OR USE registration_token_file.
///
/// example: "o&^uCtes4HPf0Vu@F20jQeeWE7"
///
/// display: sensitive
pub registration_token: Option<String>,
/// Path to a file on the system that gets read for additional registration
/// tokens. Multiple tokens can be added if you separate them with
/// whitespace
///
/// continuwuity must be able to access the file, and it must not be empty
///
/// example: "/etc/continuwuity/.reg_token"
pub registration_token_file: Option<PathBuf>,
/// The public site key for reCaptcha. If this is provided, reCaptcha
/// becomes required during registration. If both captcha *and*
/// registration token are enabled, both will be required during
@@ -2288,7 +2296,7 @@ pub struct DraupnirConfig {
pub secret: String,
}
const DEPRECATED_KEYS: &[&str] = &[
const DEPRECATED_KEYS: &[&str; 9] = &[
"cache_capacity",
"conduit_cache_capacity_modifier",
"max_concurrent_requests",
@@ -2298,7 +2306,6 @@ const DEPRECATED_KEYS: &[&str] = &[
"well_known_support_role",
"well_known_support_email",
"well_known_support_mxid",
"registration_token_file",
];
impl Config {
+2
View File
@@ -1,7 +1,9 @@
[package]
name = "conduwuit_database"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
-8
View File
@@ -141,10 +141,6 @@ pub(super) static MAPS: &[Descriptor] = &[
name: "referencedevents",
..descriptor::RANDOM
},
Descriptor {
name: "registrationtoken_info",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "roomid_invitedcount",
..descriptor::RANDOM_SMALL
@@ -394,10 +390,6 @@ pub(super) static MAPS: &[Descriptor] = &[
name: "userid_lock",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "userid_logindisabled",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "userid_presenceid",
..descriptor::RANDOM_SMALL
+2
View File
@@ -1,7 +1,9 @@
[package]
name = "conduwuit_macros"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
+6 -2
View File
@@ -2,12 +2,15 @@
name = "conduwuit"
default-run = "conduwuit"
authors.workspace = true
categories.workspace = true
description.workspace = true
edition.workspace = true
homepage.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
metadata.crane.workspace = true
@@ -20,13 +23,14 @@ crate-type = [
[package.metadata.deb]
name = "continuwuity"
maintainer = "Continuwuity Team and contributors <team@continuwuity.org>"
maintainer = "continuwuity developers <contact@continuwuity.org>"
copyright = "2024, continuwuity developers"
license-file = ["../../LICENSE", "3"]
depends = "$auto, ca-certificates"
breaks = ["conduwuit (<<0.5.0)"]
replaces = ["conduwuit (<<0.5.0)"]
extended-description = """\
A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver."""
a cool hard fork of Conduit, a Matrix homeserver written in Rust"""
section = "net"
priority = "optional"
conf-files = ["/etc/conduwuit/conduwuit.toml"]
+2
View File
@@ -1,7 +1,9 @@
[package]
name = "conduwuit_router"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
+2
View File
@@ -1,7 +1,9 @@
[package]
name = "conduwuit_service"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
+4 -1
View File
@@ -272,7 +272,10 @@ impl Service {
.get(id)
.await
.and_then(|ref bytes| serde_saphyr::from_slice(bytes).map_err(Into::into))
.map_err(|e| err!(Database("Invalid appservice {id:?} registration: {e:?}")))
.map_err(|e| {
self.db.id_appserviceregistrations.remove(id);
err!(Database("Invalid appservice {id:?} registration: {e:?}. Removed."))
})
}
pub fn read(&self) -> impl Future<Output = RwLockReadGuard<'_, Registrations>> + Send {
+15
View File
@@ -18,6 +18,7 @@ pub struct Service {
pub server_user: OwnedUserId,
pub admin_alias: OwnedRoomAliasId,
pub turn_secret: String,
pub registration_token: Option<String>,
}
type RateLimitState = (Instant, u32); // Time if last failed try, number of failed tries
@@ -40,6 +41,19 @@ impl crate::Service for Service {
},
);
let registration_token = config.registration_token_file.as_ref().map_or_else(
|| config.registration_token.clone(),
|path| {
let Ok(token) = std::fs::read_to_string(path).inspect_err(|e| {
error!("Failed to read the registration token file: {e}");
}) else {
return config.registration_token.clone();
};
Some(token.trim().to_owned())
},
);
Ok(Arc::new(Self {
db,
server: args.server.clone(),
@@ -52,6 +66,7 @@ impl crate::Service for Service {
)
.expect("@conduit:server_name is valid"),
turn_secret,
registration_token,
}))
}
-1
View File
@@ -24,7 +24,6 @@ pub mod media;
pub mod moderation;
pub mod presence;
pub mod pusher;
pub mod registration_tokens;
pub mod resolver;
pub mod rooms;
pub mod sending;
-129
View File
@@ -1,129 +0,0 @@
use std::{sync::Arc, time::SystemTime};
use conduwuit::utils::{
self,
stream::{ReadyExt, TryIgnore},
};
use database::{Database, Deserialized, Json, Map};
use futures::Stream;
use ruma::OwnedUserId;
use serde::{Deserialize, Serialize};
pub(super) struct Data {
registrationtoken_info: Arc<Map>,
}
/// Metadata of a registration token.
#[derive(Debug, Serialize, Deserialize)]
pub struct DatabaseTokenInfo {
/// The admin user who created this token.
pub creator: OwnedUserId,
/// The number of times this token has been used to create an account.
pub uses: u64,
/// When this token will expire, if it expires.
pub expires: Option<TokenExpires>,
}
impl DatabaseTokenInfo {
pub(super) fn new(creator: OwnedUserId, expires: Option<TokenExpires>) -> Self {
Self { creator, uses: 0, expires }
}
/// Determine whether this token info represents a valid token, i.e. one
/// that has not expired according to its [`Self::expires`] property. If
/// [`Self::expires`] is [`None`], this function will always return `true`.
#[must_use]
pub fn is_valid(&self) -> bool {
match self.expires {
| Some(TokenExpires::AfterUses(max_uses)) => self.uses < max_uses,
| Some(TokenExpires::AfterTime(expiry_time)) => {
let now = SystemTime::now();
expiry_time >= now
},
| None => true,
}
}
}
impl std::fmt::Display for DatabaseTokenInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Token created by {} and used {} times. ", &self.creator, self.uses)?;
if let Some(expires) = &self.expires {
write!(f, "{expires}.")?;
} else {
write!(f, "Never expires.")?;
}
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize)]
pub enum TokenExpires {
AfterUses(u64),
AfterTime(SystemTime),
}
impl std::fmt::Display for TokenExpires {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
| Self::AfterUses(max_uses) => write!(f, "Expires after {max_uses} uses"),
| Self::AfterTime(max_age) => {
let now = SystemTime::now();
let formatted_expiry = utils::time::format(*max_age, "%+");
match max_age.duration_since(now) {
| Ok(duration) => write!(
f,
"Expires in {} ({formatted_expiry})",
utils::time::pretty(duration)
),
| Err(_) => write!(f, "Expired at {formatted_expiry}"),
}
},
}
}
}
impl Data {
pub(super) fn new(db: &Arc<Database>) -> Self {
Self {
registrationtoken_info: db["registrationtoken_info"].clone(),
}
}
/// Associate a registration token with its metadata in the database.
pub(super) fn save_token(&self, token: &str, info: &DatabaseTokenInfo) {
self.registrationtoken_info.raw_put(token, Json(info));
}
/// Delete a registration token.
pub(super) fn revoke_token(&self, token: &str) { self.registrationtoken_info.remove(token); }
/// Look up a registration token's metadata.
pub(super) async fn lookup_token_info(&self, token: &str) -> Option<DatabaseTokenInfo> {
self.registrationtoken_info
.get(token)
.await
.deserialized()
.ok()
}
/// Iterate over all valid tokens and delete expired ones.
pub(super) fn iterate_and_clean_tokens(
&self,
) -> impl Stream<Item = (&str, DatabaseTokenInfo)> + Send + '_ {
self.registrationtoken_info
.stream()
.ignore_err()
.ready_filter_map(|item: (&str, DatabaseTokenInfo)| {
if item.1.is_valid() {
Some(item)
} else {
self.registrationtoken_info.remove(item.0);
None
}
})
}
}
-172
View File
@@ -1,172 +0,0 @@
mod data;
use std::sync::Arc;
use conduwuit::{Err, Result, utils};
use data::Data;
pub use data::{DatabaseTokenInfo, TokenExpires};
use futures::{Stream, StreamExt, stream};
use ruma::OwnedUserId;
use crate::{Dep, config};
const RANDOM_TOKEN_LENGTH: usize = 16;
pub struct Service {
db: Data,
services: Services,
}
struct Services {
config: Dep<config::Service>,
}
/// A validated registration token which may be used to create an account.
#[derive(Debug)]
pub struct ValidToken {
pub token: String,
pub source: ValidTokenSource,
}
impl std::fmt::Display for ValidToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "`{}` --- {}", self.token, &self.source)
}
}
impl PartialEq<str> for ValidToken {
fn eq(&self, other: &str) -> bool { self.token == other }
}
/// The source of a valid database token.
#[derive(Debug)]
pub enum ValidTokenSource {
/// The static token set in the homeserver's config file, which is
/// always valid.
ConfigFile,
/// A database token which has been checked to be valid.
Database(DatabaseTokenInfo),
}
impl std::fmt::Display for ValidTokenSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
| Self::ConfigFile => write!(f, "Token defined in config."),
| Self::Database(info) => info.fmt(f),
}
}
}
impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
Ok(Arc::new(Self {
db: Data::new(args.db),
services: Services {
config: args.depend::<config::Service>("config"),
},
}))
}
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
}
impl Service {
/// Issue a new registration token and save it in the database.
pub fn issue_token(
&self,
creator: OwnedUserId,
expires: Option<TokenExpires>,
) -> (String, DatabaseTokenInfo) {
let token = utils::random_string(RANDOM_TOKEN_LENGTH);
let info = DatabaseTokenInfo::new(creator, expires);
self.db.save_token(&token, &info);
(token, info)
}
/// Get the registration token set in the config file, if it exists.
pub fn get_config_file_token(&self) -> Option<ValidToken> {
self.services
.config
.registration_token
.clone()
.map(|token| ValidToken {
token,
source: ValidTokenSource::ConfigFile,
})
}
/// Validate a registration token.
pub async fn validate_token(&self, token: String) -> Option<ValidToken> {
// Check the registration token in the config first
if self
.get_config_file_token()
.is_some_and(|valid_token| valid_token == *token)
{
return Some(ValidToken {
token,
source: ValidTokenSource::ConfigFile,
});
}
// Now check the database
if let Some(token_info) = self.db.lookup_token_info(&token).await
&& token_info.is_valid()
{
return Some(ValidToken {
token,
source: ValidTokenSource::Database(token_info),
});
}
// Otherwise it's not valid
None
}
/// Mark a valid token as having been used to create a new account.
pub fn mark_token_as_used(&self, ValidToken { token, source }: ValidToken) {
match source {
| ValidTokenSource::ConfigFile => {
// we don't track uses of the config file token, do nothing
},
| ValidTokenSource::Database(mut info) => {
info.uses = info.uses.saturating_add(1);
self.db.save_token(&token, &info);
},
}
}
/// Try to revoke a valid token.
///
/// Note that some tokens (like the one set in the config file) cannot be
/// revoked.
pub fn revoke_token(&self, ValidToken { token, source }: ValidToken) -> Result {
match source {
| ValidTokenSource::ConfigFile => {
// the config file token cannot be revoked
Err!(
"The token set in the config file cannot be revoked. Edit the config file \
to change it."
)
},
| ValidTokenSource::Database(_) => {
self.db.revoke_token(&token);
Ok(())
},
}
}
/// Iterate over all valid registration tokens.
pub fn iterate_tokens(&self) -> impl Stream<Item = ValidToken> + Send + '_ {
let db_tokens = self
.db
.iterate_and_clean_tokens()
.map(|(token, info)| ValidToken {
token: token.to_owned(),
source: ValidTokenSource::Database(info),
});
stream::iter(self.get_config_file_token()).chain(db_tokens)
}
}
+2 -5
View File
@@ -11,9 +11,8 @@ use crate::{
account_data, admin, announcements, antispam, appservice, client, config, emergency,
federation, globals, key_backups,
manager::Manager,
media, moderation, presence, pusher, registration_tokens, resolver, rooms, sending,
server_keys,
service::{self, Args, Map, Service},
media, moderation, presence, pusher, resolver, rooms, sending, server_keys, service,
service::{Args, Map, Service},
sync, transaction_ids, uiaa, users,
};
@@ -29,7 +28,6 @@ pub struct Services {
pub media: Arc<media::Service>,
pub presence: Arc<presence::Service>,
pub pusher: Arc<pusher::Service>,
pub registration_tokens: Arc<registration_tokens::Service>,
pub resolver: Arc<resolver::Service>,
pub rooms: rooms::Service,
pub federation: Arc<federation::Service>,
@@ -79,7 +77,6 @@ impl Services {
media: build!(media::Service),
presence: build!(presence::Service),
pusher: build!(pusher::Service),
registration_tokens: build!(registration_tokens::Service),
rooms: rooms::Service {
alias: build!(rooms::alias::Service),
auth_chain: build!(rooms::auth_chain::Service),
+27 -17
View File
@@ -1,4 +1,7 @@
use std::{collections::BTreeMap, sync::Arc};
use std::{
collections::{BTreeMap, HashSet},
sync::Arc,
};
use conduwuit::{
Err, Error, Result, SyncRwLock, err, error, implement, utils,
@@ -13,7 +16,7 @@ use ruma::{
},
};
use crate::{Dep, config, globals, registration_tokens, users};
use crate::{Dep, config, globals, users};
pub struct Service {
userdevicesessionid_uiaarequest: SyncRwLock<RequestMap>,
@@ -25,7 +28,6 @@ struct Services {
globals: Dep<globals::Service>,
users: Dep<users::Service>,
config: Dep<config::Service>,
registration_tokens: Dep<registration_tokens::Service>,
}
struct Data {
@@ -48,8 +50,6 @@ impl crate::Service for Service {
globals: args.depend::<globals::Service>("globals"),
users: args.depend::<users::Service>("users"),
config: args.depend::<config::Service>("config"),
registration_tokens: args
.depend::<registration_tokens::Service>("registration_tokens"),
},
}))
}
@@ -57,6 +57,26 @@ impl crate::Service for Service {
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
}
#[implement(Service)]
pub async fn read_tokens(&self) -> Result<HashSet<String>> {
let mut tokens = HashSet::new();
if let Some(file) = &self.services.config.registration_token_file.as_ref() {
match std::fs::read_to_string(file) {
| Ok(text) => {
text.split_ascii_whitespace().for_each(|token| {
tokens.insert(token.to_owned());
});
},
| Err(e) => error!("Failed to read the registration token file: {e}"),
}
}
if let Some(token) = &self.services.config.registration_token {
tokens.insert(token.to_owned());
}
Ok(tokens)
}
/// Creates a new Uiaa session. Make sure the session token is unique.
#[implement(Service)]
pub fn create(
@@ -209,18 +229,8 @@ pub async fn try_auth(
}
},
| AuthData::RegistrationToken(t) => {
let token = t.token.trim().to_owned();
if let Some(valid_token) = self
.services
.registration_tokens
.validate_token(token)
.await
{
self.services
.registration_tokens
.mark_token_as_used(valid_token);
let tokens = self.read_tokens().await?;
if tokens.contains(t.token.trim()) {
uiaainfo.completed.push(AuthType::RegistrationToken);
} else {
uiaainfo.auth_error = Some(StandardErrorBody {
-12
View File
@@ -78,7 +78,6 @@ struct Data {
userid_password: Arc<Map>,
userid_suspension: Arc<Map>,
userid_lock: Arc<Map>,
userid_logindisabled: Arc<Map>,
userid_selfsigningkeyid: Arc<Map>,
userid_usersigningkeyid: Arc<Map>,
useridprofilekey_value: Arc<Map>,
@@ -118,7 +117,6 @@ impl crate::Service for Service {
userid_password: args.db["userid_password"].clone(),
userid_suspension: args.db["userid_suspension"].clone(),
userid_lock: args.db["userid_lock"].clone(),
userid_logindisabled: args.db["userid_logindisabled"].clone(),
userid_selfsigningkeyid: args.db["userid_selfsigningkeyid"].clone(),
userid_usersigningkeyid: args.db["userid_usersigningkeyid"].clone(),
useridprofilekey_value: args.db["useridprofilekey_value"].clone(),
@@ -297,16 +295,6 @@ impl Service {
}
}
pub fn disable_login(&self, user_id: &UserId) {
self.db.userid_logindisabled.insert(user_id, "");
}
pub fn enable_login(&self, user_id: &UserId) { self.db.userid_logindisabled.remove(user_id); }
pub async fn is_login_disabled(&self, user_id: &UserId) -> bool {
self.db.userid_logindisabled.contains(user_id).await
}
/// Check if account is active, infallible
pub async fn is_active(&self, user_id: &UserId) -> bool {
!self.is_deactivated(user_id).await.unwrap_or(true)
+2
View File
@@ -1,7 +1,9 @@
[package]
name = "conduwuit_web"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
@@ -1,12 +1,15 @@
[package]
name = "xtask-generate-commands"
authors.workspace = true
categories.workspace = true
description.workspace = true
edition.workspace = true
homepage.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
[dependencies]
+3
View File
@@ -1,12 +1,15 @@
[package]
name = "xtask"
authors.workspace = true
categories.workspace = true
description.workspace = true
edition.workspace = true
homepage.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
[dependencies]