This commit is contained in:
timokoesters 2020-03-29 21:05:20 +02:00
parent 18ed991b93
commit 533260edd8
No known key found for this signature in database
GPG key ID: 24DA7517711A2BA4
6 changed files with 274 additions and 94 deletions

26
Cargo.lock generated
View file

@ -497,6 +497,7 @@ dependencies = [
"ruma-client-api",
"ruma-events",
"ruma-identifiers",
"serde_json",
"sled",
]
@ -807,9 +808,9 @@ dependencies = [
[[package]]
name = "ruma-api"
version = "0.15.0-dev.1"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44987d5fefcf801a6fb5c5843c17f876a53852fa07e5e4d99e0dca3670f1441a"
checksum = "120f0cd8625b842423ef3a63cabb8c309ca35a02de87cc4b377fb2cdd43f1fe5"
dependencies = [
"http",
"percent-encoding 2.1.0",
@ -824,9 +825,9 @@ dependencies = [
[[package]]
name = "ruma-api-macros"
version = "0.12.0-dev.1"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36931db94874129f9202f650d91447d8317b099bae1e12cdd5769ba4eced07d2"
checksum = "bfc523efc9c1ba7033ff17888551c1d378e12eae087cfbe4fcee938ff516759e"
dependencies = [
"proc-macro2 1.0.9",
"quote 1.0.3",
@ -835,8 +836,9 @@ dependencies = [
[[package]]
name = "ruma-client-api"
version = "0.6.0"
source = "git+https://github.com/ruma/ruma-client-api#57f5e8d66168a54128426c8e34b26fa78f739c3e"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a64241cdc0cff76038484451d7a5d2689f8ea4e59b6695cd3c8448af7bcc016"
dependencies = [
"http",
"js_int",
@ -851,9 +853,9 @@ dependencies = [
[[package]]
name = "ruma-events"
version = "0.17.0"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11951235b25c72a82eb988aabf5af23cae883562665e0cb73954ffe4ae81f11c"
checksum = "80e34bfc20462f18d7f0beb6f1863db62d29438f2dcf390b625e9b20696cb2b3"
dependencies = [
"js_int",
"ruma-events-macros",
@ -864,9 +866,9 @@ dependencies = [
[[package]]
name = "ruma-events-macros"
version = "0.2.0"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "962d93056619ed61826a9d8872c863560e4892ff6a69b70f593baa5ae8b19dc8"
checksum = "ff95b6b4480c570db471b490b35ad70add5470651654e75faf0b97052b4f29e1"
dependencies = [
"proc-macro2 1.0.9",
"quote 1.0.3",
@ -994,9 +996,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.49"
version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02044a6a92866fd61624b3db4d2c9dccc2feabbc6be490b87611bf285edbac55"
checksum = "78a7a12c167809363ec3bd7329fc0a3369056996de43c4b37ef3cd54a6ce4867"
dependencies = [
"itoa",
"ryu",

View file

@ -9,12 +9,13 @@ edition = "2018"
[dependencies]
rocket = { git = "https://github.com/SergioBenitez/Rocket.git", features = ["tls"] }
http = "0.2.1"
ruma-client-api = { git = "https://github.com/ruma/ruma-client-api" }
ruma-client-api = "0.7.0"
pretty_env_logger = "0.4.0"
log = "0.4.8"
sled = "0.31.0"
directories = "2.0.2"
ruma-identifiers = "0.14.1"
ruma-api = "0.15.0-dev.1"
ruma-events = "0.17.0"
ruma-api = "0.15.0"
ruma-events = "0.18.0"
js_int = "0.1.3"
serde_json = "1.0.50"

View file

@ -1,16 +1,18 @@
use crate::utils;
use directories::ProjectDirs;
use ruma_events::collections::all::RoomEvent;
use ruma_identifiers::UserId;
use std::convert::TryInto;
const USERID_PASSWORD: &str = "userid_password";
const USERID_DEVICEIDS: &str = "userid_deviceids";
const DEVICEID_TOKEN: &str = "deviceid_token";
const TOKEN_USERID: &str = "token_userid";
pub struct Data(sled::Db);
impl Data {
pub fn set_hostname(&self, hostname: &str) {
self.0.insert("hostname", hostname).unwrap();
}
pub fn hostname(&self) -> String {
String::from_utf8(self.0.get("hostname").unwrap().unwrap().to_vec()).unwrap()
}
/// Load an existing database or create a new one.
pub fn load_or_create() -> Self {
Data(
sled::open(
@ -22,21 +24,109 @@ impl Data {
)
}
/// Set the hostname of the server. Warning: Hostname changes will likely break things.
pub fn set_hostname(&self, hostname: &str) {
self.0.insert("hostname", hostname).unwrap();
}
/// Get the hostname of the server.
pub fn hostname(&self) -> String {
utils::bytes_to_string(&self.0.get("hostname").unwrap().unwrap())
}
/// Check if a user has an account by looking for an assigned password.
pub fn user_exists(&self, user_id: &UserId) -> bool {
self.0
.open_tree("username_password")
.open_tree(USERID_PASSWORD)
.unwrap()
.contains_key(user_id.to_string())
.unwrap()
}
pub fn user_add(&self, user_id: UserId, password: Option<String>) {
/// Create a new user account by assigning them a password.
pub fn user_add(&self, user_id: &UserId, password: Option<String>) {
self.0
.open_tree("username_password")
.open_tree(USERID_PASSWORD)
.unwrap()
.insert(user_id.to_string(), &*password.unwrap_or_default())
.unwrap();
}
pub fn room_event_add(&self, room_event: &RoomEvent) {}
/// Find out which user an access token belongs to.
pub fn user_from_token(&self, token: &str) -> Option<UserId> {
self.0
.open_tree(TOKEN_USERID)
.unwrap()
.get(token)
.unwrap()
.and_then(|bytes| (*utils::bytes_to_string(&bytes)).try_into().ok())
}
/// Checks if the given password is equal to the one in the database.
pub fn password_get(&self, user_id: &UserId) -> Option<String> {
self.0
.open_tree(USERID_PASSWORD)
.unwrap()
.get(user_id.to_string())
.unwrap()
.map(|bytes| utils::bytes_to_string(&bytes))
}
/// Add a new device to a user.
pub fn device_add(&self, user_id: &UserId, device_id: &str) {
self.0
.open_tree(USERID_DEVICEIDS)
.unwrap()
.insert(user_id.to_string(), device_id)
.unwrap();
}
/// Replace the access token of one device.
pub fn token_replace(&self, user_id: &UserId, device_id: &String, token: String) {
// Make sure the device id belongs to the user
debug_assert!(self
.0
.open_tree(USERID_DEVICEIDS)
.unwrap()
.get(&user_id.to_string()) // Does the user exist?
.unwrap()
.map(|bytes| utils::bytes_to_vec(&bytes))
.filter(|devices| devices.contains(device_id)) // Does the user have that device?
.is_some());
// Remove old token
if let Some(old_token) = self
.0
.open_tree(DEVICEID_TOKEN)
.unwrap()
.get(device_id)
.unwrap()
{
self.0
.open_tree(TOKEN_USERID)
.unwrap()
.remove(old_token)
.unwrap();
// It will be removed from DEVICEID_TOKEN by the insert later
}
// Assign token to device_id
self.0
.open_tree(DEVICEID_TOKEN)
.unwrap()
.insert(device_id, &*token)
.unwrap();
// Assign token to user
self.0
.open_tree(TOKEN_USERID)
.unwrap()
.insert(token, &*user_id.to_string())
.unwrap();
}
/// Create a new room event.
pub fn room_event_add(&self, _room_event: &RoomEvent) {
todo!();
}
}

View file

@ -14,9 +14,10 @@ use ruma_client_api::{
},
unversioned::get_supported_versions,
};
use ruma_events::room::message::MessageEvent;
use ruma_events::{room::message::MessageEvent, EventResult};
use ruma_identifiers::{EventId, UserId};
use ruma_wrapper::{MatrixResult, Ruma};
use serde_json::map::Map;
use std::convert::TryFrom;
use std::{collections::HashMap, convert::TryInto};
@ -41,6 +42,7 @@ fn register_route(
data: State<Data>,
body: Ruma<register::Request>,
) -> MatrixResult<register::Response> {
// Validate user id
let user_id: UserId = match (*format!(
"@{}:{}",
body.username.clone().unwrap_or("randomname".to_owned()),
@ -59,6 +61,7 @@ fn register_route(
Ok(user_id) => user_id,
};
// Check if username is creative enough
if data.user_exists(&user_id) {
debug!("ID already taken");
return MatrixResult(Err(Error {
@ -68,68 +71,115 @@ fn register_route(
}));
}
data.user_add(user_id.clone(), body.password.clone());
// Create user
data.user_add(&user_id, body.password.clone());
// Generate new device id if the user didn't specify one
let device_id = body
.device_id
.clone()
.unwrap_or_else(|| "TODO:randomdeviceid".to_owned());
// Add device
data.device_add(&user_id, &device_id);
// Generate new token for the device
let token = "TODO:randomtoken".to_owned();
data.token_replace(&user_id, &device_id, token.clone());
MatrixResult(Ok(register::Response {
access_token: "randomtoken".to_owned(),
access_token: token,
home_server: data.hostname(),
user_id,
device_id: body.device_id.clone().unwrap_or("randomid".to_owned()),
device_id,
}))
}
#[post("/_matrix/client/r0/login", data = "<body>")]
fn login_route(data: State<Data>, body: Ruma<login::Request>) -> MatrixResult<login::Response> {
let username = if let login::UserInfo::MatrixId(mut username) = body.user.clone() {
if !username.contains(':') {
username = format!("@{}:{}", username, data.hostname());
}
if let Ok(user_id) = (*username).try_into() {
if !data.user_exists(&user_id) {
debug!("Userid does not exist. Can't log in.");
// Validate login method
let user_id =
if let (login::UserInfo::MatrixId(mut username), login::LoginInfo::Password { password }) =
(body.user.clone(), body.login_info.clone())
{
if !username.contains(':') {
username = format!("@{}:{}", username, data.hostname());
}
if let Ok(user_id) = (*username).try_into() {
if !data.user_exists(&user_id) {}
// Check password
if let Some(correct_password) = data.password_get(&user_id) {
if password == correct_password {
// Success!
user_id
} else {
debug!("Invalid password.");
return MatrixResult(Err(Error {
kind: ErrorKind::Unknown,
message: "".to_owned(),
status_code: http::StatusCode::FORBIDDEN,
}));
}
} else {
debug!("UserId does not exist (has no assigned password). Can't log in.");
return MatrixResult(Err(Error {
kind: ErrorKind::Forbidden,
message: "".to_owned(),
status_code: http::StatusCode::FORBIDDEN,
}));
}
} else {
debug!("Invalid UserId.");
return MatrixResult(Err(Error {
kind: ErrorKind::Forbidden,
message: "UserId not found.".to_owned(),
kind: ErrorKind::Unknown,
message: "Bad login type.".to_owned(),
status_code: http::StatusCode::BAD_REQUEST,
}));
}
user_id
} else {
debug!("Invalid UserId.");
debug!("Bad login type");
return MatrixResult(Err(Error {
kind: ErrorKind::Unknown,
message: "Bad login type.".to_owned(),
status_code: http::StatusCode::BAD_REQUEST,
}));
}
} else {
debug!("Bad login type");
return MatrixResult(Err(Error {
kind: ErrorKind::Unknown,
message: "Bad login type.".to_owned(),
status_code: http::StatusCode::BAD_REQUEST,
}));
};
};
// Generate new device id if the user didn't specify one
let device_id = body
.device_id
.clone()
.unwrap_or("TODO:randomdeviceid".to_owned());
// Add device (TODO: We might not want to call it when using an existing device)
data.device_add(&user_id, &device_id);
// Generate a new token for the device
let token = "TODO:randomtoken".to_owned();
data.token_replace(&user_id, &device_id, token.clone());
return MatrixResult(Ok(login::Response {
user_id: username.try_into().unwrap(), // Unwrap is okay because the user is already registered
access_token: "randomtoken".to_owned(),
home_server: Some("localhost".to_owned()),
device_id: body.device_id.clone().unwrap_or("randomid".to_owned()),
user_id,
access_token: token,
home_server: Some(data.hostname()),
device_id,
well_known: None,
}));
}
#[get("/_matrix/client/r0/directory/room/<room_alias>")]
fn get_alias_route(room_alias: String) -> MatrixResult<get_alias::Response> {
// TODO
let room_id = match &*room_alias {
"#room:localhost" => "!xclkjvdlfj:localhost",
_ => {
debug!("Room not found.");
return MatrixResult(Err(Error {
kind: ErrorKind::NotFound,
message: "Room not found.".to_owned(),
status_code: http::StatusCode::NOT_FOUND,
}))
}));
}
}
.try_into()
@ -146,6 +196,7 @@ fn join_room_by_id_route(
_room_id: String,
body: Ruma<join_room_by_id::Request>,
) -> MatrixResult<join_room_by_id::Response> {
// TODO
MatrixResult(Ok(join_room_by_id::Response {
room_id: body.room_id.clone(),
}))
@ -162,23 +213,34 @@ fn create_message_event_route(
_txn_id: String,
body: Ruma<create_message_event::Request>,
) -> MatrixResult<create_message_event::Response> {
dbg!(&body);
if let Ok(content) = body.data.clone().into_result() {
data.room_event_add(
&MessageEvent {
content,
event_id: EventId::try_from("$randomeventid:localhost").unwrap(),
origin_server_ts: utils::millis_since_unix_epoch(),
room_id: Some(body.room_id.clone()),
sender: UserId::try_from("@TODO:localhost").unwrap(),
unsigned: None,
}
.into(),
);
}
MatrixResult(Ok(create_message_event::Response {
event_id: "$randomeventid:localhost".try_into().unwrap(),
}))
// Check if content is valid
let content = match body.data.clone() {
EventResult::Ok(content) => content,
EventResult::Err(_) => {
debug!("No content.");
return MatrixResult(Err(Error {
kind: ErrorKind::NotFound,
message: "No content.".to_owned(),
status_code: http::StatusCode::BAD_REQUEST,
}));
}
};
let event_id = EventId::try_from("$TODOrandomeventid:localhost").unwrap();
data.room_event_add(
&MessageEvent {
content,
event_id: event_id.clone(),
origin_server_ts: utils::millis_since_unix_epoch(),
room_id: Some(body.room_id.clone()),
sender: body.user_id.expect("user is authenticated"),
unsigned: Map::default(),
}
.into(),
);
MatrixResult(Ok(create_message_event::Response { event_id }))
}
fn main() {

View file

@ -10,10 +10,10 @@ use {
Endpoint, Outgoing,
},
ruma_client_api::error::Error,
ruma_identifiers::UserId,
std::ops::Deref,
std::{
convert::{TryFrom, TryInto},
fmt,
io::{Cursor, Read},
},
};
@ -22,9 +22,10 @@ const MESSAGE_LIMIT: u64 = 65535;
/// This struct converts rocket requests into ruma structs by converting them into http requests
/// first.
#[derive(Debug)]
pub struct Ruma<T: Outgoing> {
body: T::Incoming,
headers: http::HeaderMap<http::header::HeaderValue>,
pub user_id: Option<UserId>,
}
impl<T: Endpoint> FromDataSimple for Ruma<T>
@ -37,9 +38,34 @@ where
Error = FromHttpResponseError<<T as Endpoint>::ResponseError>,
>,
{
type Error = ();
type Error = (); // TODO: Better error handling
fn from_data(request: &Request, data: rocket::Data) -> Outcome<Self, Self::Error> {
let user_id = if T::METADATA.requires_authentication {
let data = request.guard::<State<crate::Data>>().unwrap();
// Get token from header or query value
let token = match request
.headers()
.get_one("Authorization")
.map(|s| s.to_owned())
.or_else(|| request.get_query_value("access_token").and_then(|r| r.ok()))
{
// TODO: M_MISSING_TOKEN
None => return Failure((Status::Unauthorized, ())),
Some(token) => token,
};
// Check if token is valid
match data.user_from_token(&token) {
// TODO: M_UNKNOWN_TOKEN
None => return Failure((Status::Unauthorized, ())),
Some(user_id) => Some(user_id),
}
} else {
None
};
let mut http_request = http::Request::builder()
.uri(request.uri().to_string())
.method(&*request.method().to_string());
@ -52,17 +78,10 @@ where
handle.read_to_end(&mut body).unwrap();
let http_request = http_request.body(body).unwrap();
let headers = http_request.headers().clone();
log::info!("{:?}", http_request);
match T::Incoming::try_from(http_request) {
Ok(t) => {
if T::METADATA.requires_authentication {
let data = request.guard::<State<crate::Data>>();
// TODO: auth
}
Success(Ruma { body: t, headers })
}
Ok(t) => Success(Ruma { body: t, user_id }),
Err(e) => {
log::error!("{:?}", e);
Failure((Status::InternalServerError, ()))
@ -79,18 +98,6 @@ impl<T: Outgoing> Deref for Ruma<T> {
}
}
impl<T: Outgoing> fmt::Debug for Ruma<T>
where
T::Incoming: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Ruma")
.field("body", &self.body)
.field("headers", &self.headers)
.finish()
}
}
/// This struct converts ruma responses into rocket http responses.
pub struct MatrixResult<T>(pub std::result::Result<T, Error>);
impl<T: TryInto<http::Response<Vec<u8>>>> TryInto<http::Response<Vec<u8>>> for MatrixResult<T> {

View file

@ -7,3 +7,21 @@ pub fn millis_since_unix_epoch() -> js_int::UInt {
.as_millis() as u32)
.into()
}
pub fn bytes_to_string(bytes: &[u8]) -> String {
String::from_utf8(bytes.to_vec()).expect("convert bytes to string")
}
pub fn vec_to_bytes(vec: Vec<String>) -> Vec<u8> {
vec.into_iter()
.map(|string| string.into_bytes())
.collect::<Vec<Vec<u8>>>()
.join(&0)
}
pub fn bytes_to_vec(bytes: &[u8]) -> Vec<String> {
bytes
.split(|&b| b == 0)
.map(|bytes_string| bytes_to_string(bytes_string))
.collect::<Vec<String>>()
}