feat: Readded support for reading registration tokens from a file

Co-authored-by: Ginger <ginger@gingershaped.computer>
This commit is contained in:
Ben Botwin 2026-02-16 16:27:59 -05:00 committed by Ellis Git
parent da561ab792
commit 5eb74bc1dd
5 changed files with 79 additions and 28 deletions

View file

@ -476,18 +476,23 @@
#yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = false #yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = false
# A static registration token that new users will have to provide when # A static registration token that new users will have to provide when
# creating an account. If unset and `allow_registration` is true, # creating an account. This token does not supersede tokens from other sources, such as the `!admin token`
# you must set # command or the `registration_token_file` configuration option.
# `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.
# #
# example: "o&^uCtes4HPf0Vu@F20jQeeWE7" # example: "o&^uCtes4HPf0Vu@F20jQeeWE7"
# #
#registration_token = #registration_token =
# A path to a file containing static registration tokens, one per line.
# Tokens in this file do not supersede tokens from other sources, such as the `!admin token`
# command or the `registration_token` configuration option.
#
# The file will be read once, when Continuwuity starts. It is not currently reread
# when the server configuration is reloaded. If the file cannot be read, Continuwuity
# will fail to start.
#
#registration_token_file =
# The public site key for reCaptcha. If this is provided, reCaptcha # The public site key for reCaptcha. If this is provided, reCaptcha
# becomes required during registration. If both captcha *and* # becomes required during registration. If both captcha *and*
# registration token are enabled, both will be required during # registration token are enabled, both will be required during

View file

