Skip to main content

tuwunel_service/registration_tokens/
mod.rs

1//! Registration token validation and lifecycle management.
2//!
3//! The service combines static configuration tokens with database-backed tokens whose stored use
4//! count or expiration time can invalidate them. Validation, consumption, listing, update, and
5//! revocation share one interface.
6
7mod data;
8
9use std::{collections::HashSet, fmt::Display, sync::Arc};
10
11use data::Data;
12/// Database-backed registration token metadata and expiration policy.
13///
14/// These types form the stored representation and are also exposed to administrative callers.
15pub use data::{DatabaseTokenInfo, TokenExpires};
16use futures::{Stream, StreamExt, pin_mut};
17use tuwunel_core::{
18	Err, Result, error,
19	utils::{IterStream, random_string},
20};
21
22const RANDOM_TOKEN_LENGTH: usize = 16;
23
24/// Manages configured and database-backed registration tokens.
25///
26/// Configured tokens are read from the active configuration and optional token file on demand.
27/// Database tokens retain use counts and expiration limits in persistent storage.
28pub struct Service {
29	db: Data,
30	services: Arc<crate::services::OnceServices>,
31}
32
33/// Registration token returned by the valid-token stream.
34///
35/// The accompanying metadata identifies whether the token came from configuration or persistent
36/// storage.
37#[derive(Debug)]
38pub struct ValidToken {
39	/// Literal token accepted during registration.
40	pub token: String,
41
42	/// Origin and metadata associated with the token.
43	pub info: TokenInfo,
44}
45
46impl Display for ValidToken {
47	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48		write!(f, "`{}` --- {}", self.token, self.info)
49	}
50}
51
52impl PartialEq<str> for ValidToken {
53	fn eq(&self, other: &str) -> bool { self.token == other }
54}
55
56/// Describes the origin and stored metadata of a registration token.
57///
58/// Configuration tokens have no mutable counters or expiry. Database tokens carry their current
59/// stored metadata, which callers may inspect independently of validity checks.
60#[derive(Clone, Copy, Debug)]
61pub enum TokenInfo {
62	/// Static token supplied by the homeserver configuration.
63	Config,
64
65	/// Metadata loaded for a database-backed token.
66	Database(DatabaseTokenInfo),
67}
68
69impl Display for TokenInfo {
70	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71		match self {
72			| Self::Config => write!(f, "Token defined in config file"),
73			| Self::Database(info) => info.fmt(f),
74		}
75	}
76}
77
78impl crate::Service for Service {
79	fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
80		Ok(Arc::new(Self {
81			db: Data::new(args.db),
82			services: args.services.clone(),
83		}))
84	}
85
86	fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
87}
88
89impl Service {
90	/// Creates a database-backed registration token.
91	///
92	/// A supplied token is stored verbatim; otherwise a random token of `length` characters is
93	/// generated, defaulting to the service length. Existing database tokens are rejected.
94	pub async fn create_token(
95		&self,
96		token: Option<&str>,
97		length: Option<usize>,
98		expires: TokenExpires,
99	) -> Result<(String, DatabaseTokenInfo)> {
100		let token = token.map(ToOwned::to_owned).unwrap_or_else(|| {
101			let length = length.unwrap_or(RANDOM_TOKEN_LENGTH);
102
103			random_string(length)
104		});
105
106		let info = self.db.save_token(&token, expires).await?;
107
108		Ok((token, info))
109	}
110
111	/// Returns a token's origin and stored metadata.
112	///
113	/// Configured tokens return [`TokenInfo::Config`]. Database metadata is returned without a
114	/// validity check, while unknown tokens produce a not-found request error.
115	pub async fn get_token_info(&self, token: &str) -> Result<TokenInfo> {
116		if self.get_config_tokens().await.contains(token) {
117			return Ok(TokenInfo::Config);
118		}
119
120		self.db
121			.get_token_info(token)
122			.await
123			.map(TokenInfo::Database)
124	}
125
126	/// Replaces a database token's expiration policy.
127	///
128	/// The existing use counter is preserved. Configured tokens cannot be updated, and unknown
129	/// database tokens produce a not-found request error.
130	pub async fn update_token(
131		&self,
132		token: &str,
133		expires: TokenExpires,
134	) -> Result<DatabaseTokenInfo> {
135		if self.get_config_tokens().await.contains(token) {
136			return Err!(Request(Forbidden(
137				"The token set in the config file cannot be updated"
138			)));
139		}
140
141		self.db.update_token(token, expires).await
142	}
143
144	/// Reports whether at least one valid registration token is available.
145	///
146	/// The check stops at the first configured or valid stored token. Invalid database entries
147	/// encountered before that result are removed as the stream advances.
148	pub async fn is_enabled(&self) -> bool {
149		let stream = self.iterate_tokens().await;
150
151		pin_mut!(stream);
152
153		stream.next().await.is_some()
154	}
155
156	/// Loads every registration token supplied by configuration.
157	///
158	/// Whitespace-delimited tokens are read from the optional token file and combined with the inline
159	/// token. Failure to read the file is logged and leaves only other configured tokens.
160	pub async fn get_config_tokens(&self) -> HashSet<String> {
161		let mut tokens = HashSet::new();
162
163		if let Some(file) = &self.services.config.registration_token_file {
164			match tokio::fs::read_to_string(file).await {
165				| Err(e) => error!("Failed to read the registration token file: {e}"),
166				| Ok(text) => tokens.extend(
167					text.split_ascii_whitespace()
168						.map(ToOwned::to_owned),
169				),
170			}
171		}
172
173		if let Some(token) = &self.services.config.registration_token {
174			tokens.insert(token.to_owned());
175		}
176
177		tokens
178	}
179
180	/// Validates a registration token without consuming a use.
181	///
182	/// Configuration tokens are always valid. Invalid database tokens are removed and reported as a
183	/// forbidden request.
184	pub async fn is_token_valid(&self, token: &str) -> Result { self.check(token, false).await }
185
186	/// Validates a registration token and consumes one permitted use.
187	///
188	/// Configuration tokens are accepted without mutation. A database token is removed when its
189	/// updated count reaches the stored threshold. Concurrent consumers are not serialized and can
190	/// share a prior count. Invalid tokens are reported as forbidden.
191	pub async fn try_consume(&self, token: &str) -> Result { self.check(token, true).await }
192
193	async fn check(&self, token: &str, consume: bool) -> Result {
194		if self.get_config_tokens().await.contains(token)
195			|| self.db.check_token(token, consume).await
196		{
197			return Ok(());
198		}
199
200		Err!(Request(Forbidden("Registration token not valid")))
201	}
202
203	/// Revokes a database-backed registration token.
204	///
205	/// Configuration tokens cannot be revoked through this service. An unknown database token
206	/// produces a not-found request error.
207	pub async fn revoke_token(&self, token: &str) -> Result {
208		if self.get_config_tokens().await.contains(token) {
209			return Err!(Request(Forbidden(
210				"The token set in the config file cannot be revoked. Edit the config file to \
211				 change it."
212			)));
213		}
214
215		self.db.revoke_token(token).await
216	}
217
218	/// Streams every currently valid registration token.
219	///
220	/// Configuration tokens are yielded first, followed by valid database tokens. Invalid stored
221	/// tokens are removed while the database stream is consumed.
222	pub async fn iterate_tokens(&self) -> impl Stream<Item = ValidToken> + Send + '_ {
223		let config_tokens = self
224			.get_config_tokens()
225			.await
226			.into_iter()
227			.map(|token| ValidToken { token, info: TokenInfo::Config })
228			.stream();
229
230		let db_tokens = self
231			.db
232			.iterate_and_clean_tokens()
233			.map(|(token, info)| ValidToken {
234				token: token.to_owned(),
235				info: TokenInfo::Database(info),
236			});
237
238		config_tokens.chain(db_tokens)
239	}
240}