combine service/users data w/ mod unit split sliding sync related out of service/users instrument database entry points remove increment crap from database interface de-wrap all database get() calls de-wrap all database insert() calls de-wrap all database remove() calls refactor database interface for async streaming add query key serializer for database implement Debug for result handle add query deserializer for database add deserialization trait for option handle start a stream utils suite de-wrap/asyncify/type-query count_one_time_keys() de-wrap/asyncify users count add admin query users command suite de-wrap/asyncify users exists de-wrap/partially asyncify user filter related asyncify/de-wrap users device/keys related asyncify/de-wrap user auth/misc related asyncify/de-wrap users blurhash asyncify/de-wrap account_data get; merge Data into Service partial asyncify/de-wrap uiaa; merge Data into Service partially asyncify/de-wrap transaction_ids get; merge Data into Service partially asyncify/de-wrap key_backups; merge Data into Service asyncify/de-wrap pusher service getters; merge Data into Service asyncify/de-wrap rooms alias getters/some iterators asyncify/de-wrap rooms directory getters/iterator partially asyncify/de-wrap rooms lazy-loading partially asyncify/de-wrap rooms metadata asyncify/dewrap rooms outlier asyncify/dewrap rooms pdu_metadata dewrap/partially asyncify rooms read receipt de-wrap rooms search service de-wrap/partially asyncify rooms user service partial de-wrap rooms state_compressor de-wrap rooms state_cache de-wrap room state et al de-wrap rooms timeline service additional users device/keys related de-wrap/asyncify sender asyncify services refactor database to TryFuture/TryStream refactor services for TryFuture/TryStream asyncify api handlers additional asyncification for admin module abstract stream related; support reverse streams additional stream conversions asyncify state-res related Signed-off-by: Jason Volk <jason@zemos.net>
280 lines
8.7 KiB
Rust
280 lines
8.7 KiB
Rust
use axum::extract::State;
|
|
use axum_client_ip::InsecureClientIp;
|
|
use conduit::{debug, err, info, utils::ReadyExt, warn, Err};
|
|
use futures::StreamExt;
|
|
use ruma::{
|
|
api::client::{
|
|
error::ErrorKind,
|
|
session::{
|
|
get_login_types::{
|
|
self,
|
|
v3::{ApplicationServiceLoginType, PasswordLoginType},
|
|
},
|
|
login::{
|
|
self,
|
|
v3::{DiscoveryInfo, HomeserverInfo},
|
|
},
|
|
logout, logout_all,
|
|
},
|
|
uiaa::UserIdentifier,
|
|
},
|
|
UserId,
|
|
};
|
|
use serde::Deserialize;
|
|
|
|
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
|
use crate::{utils, utils::hash, Error, Result, Ruma};
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct Claims {
|
|
sub: String,
|
|
//exp: usize,
|
|
}
|
|
|
|
/// # `GET /_matrix/client/v3/login`
|
|
///
|
|
/// Get the supported login types of this server. One of these should be used as
|
|
/// the `type` field when logging in.
|
|
#[tracing::instrument(skip_all, fields(%client), name = "login")]
|
|
pub(crate) async fn get_login_types_route(
|
|
InsecureClientIp(client): InsecureClientIp, _body: Ruma<get_login_types::v3::Request>,
|
|
) -> Result<get_login_types::v3::Response> {
|
|
Ok(get_login_types::v3::Response::new(vec![
|
|
get_login_types::v3::LoginType::Password(PasswordLoginType::default()),
|
|
get_login_types::v3::LoginType::ApplicationService(ApplicationServiceLoginType::default()),
|
|
]))
|
|
}
|
|
|
|
/// # `POST /_matrix/client/v3/login`
|
|
///
|
|
/// Authenticates the user and returns an access token it can use in subsequent
|
|
/// requests.
|
|
///
|
|
/// - The user needs to authenticate using their password (or if enabled using a
|
|
/// json web token)
|
|
/// - If `device_id` is known: invalidates old access token of that device
|
|
/// - If `device_id` is unknown: creates a new device
|
|
/// - Returns access token that is associated with the user and device
|
|
///
|
|
/// Note: You can use [`GET
|
|
/// /_matrix/client/r0/login`](fn.get_supported_versions_route.html) to see
|
|
/// supported login types.
|
|
#[tracing::instrument(skip_all, fields(%client), name = "login")]
|
|
pub(crate) async fn login_route(
|
|
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp, body: Ruma<login::v3::Request>,
|
|
) -> Result<login::v3::Response> {
|
|
// Validate login method
|
|
// TODO: Other login methods
|
|
let user_id = match &body.login_info {
|
|
#[allow(deprecated)]
|
|
login::v3::LoginInfo::Password(login::v3::Password {
|
|
identifier,
|
|
password,
|
|
user,
|
|
..
|
|
}) => {
|
|
debug!("Got password login type");
|
|
let user_id = if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
|
|
UserId::parse_with_server_name(user_id.to_lowercase(), services.globals.server_name())
|
|
} else if let Some(user) = user {
|
|
UserId::parse(user)
|
|
} else {
|
|
warn!("Bad login type: {:?}", &body.login_info);
|
|
return Err!(Request(Forbidden("Bad login type.")));
|
|
}
|
|
.map_err(|_| Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."))?;
|
|
|
|
let hash = services
|
|
.users
|
|
.password_hash(&user_id)
|
|
.await
|
|
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?;
|
|
|
|
if hash.is_empty() {
|
|
return Err!(Request(UserDeactivated("The user has been deactivated")));
|
|
}
|
|
|
|
if hash::verify_password(password, &hash).is_err() {
|
|
return Err!(Request(Forbidden("Wrong username or password.")));
|
|
}
|
|
|
|
user_id
|
|
},
|
|
login::v3::LoginInfo::Token(login::v3::Token {
|
|
token,
|
|
}) => {
|
|
debug!("Got token login type");
|
|
if let Some(jwt_decoding_key) = services.globals.jwt_decoding_key() {
|
|
let token =
|
|
jsonwebtoken::decode::<Claims>(token, jwt_decoding_key, &jsonwebtoken::Validation::default())
|
|
.map_err(|e| {
|
|
warn!("Failed to parse JWT token from user logging in: {e}");
|
|
Error::BadRequest(ErrorKind::InvalidUsername, "Token is invalid.")
|
|
})?;
|
|
|
|
let username = token.claims.sub.to_lowercase();
|
|
|
|
UserId::parse_with_server_name(username, services.globals.server_name())
|
|
.map_err(|e| err!(Request(InvalidUsername(debug_error!(?e, "Failed to parse login username")))))?
|
|
} else {
|
|
return Err!(Request(Unknown(
|
|
"Token login is not supported (server has no jwt decoding key)."
|
|
)));
|
|
}
|
|
},
|
|
#[allow(deprecated)]
|
|
login::v3::LoginInfo::ApplicationService(login::v3::ApplicationService {
|
|
identifier,
|
|
user,
|
|
}) => {
|
|
debug!("Got appservice login type");
|
|
let user_id = if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
|
|
UserId::parse_with_server_name(user_id.to_lowercase(), services.globals.server_name())
|
|
} else if let Some(user) = user {
|
|
UserId::parse(user)
|
|
} else {
|
|
warn!("Bad login type: {:?}", &body.login_info);
|
|
return Err(Error::BadRequest(ErrorKind::forbidden(), "Bad login type."));
|
|
}
|
|
.map_err(|e| {
|
|
warn!("Failed to parse username from appservice logging in: {e}");
|
|
Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.")
|
|
})?;
|
|
|
|
if let Some(ref info) = body.appservice_info {
|
|
if !info.is_user_match(&user_id) {
|
|
return Err(Error::BadRequest(ErrorKind::Exclusive, "User is not in namespace."));
|
|
}
|
|
} else {
|
|
return Err(Error::BadRequest(ErrorKind::MissingToken, "Missing appservice token."));
|
|
}
|
|
|
|
user_id
|
|
},
|
|
_ => {
|
|
warn!("Unsupported or unknown login type: {:?}", &body.login_info);
|
|
debug!("JSON body: {:?}", &body.json_body);
|
|
return Err(Error::BadRequest(ErrorKind::Unknown, "Unsupported or unknown login type."));
|
|
},
|
|
};
|
|
|
|
// Generate new device id if the user didn't specify one
|
|
let device_id = body
|
|
.device_id
|
|
.clone()
|
|
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
|
|
|
|
// Generate a new token for the device
|
|
let token = utils::random_string(TOKEN_LENGTH);
|
|
|
|
// Determine if device_id was provided and exists in the db for this user
|
|
let device_exists = if body.device_id.is_some() {
|
|
services
|
|
.users
|
|
.all_device_ids(&user_id)
|
|
.ready_any(|v| v == device_id)
|
|
.await
|
|
} else {
|
|
false
|
|
};
|
|
|
|
if device_exists {
|
|
services
|
|
.users
|
|
.set_token(&user_id, &device_id, &token)
|
|
.await?;
|
|
} else {
|
|
services
|
|
.users
|
|
.create_device(
|
|
&user_id,
|
|
&device_id,
|
|
&token,
|
|
body.initial_device_display_name.clone(),
|
|
Some(client.to_string()),
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
// send client well-known if specified so the client knows to reconfigure itself
|
|
let client_discovery_info: Option<DiscoveryInfo> = services
|
|
.globals
|
|
.well_known_client()
|
|
.as_ref()
|
|
.map(|server| DiscoveryInfo::new(HomeserverInfo::new(server.to_string())));
|
|
|
|
info!("{user_id} logged in");
|
|
|
|
// home_server is deprecated but apparently must still be sent despite it being
|
|
// deprecated over 6 years ago. initially i thought this macro was unnecessary,
|
|
// but ruma uses this same macro for the same reason so...
|
|
#[allow(deprecated)]
|
|
Ok(login::v3::Response {
|
|
user_id,
|
|
access_token: token,
|
|
device_id,
|
|
well_known: client_discovery_info,
|
|
expires_in: None,
|
|
home_server: Some(services.globals.server_name().to_owned()),
|
|
refresh_token: None,
|
|
})
|
|
}
|
|
|
|
/// # `POST /_matrix/client/v3/logout`
|
|
///
|
|
/// Log out the current device.
|
|
///
|
|
/// - Invalidates access token
|
|
/// - Deletes device metadata (device id, device display name, last seen ip,
|
|
/// last seen ts)
|
|
/// - Forgets to-device events
|
|
/// - Triggers device list updates
|
|
#[tracing::instrument(skip_all, fields(%client), name = "logout")]
|
|
pub(crate) async fn logout_route(
|
|
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp, body: Ruma<logout::v3::Request>,
|
|
) -> Result<logout::v3::Response> {
|
|
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
|
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
|
|
|
services
|
|
.users
|
|
.remove_device(sender_user, sender_device)
|
|
.await;
|
|
|
|
// send device list update for user after logout
|
|
services.users.mark_device_key_update(sender_user).await;
|
|
|
|
Ok(logout::v3::Response::new())
|
|
}
|
|
|
|
/// # `POST /_matrix/client/r0/logout/all`
|
|
///
|
|
/// Log out all devices of this user.
|
|
///
|
|
/// - Invalidates all access tokens
|
|
/// - Deletes all device metadata (device id, device display name, last seen ip,
|
|
/// last seen ts)
|
|
/// - Forgets all to-device events
|
|
/// - Triggers device list updates
|
|
///
|
|
/// Note: This is equivalent to calling [`GET
|
|
/// /_matrix/client/r0/logout`](fn.logout_route.html) from each device of this
|
|
/// user.
|
|
#[tracing::instrument(skip_all, fields(%client), name = "logout")]
|
|
pub(crate) async fn logout_all_route(
|
|
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
|
|
body: Ruma<logout_all::v3::Request>,
|
|
) -> Result<logout_all::v3::Response> {
|
|
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
|
|
|
services
|
|
.users
|
|
.all_device_ids(sender_user)
|
|
.for_each(|device_id| services.users.remove_device(sender_user, device_id))
|
|
.await;
|
|
|
|
// send device list update for user after logout
|
|
services.users.mark_device_key_update(sender_user).await;
|
|
|
|
Ok(logout_all::v3::Response::new())
|
|
}
|