@ -174,6 +174,7 @@ pub fn check(config: &Config) -> Result {
if config.allow_registration if config.allow_registration
&& config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse && config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
&& config.registration_token.is_none() && config.registration_token.is_none()
&& config.registration_token_file.is_none()
{ {
warn!( warn!(
"Open registration is enabled via setting \ "Open registration is enabled via setting \

View file

@ -609,19 +609,25 @@ pub struct Config {
pub yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse: bool, pub yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse: bool,
/// A static registration token that new users will have to provide when /// A static registration token that new users will have to provide when
/// creating an account. If unset and `allow_registration` is true, /// creating an account. This token does not supersede tokens from other
/// you must set /// sources, such as the `!admin token` command or the
/// `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse` /// `registration_token_file` configuration option.
/// 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.
/// ///
/// example: "o&^uCtes4HPf0Vu@F20jQeeWE7" /// example: "o&^uCtes4HPf0Vu@F20jQeeWE7"
/// ///
/// display: sensitive /// display: sensitive
pub registration_token: Option<String>, pub registration_token: Option<String>,
/// A path to a file containing static registration tokens, one per line.
/// Tokens in this file do not supersede tokens from other sources, such as
/// the `!admin token` command or the `registration_token` configuration
/// option.
///
/// The file will be read once, when Continuwuity starts. It is not
/// currently reread when the server configuration is reloaded. If the file
/// cannot be read, Continuwuity will fail to start.
pub registration_token_file: Option<PathBuf>,
/// The public site key for reCaptcha. If this is provided, reCaptcha /// The public site key for reCaptcha. If this is provided, reCaptcha
/// becomes required during registration. If both captcha *and* /// becomes required during registration. If both captcha *and*
/// registration token are enabled, both will be required during /// registration token are enabled, both will be required during

View file

@ -19,10 +19,9 @@ impl Service {
/// Get the registration token set in the config file, if it exists. /// Get the registration token set in the config file, if it exists.
#[must_use] #[must_use]
pub fn get_config_file_token(&self) -> Option<ValidToken> { pub fn get_config_file_token(&self) -> Option<ValidToken> {
self.registration_token.clone().map(|token| ValidToken { self.registration_token
token, .clone()
source: ValidTokenSource::ConfigFile, .map(|token| ValidToken { token, source: ValidTokenSource::Config })
})
} }
} }

View file

@ -2,7 +2,7 @@ mod data;
use std::{future::ready, pin::Pin, sync::Arc}; use std::{future::ready, pin::Pin, sync::Arc};
use conduwuit::{Err, Result, utils}; use conduwuit::{Err, Result, err, utils};
use data::Data; use data::Data;
pub use data::{DatabaseTokenInfo, TokenExpires}; pub use data::{DatabaseTokenInfo, TokenExpires};
use futures::{ use futures::{
@ -18,6 +18,9 @@ const RANDOM_TOKEN_LENGTH: usize = 16;
pub struct Service { pub struct Service {
db: Data, db: Data,
services: Services, services: Services,
/// The registration tokens which were read from the registration token
/// file, if one is configured.
registration_tokens_from_file: Vec<String>,
} }
struct Services { struct Services {
@ -45,34 +48,54 @@ impl PartialEq<str> for ValidToken {
/// The source of a valid database token. /// The source of a valid database token.
#[derive(Debug)] #[derive(Debug)]
pub enum ValidTokenSource { pub enum ValidTokenSource {
/// The static token set in the homeserver's config file, which is /// The static token set in the homeserver's config file.
/// always valid. Config,
ConfigFile,
/// A database token which has been checked to be valid. /// A database token which has been checked to be valid.
Database(DatabaseTokenInfo), Database(DatabaseTokenInfo),
/// The single-use token which may be used to create the homeserver's first /// The single-use token which may be used to create the homeserver's first
/// account. /// account.
FirstAccount, FirstAccount,
/// A registration token read from the registration token file set in the
/// config.
TokenFile,
} }
impl std::fmt::Display for ValidTokenSource { impl std::fmt::Display for ValidTokenSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
| Self::ConfigFile => write!(f, "Token defined in config."), | Self::Config => write!(f, "Static token set in the server configuration."),
| Self::Database(info) => info.fmt(f), | Self::Database(info) => info.fmt(f),
| Self::FirstAccount => write!(f, "Initial setup token."), | Self::FirstAccount => write!(f, "Initial setup token."),
| Self::TokenFile => write!(f, "Static token set in the registration token file."),
} }
} }
} }
impl crate::Service for Service { impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> { fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
let registration_tokens_from_file = args.server.config.registration_token_file
.clone()
// If the token file option was set, read the path it points to
.map(std::fs::read_to_string)
.transpose()
.map_err(|err| err!("Failed to read registration token file: {err}"))
.map(|tokens| {
if let Some(tokens) = tokens {
// If the token file option was set, return the file's lines as tokens
tokens.lines().map(ToOwned::to_owned).collect()
} else {
// Otherwise, if the option wasn't set, return no tokens
vec![]
}
})?;
Ok(Arc::new(Self { Ok(Arc::new(Self {
db: Data::new(args.db), db: Data::new(args.db),
services: Services { services: Services {
config: args.depend::<config::Service>("config"), config: args.depend::<config::Service>("config"),
firstrun: args.depend::<firstrun::Service>("firstrun"), firstrun: args.depend::<firstrun::Service>("firstrun"),
}, },
registration_tokens_from_file,
})) }))
} }
@ -97,12 +120,23 @@ impl Service {
(token, info) (token, info)
} }
/// Get all the "special" registration tokens that aren't defined in the /// Get all the static registration tokens that aren't defined in the
/// database. /// database.
fn iterate_static_tokens(&self) -> impl Iterator<Item = ValidToken> { fn iterate_static_tokens(&self) -> impl Iterator<Item = ValidToken> {
// This does not include the first-account token, because it's special: // This does not include the first-account token, because it has special
// no other registration tokens are valid when it is set. // behavior: no other registration tokens are valid when it is set.
self.services.config.get_config_file_token().into_iter() self.services
.config
.get_config_file_token()
.into_iter()
.chain(
self.registration_tokens_from_file
.iter()
.map(|token_string| ValidToken {
token: token_string.clone(),
source: ValidTokenSource::TokenFile,
}),
)
} }
/// Validate a registration token. /// Validate a registration token.
@ -158,7 +192,7 @@ impl Service {
/// revoked. /// revoked.
pub fn revoke_token(&self, ValidToken { token, source }: ValidToken) -> Result { pub fn revoke_token(&self, ValidToken { token, source }: ValidToken) -> Result {
match source { match source {
| ValidTokenSource::ConfigFile => { | ValidTokenSource::Config => {
Err!( Err!(
"The token set in the config file cannot be revoked. Edit the config file \ "The token set in the config file cannot be revoked. Edit the config file \
to change it." to change it."
@ -171,6 +205,12 @@ impl Service {
| ValidTokenSource::FirstAccount => { | ValidTokenSource::FirstAccount => {
Err!("The initial setup token cannot be revoked.") Err!("The initial setup token cannot be revoked.")
}, },
| ValidTokenSource::TokenFile => {
Err!(
"Tokens set in the registration token file cannot be revoked. Edit the \
registration token file and restart Continuwuity to change them."
)
},
} }
} }