1#![deny(clippy::future_not_send)]
9#![allow(
10 clippy::unused_async,
12 clippy::too_many_arguments,
14 clippy::let_with_type_underscore,
17)]
18
19use std::{
20 convert::Infallible,
21 sync::{Arc, LazyLock},
22 time::Duration,
23};
24
25use axum::{
26 Extension, Router,
27 extract::{FromRef, FromRequestParts, OriginalUri, RawQuery, State},
28 http::Method,
29 response::{Html, IntoResponse},
30 routing::{MethodRouter, get, post},
31};
32use headers::HeaderName;
33use hyper::{
34 StatusCode, Version,
35 header::{
36 ACCEPT, ACCEPT_LANGUAGE, AUTHORIZATION, CONTENT_LANGUAGE, CONTENT_LENGTH, CONTENT_TYPE,
37 X_FRAME_OPTIONS,
38 },
39};
40use mas_axum_utils::{InternalError, cookies::CookieJar};
41use mas_data_model::SiteConfig;
42use mas_http::CorsLayerExt;
43use mas_keystore::{Encrypter, Keystore};
44use mas_matrix::HomeserverConnection;
45use mas_policy::Policy;
46use mas_router::{Route, UrlBuilder};
47use mas_storage::{BoxRepository, BoxRepositoryFactory};
48use mas_templates::{ErrorContext, NotFoundContext, TemplateContext, Templates};
49use opentelemetry::metrics::Meter;
50use sqlx::PgPool;
51use tower::util::AndThenLayer;
52use tower_http::{
53 cors::{Any, CorsLayer},
54 set_header::SetResponseHeaderLayer,
55};
56
57use self::{graphql::ExtraRouterParameters, passwords::PasswordManager};
58
59mod admin;
60mod compat;
61mod graphql;
62mod health;
63mod oauth2;
64pub mod passwords;
65pub mod upstream_oauth2;
66mod views;
67
68mod activity_tracker;
69mod captcha;
70#[cfg(test)]
71mod cleanup_tests;
72mod client_ip;
73mod preferred_language;
74mod rate_limit;
75mod room_id;
76mod session;
77#[cfg(test)]
78mod test_utils;
79mod username_template;
80
81static METER: LazyLock<Meter> = LazyLock::new(|| {
82 let scope = opentelemetry::InstrumentationScope::builder(env!("CARGO_PKG_NAME"))
83 .with_version(env!("CARGO_PKG_VERSION"))
84 .with_schema_url(opentelemetry_semantic_conventions::SCHEMA_URL)
85 .build();
86
87 opentelemetry::global::meter_with_scope(scope)
88});
89
90#[macro_export]
93macro_rules! impl_from_error_for_route {
94 ($route_error:ty : $error:ty) => {
95 impl From<$error> for $route_error {
96 fn from(e: $error) -> Self {
97 Self::Internal(Box::new(e))
98 }
99 }
100 };
101 ($error:ty) => {
102 impl_from_error_for_route!(self::RouteError: $error);
103 };
104}
105
106pub use mas_axum_utils::{ErrorWrapper, cookies::CookieManager};
107use mas_data_model::{BoxClock, BoxRng};
108
109pub use self::{
110 activity_tracker::{ActivityTracker, Bound as BoundActivityTracker},
111 admin::router as admin_api_router,
112 client_ip::ClientIp,
113 graphql::{
114 GraphQLOperation, Schema as GraphQLSchema, schema as graphql_schema,
115 schema_builder as graphql_schema_builder,
116 },
117 preferred_language::PreferredLanguage,
118 rate_limit::{Limiter, RequesterFingerprint},
119 upstream_oauth2::cache::MetadataCache,
120};
121
122pub fn healthcheck_router<S>() -> Router<S>
123where
124 S: Clone + Send + Sync + 'static,
125 PgPool: FromRef<S>,
126{
127 Router::new().route(mas_router::Healthcheck::route(), get(self::health::get))
128}
129
130pub fn graphql_router<S>(undocumented_oauth2_access: bool) -> Router<S>
131where
132 S: Clone + Send + Sync + 'static,
133 graphql::Schema: FromRef<S>,
134 BoundActivityTracker: FromRequestParts<S>,
135 BoxRepository: FromRequestParts<S>,
136 BoxClock: FromRequestParts<S>,
137 Encrypter: FromRef<S>,
138 CookieJar: FromRequestParts<S>,
139 Limiter: FromRef<S>,
140 RequesterFingerprint: FromRequestParts<S>,
141{
142 Router::new()
143 .route(
144 mas_router::GraphQL::route(),
145 get(self::graphql::get).post(self::graphql::post),
146 )
147 .layer(Extension(ExtraRouterParameters {
150 undocumented_oauth2_access,
151 }))
152 .layer(
153 CorsLayer::new()
154 .allow_origin(Any)
155 .allow_methods(Any)
156 .allow_otel_headers([
157 AUTHORIZATION,
158 ACCEPT,
159 ACCEPT_LANGUAGE,
160 CONTENT_LANGUAGE,
161 CONTENT_TYPE,
162 ]),
163 )
164}
165
166pub fn discovery_router<S>() -> Router<S>
167where
168 S: Clone + Send + Sync + 'static,
169 Keystore: FromRef<S>,
170 SiteConfig: FromRef<S>,
171 UrlBuilder: FromRef<S>,
172 BoxClock: FromRequestParts<S>,
173 BoxRng: FromRequestParts<S>,
174{
175 Router::new()
176 .route(
177 mas_router::OidcConfiguration::route(),
178 get(self::oauth2::discovery::get),
179 )
180 .route(
181 mas_router::Webfinger::route(),
182 get(self::oauth2::webfinger::get),
183 )
184 .layer(
185 CorsLayer::new()
186 .allow_origin(Any)
187 .allow_methods(Any)
188 .allow_otel_headers([
189 AUTHORIZATION,
190 ACCEPT,
191 ACCEPT_LANGUAGE,
192 CONTENT_LANGUAGE,
193 CONTENT_TYPE,
194 ])
195 .max_age(Duration::from_hours(1)),
196 )
197}
198
199pub fn api_router<S>() -> Router<S>
200where
201 S: Clone + Send + Sync + 'static,
202 Keystore: FromRef<S>,
203 UrlBuilder: FromRef<S>,
204 BoxRepository: FromRequestParts<S>,
205 ActivityTracker: FromRequestParts<S>,
206 BoundActivityTracker: FromRequestParts<S>,
207 Encrypter: FromRef<S>,
208 reqwest::Client: FromRef<S>,
209 SiteConfig: FromRef<S>,
210 Templates: FromRef<S>,
211 Arc<dyn HomeserverConnection>: FromRef<S>,
212 BoxClock: FromRequestParts<S>,
213 BoxRng: FromRequestParts<S>,
214 Policy: FromRequestParts<S>,
215{
216 Router::new()
218 .route(
219 mas_router::OAuth2Keys::route(),
220 get(self::oauth2::keys::get),
221 )
222 .route(
223 mas_router::OidcUserinfo::route(),
224 get(self::oauth2::userinfo::get).post(self::oauth2::userinfo::get),
225 )
226 .route(
227 mas_router::OAuth2Introspection::route(),
228 post(self::oauth2::introspection::post),
229 )
230 .route(
231 mas_router::OAuth2Revocation::route(),
232 post(self::oauth2::revoke::post),
233 )
234 .route(
235 mas_router::OAuth2TokenEndpoint::route(),
236 post(self::oauth2::token::post),
237 )
238 .route(
239 mas_router::OAuth2RegistrationEndpoint::route(),
240 post(self::oauth2::registration::post),
241 )
242 .route(
243 mas_router::OAuth2DeviceAuthorizationEndpoint::route(),
244 post(self::oauth2::device::authorize::post),
245 )
246 .layer(
247 CorsLayer::new()
248 .allow_origin(Any)
249 .allow_methods(Any)
250 .allow_otel_headers([
251 AUTHORIZATION,
252 ACCEPT,
253 ACCEPT_LANGUAGE,
254 CONTENT_LANGUAGE,
255 CONTENT_TYPE,
256 HeaderName::from_static("x-requested-with"),
258 ])
259 .max_age(Duration::from_hours(1)),
260 )
261}
262
263pub fn compat_router<S>(templates: Templates) -> Router<S>
264where
265 S: Clone + Send + Sync + 'static,
266 UrlBuilder: FromRef<S>,
267 SiteConfig: FromRef<S>,
268 Arc<dyn HomeserverConnection>: FromRef<S>,
269 PasswordManager: FromRef<S>,
270 Limiter: FromRef<S>,
271 BoxRepositoryFactory: FromRef<S>,
272 BoundActivityTracker: FromRequestParts<S>,
273 RequesterFingerprint: FromRequestParts<S>,
274 BoxRepository: FromRequestParts<S>,
275 BoxClock: FromRequestParts<S>,
276 BoxRng: FromRequestParts<S>,
277 Policy: FromRequestParts<S>,
278{
279 let human_router = Router::new()
281 .route(
282 mas_router::CompatLoginSsoRedirect::route(),
283 get(self::compat::login_sso_redirect::get),
284 )
285 .route(
286 mas_router::CompatLoginSsoRedirectIdp::route(),
287 get(self::compat::login_sso_redirect::get),
288 )
289 .route(
290 mas_router::CompatLoginSsoRedirectSlash::route(),
291 get(self::compat::login_sso_redirect::get),
292 )
293 .layer(AndThenLayer::new(
294 async move |response: axum::response::Response| {
295 Ok::<_, Infallible>(recover_error(&templates, response))
296 },
297 ));
298
299 let api_router = Router::new()
301 .route(
302 mas_router::CompatLogin::route(),
303 get(self::compat::login::get).post(self::compat::login::post),
304 )
305 .route(
306 mas_router::CompatLogout::route(),
307 post(self::compat::logout::post),
308 )
309 .route(
310 mas_router::CompatLogoutAll::route(),
311 post(self::compat::logout_all::post),
312 )
313 .route(
314 mas_router::CompatRefresh::route(),
315 post(self::compat::refresh::post),
316 )
317 .layer(
318 CorsLayer::new()
319 .allow_origin(Any)
320 .allow_methods(Any)
321 .allow_otel_headers([
322 AUTHORIZATION,
323 ACCEPT,
324 ACCEPT_LANGUAGE,
325 CONTENT_LANGUAGE,
326 CONTENT_TYPE,
327 HeaderName::from_static("x-requested-with"),
328 ])
329 .max_age(Duration::from_hours(1)),
330 );
331
332 Router::new().merge(human_router).merge(api_router)
333}
334
335fn redirect_to<S>(route: &'static str) -> MethodRouter<S>
337where
338 S: Clone + Send + Sync + 'static,
339 UrlBuilder: FromRef<S>,
340{
341 get(
342 async move |State(url_builder): State<UrlBuilder>, RawQuery(query): RawQuery| {
343 let prefix = url_builder.prefix().unwrap_or_default();
344 let destination = if let Some(query) = query {
345 format!("{prefix}{route}?{query}")
346 } else {
347 format!("{prefix}{route}")
348 };
349
350 axum::response::Redirect::to(&destination)
351 },
352 )
353}
354
355pub fn human_router<S>(templates: Templates) -> Router<S>
356where
357 S: Clone + Send + Sync + 'static,
358 UrlBuilder: FromRef<S>,
359 PreferredLanguage: FromRequestParts<S>,
360 BoxRepository: FromRequestParts<S>,
361 CookieJar: FromRequestParts<S>,
362 BoundActivityTracker: FromRequestParts<S>,
363 RequesterFingerprint: FromRequestParts<S>,
364 Encrypter: FromRef<S>,
365 Templates: FromRef<S>,
366 Keystore: FromRef<S>,
367 PasswordManager: FromRef<S>,
368 MetadataCache: FromRef<S>,
369 SiteConfig: FromRef<S>,
370 Limiter: FromRef<S>,
371 reqwest::Client: FromRef<S>,
372 Arc<dyn HomeserverConnection>: FromRef<S>,
373 BoxClock: FromRequestParts<S>,
374 BoxRng: FromRequestParts<S>,
375 Policy: FromRequestParts<S>,
376{
377 Router::new()
378 .route("/account", redirect_to(mas_router::Account::route()))
380 .route(mas_router::Account::route(), get(self::views::app::get))
381 .route(
382 mas_router::AccountWildcard::route(),
383 get(self::views::app::get),
384 )
385 .route(
386 mas_router::AccountRecoveryFinish::route(),
387 get(self::views::app::get_anonymous),
388 )
389 .route(
390 mas_router::ChangePasswordDiscovery::route(),
391 get(async |State(url_builder): State<UrlBuilder>| {
392 url_builder.redirect(&mas_router::AccountPasswordChange)
393 }),
394 )
395 .route(mas_router::Index::route(), get(self::views::index::get))
396 .route(
397 mas_router::Login::route(),
398 get(self::views::login::get).post(self::views::login::post),
399 )
400 .route(mas_router::Logout::route(), post(self::views::logout::post))
401 .route(
402 mas_router::OpenInviteRoom::route(),
403 get(self::views::open_room::get),
404 )
405 .route(
406 mas_router::Register::route(),
407 get(self::views::register::get).post(self::views::register::post),
408 )
409 .route(
411 "/register/password",
412 redirect_to(mas_router::Register::route()),
413 )
414 .route(
415 mas_router::RegisterVerifyEmail::route(),
416 get(self::views::register::steps::verify_email::get)
417 .post(self::views::register::steps::verify_email::post),
418 )
419 .route(
420 mas_router::RegisterToken::route(),
421 get(self::views::register::steps::registration_token::get)
422 .post(self::views::register::steps::registration_token::post),
423 )
424 .route(
425 mas_router::RegisterDisplayName::route(),
426 get(self::views::register::steps::display_name::get)
427 .post(self::views::register::steps::display_name::post),
428 )
429 .route(
430 mas_router::RegisterFinish::route(),
431 get(self::views::register::steps::finish::get),
432 )
433 .route(
434 mas_router::AccountRecoveryStart::route(),
435 get(self::views::recovery::start::get).post(self::views::recovery::start::post),
436 )
437 .route(
438 mas_router::AccountRecoveryProgress::route(),
439 get(self::views::recovery::progress::get).post(self::views::recovery::progress::post),
440 )
441 .route(
442 mas_router::OAuth2AuthorizationEndpoint::route(),
443 get(self::oauth2::authorization::get),
444 )
445 .route(
446 mas_router::Consent::route(),
447 get(self::oauth2::authorization::consent::get)
448 .post(self::oauth2::authorization::consent::post),
449 )
450 .route(
451 mas_router::CompatLoginSsoComplete::route(),
452 get(self::compat::login_sso_complete::get).post(self::compat::login_sso_complete::post),
453 )
454 .route(
455 mas_router::UpstreamOAuth2Authorize::route(),
456 get(self::upstream_oauth2::authorize::get),
457 )
458 .route(
459 mas_router::UpstreamOAuth2Callback::route(),
460 get(self::upstream_oauth2::callback::handler)
461 .post(self::upstream_oauth2::callback::handler),
462 )
463 .route(
464 mas_router::UpstreamOAuth2Link::route(),
465 get(self::upstream_oauth2::link::get).post(self::upstream_oauth2::link::post),
466 )
467 .route(
468 mas_router::UpstreamOAuth2BackchannelLogout::route(),
469 post(self::upstream_oauth2::backchannel_logout::post),
470 )
471 .route(
472 mas_router::DeviceCodeLink::route(),
473 get(self::oauth2::device::link::get).post(self::oauth2::device::link::post),
474 )
475 .route(
476 mas_router::DeviceCodeConsent::route(),
477 get(self::oauth2::device::consent::get).post(self::oauth2::device::consent::post),
478 )
479 .layer(AndThenLayer::new(
480 async move |response: axum::response::Response| {
481 Ok::<_, Infallible>(recover_error(&templates, response))
482 },
483 ))
484 .layer(SetResponseHeaderLayer::if_not_present(
485 X_FRAME_OPTIONS,
486 http::HeaderValue::from_static("DENY"),
487 ))
488}
489
490fn recover_error(
491 templates: &Templates,
492 response: axum::response::Response,
493) -> axum::response::Response {
494 let ext = response.extensions().get::<ErrorContext>();
496 if let Some(ctx) = ext
497 && let Ok(res) = templates.render_error(ctx)
498 {
499 let (mut parts, _original_body) = response.into_parts();
500 parts.headers.remove(CONTENT_TYPE);
501 parts.headers.remove(CONTENT_LENGTH);
502 return (parts, Html(res)).into_response();
503 }
504
505 response
506}
507
508pub async fn fallback(
514 State(templates): State<Templates>,
515 OriginalUri(uri): OriginalUri,
516 method: Method,
517 version: Version,
518 PreferredLanguage(locale): PreferredLanguage,
519) -> Result<impl IntoResponse, InternalError> {
520 let ctx = NotFoundContext::new(&method, version, &uri).with_language(locale);
521 let res = templates.render_not_found(&ctx)?;
524
525 Ok((StatusCode::NOT_FOUND, Html(res)))
526}