chore: Initialize the project

Signed-off-by: Awiteb <a@4rs.nl>
This commit is contained in:
Awiteb 2024-06-26 23:05:17 +03:00
parent a9ba2c4f31
commit 7e3558d776
Signed by: awiteb
GPG key ID: 3F6B55640AA6682F
42 changed files with 7458 additions and 0 deletions

1
.dockerignore Normal file
View file

@ -0,0 +1 @@
/target

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/target
# Ignore the server configration file (Used for local development only)
config.toml

4461
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

30
Cargo.toml Normal file
View file

@ -0,0 +1,30 @@
[workspace]
members = ["crates/*"]
resolver = "2"
[workspace.package]
authors = ["OxideTalis Developers <otmp@4rs.nl>"]
readme = "README.md"
repository = "https://git.4rs.nl/oxidetalis/oxidetalis"
version = "0.1.0"
rust-version = "1.76.0"
[workspace.dependencies]
# Local crates
oxidetalis_core = { path = "crates/oxidetalis_core" }
oxidetalis_config = { path = "crates/oxidetalis_config" }
oxidetalis_migrations = { path = "crates/oxidetalis_migrations" }
oxidetalis_entities = { path = "crates/oxidetalis_entities" }
# Shered dependencies
base58 = "0.2.0"
serde = "1.0.203"
thiserror = "1.0.61"
log = "0.4.21"
logcall = "0.1.9"
chrono = "0.4.38"
sea-orm = { version = "0.12.15", features = ["with-chrono", "macros"] }
salvo_core = { version = "0.68.3", default-features = false }
salvo-oapi = { version = "0.68.3", features = ["rapidoc","redoc","scalar","swagger-ui"] }
[profile.release]
strip = true # Automatically strip symbols from the binary.

View file

@ -0,0 +1,75 @@
[package]
name = "oxidetalis"
description = "OxideTalis Messaging Protocol homeserver"
edition = "2021"
license = "AGPL-3.0-or-later"
authors.workspace = true
readme.workspace = true
repository.workspace = true
version.workspace = true
rust-version.workspace = true
[dependencies]
oxidetalis_core = { workspace = true }
oxidetalis_config = { workspace = true }
oxidetalis_entities = { workspace = true }
oxidetalis_migrations = { workspace = true }
log = { workspace = true }
logcall = { workspace = true }
sea-orm = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
chrono = { workspace = true }
salvo = { version = "0.68.2", features = ["affix", "logging", "native-tls", "oapi", "rate-limiter", "websocket"] }
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
derive-new = "0.6.0"
pretty_env_logger = "0.5.0"
serde_json = "1.0.117"
[lints.rust]
unsafe_code = "deny"
missing_docs = "warn"
[lints.clippy]
wildcard_imports = "deny"
manual_let_else = "deny"
match_bool = "deny"
match_on_vec_items = "deny"
or_fun_call = "deny"
panic = "deny"
unwrap_used = "deny"
missing_assert_message = "warn"
missing_const_for_fn = "warn"
missing_errors_doc = "warn"
absolute_paths = "warn"
cast_lossless = "warn"
clone_on_ref_ptr = "warn"
cloned_instead_of_copied = "warn"
dbg_macro = "warn"
default_trait_access = "warn"
empty_enum_variants_with_brackets = "warn"
empty_line_after_doc_comments = "warn"
empty_line_after_outer_attr = "warn"
empty_structs_with_brackets = "warn"
enum_glob_use = "warn"
equatable_if_let = "warn"
explicit_iter_loop = "warn"
filetype_is_file = "warn"
filter_map_next = "warn"
flat_map_option = "warn"
float_cmp = "warn"
format_push_string = "warn"
future_not_send = "warn"
if_not_else = "warn"
if_then_some_else_none = "warn"
implicit_clone = "warn"
inconsistent_struct_constructor = "warn"
iter_filter_is_ok = "warn"
iter_filter_is_some = "warn"
iter_not_returning_iterator = "warn"
manual_is_variant_and = "warn"
option_if_let_else = "warn"
option_option = "warn"

View file

@ -0,0 +1,15 @@
FROM rust:1.70.0-slim-bullseye as builder
WORKDIR /builder
COPY ./ ./
RUN cargo build --release
FROM debian:bullseye-slim
WORKDIR /app
COPY --from=builder /builder/target/release/oxidetalis .
CMD ["./oxidetalis"]

View file

@ -0,0 +1,21 @@
// OxideTalis Messaging Protocol homeserver implementation
// Copyright (C) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://gnu.org/licenses/agpl-3.0>.
//! Database utilities for the OxideTalis homeserver.
mod user;
pub use user::*;

View file

@ -0,0 +1,60 @@
// OxideTalis Messaging Protocol homeserver implementation
// Copyright (C) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://gnu.org/licenses/agpl-3.0>.
//! Functions for interacting with the user table in the database.
use logcall::logcall;
use oxidetalis_core::types::PublicKey;
use oxidetalis_entities::prelude::*;
use sea_orm::DatabaseConnection;
use crate::errors::{ApiError, ApiResult};
pub trait UserTableExt {
/// Returns true if there is users in the database
async fn users_exists_in_database(&self) -> ApiResult<bool>;
/// Register new user
async fn register_user(&self, public_key: &PublicKey, is_admin: bool) -> ApiResult<()>;
}
impl UserTableExt for DatabaseConnection {
#[logcall]
async fn users_exists_in_database(&self) -> ApiResult<bool> {
UserEntity::find()
.one(self)
.await
.map_err(Into::into)
.map(|u| u.is_some())
}
#[logcall]
async fn register_user(&self, public_key: &PublicKey, is_admin: bool) -> ApiResult<()> {
if let Err(err) = (UserActiveModel {
public_key: Set(public_key.to_string()),
is_admin: Set(is_admin),
..Default::default()
})
.save(self)
.await
{
if let Some(SqlErr::UniqueConstraintViolation(_)) = err.sql_err() {
return Err(ApiError::DuplicatedUser);
}
}
Ok(())
}
}

View file

@ -0,0 +1,81 @@
// OxideTalis Messaging Protocol homeserver implementation
// Copyright (C) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://gnu.org/licenses/agpl-3.0>.
use salvo::{
http::StatusCode,
oapi::{Components as OapiComponents, EndpointOutRegister, Operation as OapiOperation},
Response,
Scribe,
};
use crate::{routes::write_json_body, schemas::MessageSchema};
/// Result type of the homeserver
#[allow(clippy::absolute_paths)]
pub(crate) type Result<T> = std::result::Result<T, Error>;
#[allow(clippy::absolute_paths)]
pub type ApiResult<T> = std::result::Result<T, ApiError>;
/// The homeserver errors
#[derive(Debug, thiserror::Error)]
pub(crate) enum Error {
#[error("Database Error: {0}")]
Database(#[from] sea_orm::DbErr),
#[error("{0}")]
Configuration(#[from] oxidetalis_config::Error),
}
#[derive(Debug, thiserror::Error)]
pub enum ApiError {
/// Error from the database (500 Internal Server Error)
#[error("Internal server error")]
SeaOrm(#[from] sea_orm::DbErr),
/// The server registration is closed (403 Forbidden)
#[error("Server registration is closed")]
RegistrationClosed,
/// The entered public key is already registered (400 Bad Request)
#[error("The entered public key is already registered")]
DuplicatedUser,
/// The user enterd tow different public keys
/// one in the header and other in the request body
/// (400 Bad Request)
#[error("TODO")]
TwoDifferentKeys,
}
impl ApiError {
/// Status code of the error
pub const fn status_code(&self) -> StatusCode {
match self {
Self::SeaOrm(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::RegistrationClosed => StatusCode::FORBIDDEN,
Self::DuplicatedUser | Self::TwoDifferentKeys => StatusCode::BAD_REQUEST,
}
}
}
impl EndpointOutRegister for ApiError {
fn register(_: &mut OapiComponents, _: &mut OapiOperation) {}
}
impl Scribe for ApiError {
fn render(self, res: &mut Response) {
log::error!("Error: {self}");
res.status_code(self.status_code());
write_json_body(res, MessageSchema::new(self.to_string()));
}
}

View file

@ -0,0 +1,91 @@
// OxideTalis Messaging Protocol homeserver implementation
// Copyright (C) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://gnu.org/licenses/agpl-3.0>.
use std::sync::Arc;
use chrono::Utc;
use oxidetalis_config::Config;
use salvo::Depot;
use sea_orm::DatabaseConnection;
use crate::{routes::DEPOT_NONCE_CACHE_SIZE, NonceCache};
/// Extension trait for the Depot.
pub trait DepotExt {
/// Returns the database connection
fn db_conn(&self) -> &DatabaseConnection;
/// Returns the server configuration
fn config(&self) -> &Config;
/// Retutns the nonce cache
fn nonce_cache(&self) -> &NonceCache;
/// Returns the size of the nonce cache
fn nonce_cache_size(&self) -> &usize;
}
/// Extension trait for the nonce cache.
pub trait NonceCacheExt {
/// Add a nonce to the cache, returns `true` if the nonce is added, `false`
/// if the nonce is already exist in the cache.
fn add_nonce(&self, nonce: &[u8; 16], limit: &usize) -> bool;
}
impl DepotExt for Depot {
fn db_conn(&self) -> &DatabaseConnection {
self.obtain::<Arc<DatabaseConnection>>()
.expect("Database connection not found")
}
fn config(&self) -> &Config {
self.obtain::<Arc<Config>>().expect("Config not found")
}
fn nonce_cache(&self) -> &NonceCache {
self.obtain::<Arc<NonceCache>>()
.expect("Nonce cache not found")
}
fn nonce_cache_size(&self) -> &usize {
let s: &Arc<usize> = self
.get(DEPOT_NONCE_CACHE_SIZE)
.expect("Nonce cache size not found");
s.as_ref()
}
}
impl NonceCacheExt for &NonceCache {
fn add_nonce(&self, nonce: &[u8; 16], limit: &usize) -> bool {
let mut cache = self.lock().expect("Nonce cache lock poisoned, aborting...");
let now = Utc::now().timestamp();
cache.retain(|_, time| (now - *time) < 30);
if &cache.len() >= limit {
log::warn!("Nonce cache limit reached, clearing 10% of the cache");
let num_to_remove = limit / 10;
let keys: Vec<[u8; 16]> = cache.keys().copied().collect();
for key in keys.iter().take(num_to_remove) {
cache.remove(key);
}
}
// We can use insert directly, but it's will update the value if the key is
// already exist so we need to check if the key is already exist or not
if cache.contains_key(nonce) {
return false;
}
cache.insert(*nonce, now);
true
}
}

View file

@ -0,0 +1,77 @@
// OxideTalis Messaging Protocol homeserver implementation
// Copyright (C) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://gnu.org/licenses/agpl-3.0>.
#![doc = include_str!("../../../README.md")]
#![warn(missing_docs, unsafe_code)]
use std::{collections::HashMap, process::ExitCode, sync::Mutex};
use oxidetalis_config::{CliArgs, Parser};
use oxidetalis_migrations::MigratorTrait;
use salvo::{conn::TcpListener, Listener, Server};
mod database;
mod errors;
mod extensions;
mod middlewares;
mod routes;
mod schemas;
mod utils;
/// Nonce cache type, used to store nonces for a certain amount of time
pub type NonceCache = Mutex<HashMap<[u8; 16], i64>>;
async fn try_main() -> errors::Result<()> {
pretty_env_logger::init_timed();
log::info!("Parsing configuration");
let config = oxidetalis_config::Config::load(CliArgs::parse())?;
log::info!("Configuration parsed successfully");
log::info!("Connecting to the database");
let connection = sea_orm::Database::connect(utils::postgres_url(&config.postgresdb)).await?;
log::info!("Connected to the database successfully");
oxidetalis_migrations::Migrator::up(&connection, None).await?;
log::info!("Migrations applied successfully");
let local_addr = format!("{}:{}", config.server.host, config.server.port);
let acceptor = TcpListener::new(&local_addr).bind().await;
log::info!("Server listening on http://{local_addr}");
if config.openapi.enable {
log::info!(
"The openapi schema is available at http://{local_addr}{}",
config.openapi.path
);
log::info!(
"The openapi viewer is available at http://{local_addr}{}",
config.openapi.viewer_path
);
}
log::info!("Server version: {}", env!("CARGO_PKG_VERSION"));
Server::new(acceptor)
.serve(routes::service(connection, &config))
.await;
Ok(())
}
#[tokio::main]
async fn main() -> ExitCode {
if let Err(err) = try_main().await {
eprintln!("{err}");
log::error!("{err}");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}

View file

@ -0,0 +1,58 @@
// OxideTalis Messaging Protocol homeserver implementation
// Copyright (C) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://gnu.org/licenses/agpl-3.0>.
//! Middlewares for the OxideTalis homeserver.
use salvo::{
handler,
http::{header, HeaderValue, StatusCode},
FlowCtrl,
Request,
Response,
};
mod public_key;
mod signature;
pub use public_key::*;
pub use signature::*;
use crate::{routes::write_json_body, schemas::MessageSchema};
/// Add server headers to the response and request.
#[handler]
pub async fn add_server_headers(req: &mut Request, res: &mut Response) {
let res_headers = res.headers_mut();
let req_headers = req.headers_mut();
res_headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
// Insert the accept header for salvo, so it returns JSON if there is error
req_headers.insert(header::ACCEPT, HeaderValue::from_static("application/json"));
}
/// Write an errror message in the response
pub fn write_error(
res: &mut Response,
ctrl: &mut FlowCtrl,
message: String,
status_code: StatusCode,
) {
res.status_code(status_code);
write_json_body(res, MessageSchema::new(message));
ctrl.skip_rest();
}

View file

@ -0,0 +1,32 @@
// OxideTalis Messaging Protocol homeserver implementation
// Copyright (C) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://gnu.org/licenses/agpl-3.0>.
//! Request sender public key middleware.
use salvo::{handler, http::StatusCode, FlowCtrl, Request, Response};
use crate::utils;
/// Middleware to check the public key of the request sender.
///
/// If the public key is valid, the request will be passed to the next handler.
/// Otherwise, a 401 Unauthorized response will be returned.
#[handler]
pub async fn public_key_check(req: &mut Request, res: &mut Response, ctrl: &mut FlowCtrl) {
if let Err(err) = utils::extract_public_key(req) {
super::write_error(res, ctrl, err.to_string(), StatusCode::UNAUTHORIZED)
}
}

View file

@ -0,0 +1,86 @@
// OxideTalis Messaging Protocol homeserver implementation
// Copyright (C) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://gnu.org/licenses/agpl-3.0>.
//! Request signature middleware.
use salvo::{
handler,
http::{Body, StatusCode},
Depot,
FlowCtrl,
Request,
Response,
};
use crate::{extensions::DepotExt, utils};
/// Middleware to check the signature of the request.
///
/// If the signature is valid, the request will be passed to the next handler.
/// Otherwise, a 401 Unauthorized response will be returned.
#[handler]
pub async fn signature_check(
req: &mut Request,
res: &mut Response,
depot: &mut Depot,
ctrl: &mut FlowCtrl,
) {
const UNAUTHORIZED: StatusCode = StatusCode::UNAUTHORIZED;
let mut write_err =
|message: &str, status_code| super::write_error(res, ctrl, message.to_owned(), status_code);
if req.body().is_end_stream() {
write_err(
"Request body is empty, the signature need a signed body",
UNAUTHORIZED,
);
return;
}
let json_body = match req.parse_json::<serde_json::Value>().await {
Ok(j) => j.to_string(),
Err(err) => {
write_err(&err.to_string(), UNAUTHORIZED);
return;
}
};
let signature = match utils::extract_signature(req) {
Ok(s) => s,
Err(err) => {
write_err(&err.to_string(), UNAUTHORIZED);
return;
}
};
let sender_public_key = match utils::extract_public_key(req) {
Ok(k) => k,
Err(err) => {
write_err(&err.to_string(), UNAUTHORIZED);
return;
}
};
if !utils::is_valid_nonce(&signature, depot.nonce_cache(), depot.nonce_cache_size())
|| !utils::is_valid_signature(
&sender_public_key,
&depot.config().server.private_key,
&signature,
json_body.as_bytes(),
)
{
write_err("Invalid signature", UNAUTHORIZED);
return;
}
}

View file

@ -0,0 +1,162 @@
// OxideTalis Messaging Protocol homeserver implementation
// Copyright (C) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://gnu.org/licenses/agpl-3.0>.
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::{env, mem};
use oxidetalis_config::Config;
use salvo::http::ResBody;
use salvo::oapi::{Info, License};
use salvo::rate_limiter::{BasicQuota, FixedGuard, MokaStore, RateLimiter, RemoteIpIssuer};
use salvo::{catcher::Catcher, logging::Logger, prelude::*};
use crate::schemas::MessageSchema;
use crate::{middlewares, NonceCache};
mod user;
/// Size of each entry in the nonce cache
pub(crate) const NONCE_ENTRY_SIZE: usize = mem::size_of::<[u8; 16]>() + mem::size_of::<i16>();
/// Size of the hashmap itself without the entrys (48 bytes)
pub(crate) const HASH_MAP_SIZE: usize = mem::size_of::<HashMap<u8, u8>>();
/// Name of the nonce cache size in the depot
pub(crate) const DEPOT_NONCE_CACHE_SIZE: &str = "NONCE_CACHE_SIZE";
pub fn write_json_body(res: &mut Response, json_body: impl serde::Serialize) {
res.write_body(serde_json::to_string(&json_body).expect("Json serialization can't be fail"))
.ok();
}
#[handler]
async fn handle404(res: &mut Response, ctrl: &mut FlowCtrl) {
if res.status_code == Some(StatusCode::NOT_FOUND) {
write_json_body(res, MessageSchema::new("Not Found".to_owned()));
ctrl.skip_rest();
}
}
#[handler]
async fn handle_server_errors(res: &mut Response, ctrl: &mut FlowCtrl) {
log::info!("New response catched: {res:#?}");
if matches!(res.status_code, Some(status) if !status.is_success()) {
if res.status_code == Some(StatusCode::TOO_MANY_REQUESTS) {
write_json_body(
res,
MessageSchema::new("Too many requests, please try again later".to_owned()),
);
ctrl.skip_rest();
} else if let ResBody::Error(err) = &res.body {
log::error!("Error: {err}");
write_json_body(
res,
MessageSchema::new(format!(
"{}, {}: {}",
err.name,
err.brief.trim_end_matches('.'),
err.cause
.as_deref()
.map_or_else(|| "".to_owned(), ToString::to_string)
.trim_end_matches('.')
.split(':')
.last()
.unwrap_or_default()
.trim()
)),
);
ctrl.skip_rest();
} else {
log::warn!("Unknown error uncatched: {res:#?}");
}
} else {
log::warn!("Unknown response uncatched: {res:#?}");
}
}
/// Hoop a middleware if the condation is true
fn hoop_if(router: Router, middleware: impl Handler, condation: bool) -> Router {
if condation {
router.hoop(middleware)
} else {
router
}
}
/// Create the ratelimit middleware
fn ratelimiter(
config: &Config,
) -> RateLimiter<FixedGuard, MokaStore<String, FixedGuard>, RemoteIpIssuer, BasicQuota> {
RateLimiter::new(
FixedGuard::new(),
MokaStore::<String, FixedGuard>::new(),
RemoteIpIssuer,
BasicQuota::set_seconds(config.ratelimit.limit, config.ratelimit.period_secs as i64),
)
.add_headers(true)
}
/// Create openapi and its viewer, and unshift them
fn route_openapi(config: &Config, router: Router) -> Router {
if config.openapi.enable {
let openapi = OpenApi::new(&config.openapi.title, env!("CARGO_PKG_VERSION"))
.info(
Info::new(&config.openapi.title, env!("CARGO_PKG_VERSION"))
.license(
License::new("AGPL-3.0-or-later").url("https://gnu.org/licenses/agpl-3.0"),
)
.description(&config.openapi.description),
)
.merge_router(&router);
let router = router
.unshift(openapi.into_router(&config.openapi.path))
.unshift(config.openapi.viewer.into_router(config));
return router;
}
router
}
pub fn service(conn: sea_orm::DatabaseConnection, config: &Config) -> Service {
let nonce_cache_size = config.server.nonce_cache_size.as_bytes();
let nonce_cache: NonceCache = Mutex::new(HashMap::with_capacity(
(nonce_cache_size - HASH_MAP_SIZE) / NONCE_ENTRY_SIZE,
));
log::info!(
"Nonce cache created with a capacity of {} ({})",
(nonce_cache_size - HASH_MAP_SIZE) / NONCE_ENTRY_SIZE,
config.server.nonce_cache_size
);
let router = Router::new()
.push(Router::with_path("user").push(user::route()))
.hoop(middlewares::add_server_headers)
.hoop(Logger::new())
.hoop(
affix::inject(Arc::new(conn))
.insert(DEPOT_NONCE_CACHE_SIZE, Arc::new(nonce_cache_size))
.inject(Arc::new(config.clone()))
.inject(Arc::new(nonce_cache)),
);
let router = hoop_if(router, ratelimiter(config), config.ratelimit.enable);
let router = route_openapi(config, router);
Service::new(router).catcher(
Catcher::default()
.hoop(middlewares::add_server_headers)
.hoop(handle404)
.hoop(handle_server_errors),
)
}

View file

@ -0,0 +1,88 @@
// OxideTalis Messaging Protocol homeserver implementation
// Copyright (C) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://gnu.org/licenses/agpl-3.0>.
//! REST API endpoints for user management
use oxidetalis_core::types::{PublicKey, Signature};
use salvo::{
http::StatusCode,
oapi::{endpoint, extract::JsonBody},
Depot,
Request,
Router,
Writer,
};
use crate::{
database::UserTableExt,
errors::{ApiError, ApiResult},
extensions::DepotExt,
middlewares,
schemas::{EmptySchema, MessageSchema, RegisterUserBody},
utils,
};
#[endpoint(
operation_id = "register",
tags("User"),
responses(
(status_code = 201, description = "User registered"),
(status_code = 403, description = "Server registration is closed", content_type = "application/json", body = MessageSchema),
(status_code = 400, description = "The public key in the header is not the same as the key in the body", content_type = "application/json", body = MessageSchema),
(status_code = 400, description = "The entered public key is already registered", content_type = "application/json", body = MessageSchema),
(status_code = 401, description = "The entered signature is invalid", content_type = "application/json", body = MessageSchema),
(status_code = 401, description = "The entered public key is invalid", content_type = "application/json", body = MessageSchema),
(status_code = 429, description = "Too many requests", content_type = "application/json", body = MessageSchema),
(status_code = 500, description = "Internal server error", content_type = "application/json", body = MessageSchema),
),
parameters(
("X-OTMP-SIGNATURE" = Signature, Header, description = "Signature of the request"),
("X-OTMP-PUBLIC" = PublicKey, Header, description = "Public key of the sender"),
),
)]
pub async fn register(
body: JsonBody<RegisterUserBody>,
req: &Request,
depot: &mut Depot,
) -> ApiResult<EmptySchema> {
let body = body.into_inner();
let db = depot.db_conn();
let config = depot.config();
if utils::extract_public_key(req).expect("Public key should be checked in the middleware")
!= body.public_key
{
return Err(ApiError::TwoDifferentKeys);
}
if !db.users_exists_in_database().await? {
db.register_user(&body.public_key, true).await?;
} else if config.register.enable {
db.register_user(&body.public_key, false).await?;
} else {
return Err(ApiError::RegistrationClosed);
}
Ok(EmptySchema::new(StatusCode::CREATED))
}
/// The route of the endpoints of this module
pub fn route() -> Router {
Router::new()
.push(Router::with_path("register").post(register))
.hoop(middlewares::public_key_check)
.hoop(middlewares::signature_check)
}

View file

@ -0,0 +1,71 @@
// OxideTalis Messaging Protocol homeserver implementation
// Copyright (C) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://gnu.org/licenses/agpl-3.0>.
use salvo::{
http::{header, StatusCode},
oapi::{
Components as OapiComponents,
EndpointOutRegister,
Operation as OapiOperation,
ToSchema,
},
Response,
Scribe,
};
use serde::{Deserialize, Serialize};
mod user;
pub use user::*;
/// Json message schema, used for returning messages to the client, the message
/// must be human readable.
///
/// # Example
/// ```json
/// {
/// "message": "Message"
/// }
/// ```
#[derive(Serialize, Deserialize, Clone, Debug, ToSchema, derive_new::new)]
#[salvo(schema(name = MessageSchema, example = json!(MessageSchema::new("Message".to_owned()))))]
pub struct MessageSchema {
#[salvo(schema(example = "Message"))]
message: String,
}
/// Empty schema, used for returning empty responses.
#[derive(Serialize, Deserialize, Clone, Debug, ToSchema)]
#[salvo(schema(name = EmptySchema))]
pub struct EmptySchema(u16);
impl EmptySchema {
/// Returns empty schema with the given status code
pub fn new(code: StatusCode) -> Self {
Self(code.as_u16())
}
}
impl EndpointOutRegister for EmptySchema {
fn register(_components: &mut OapiComponents, _operation: &mut OapiOperation) {}
}
impl Scribe for EmptySchema {
fn render(self, res: &mut Response) {
res.status_code(StatusCode::from_u16(self.0).expect("Is correct, from new function"));
res.headers_mut().remove(header::CONTENT_TYPE);
}
}

View file

@ -0,0 +1,27 @@
// OxideTalis Messaging Protocol homeserver implementation
// Copyright (C) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://gnu.org/licenses/agpl-3.0>.
use oxidetalis_core::{cipher::K256Secret, types::PublicKey};
use salvo::oapi::ToSchema;
use serde::{Deserialize, Serialize};
/// The schema for the user registration request
#[derive(Serialize, Deserialize, Clone, Debug, ToSchema, derive_new::new)]
#[salvo(schema(name = RegisterUserBody, example = json!(RegisterUserBody::new(K256Secret::new().pubkey()))))]
pub struct RegisterUserBody {
/// The public key of the user
pub public_key: PublicKey,
}

View file

@ -0,0 +1,95 @@
// OxideTalis Messaging Protocol homeserver implementation
// Copyright (C) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://gnu.org/licenses/agpl-3.0>.
use std::str::FromStr;
use chrono::Utc;
use logcall::logcall;
use oxidetalis_config::Postgres;
use oxidetalis_core::{
cipher::K256Secret,
types::{PrivateKey, PublicKey, Signature},
PUBLIC_KEY_HEADER,
SIGNATURE_HEADER,
};
use salvo::Request;
use crate::{extensions::NonceCacheExt, NonceCache};
/// Returns the postgres database url
#[logcall]
pub(crate) fn postgres_url(db_config: &Postgres) -> String {
format!(
"postgres://{}:{}@{}:{}/{}",
db_config.user,
db_config.password,
db_config.host.as_str(),
db_config.port,
db_config.name
)
}
/// Returns true if the given nonce a valid nonce.
pub(crate) fn is_valid_nonce(
signature: &Signature,
nonce_cache: &NonceCache,
nonce_cache_limit: &usize,
) -> bool {
let new_timestamp = Utc::now()
.timestamp()
.checked_sub(u64::from_be_bytes(*signature.timestamp()) as i64)
.is_some_and(|n| n <= 20);
let unused_nonce = new_timestamp && nonce_cache.add_nonce(signature.nonce(), nonce_cache_limit);
new_timestamp && unused_nonce
}
/// Returns true if the given signature is valid.
pub(crate) fn is_valid_signature(
signer: &PublicKey,
private_key: &PrivateKey,
signature: &Signature,
data: &[u8],
) -> bool {
K256Secret::from_privkey(private_key).verify(data, signature, signer)
}
/// Extract the sender public key from the request
///
/// Returns the public key of the sender extracted from the request, or the
/// reason why it failed.
pub(crate) fn extract_public_key(req: &Request) -> Result<PublicKey, String> {
req.headers()
.get(PUBLIC_KEY_HEADER)
.map(|v| {
PublicKey::from_str(v.to_str().map_err(|err| err.to_string())?)
.map_err(|err| err.to_string())
})
.ok_or_else(|| "The public key is missing".to_owned())?
}
/// Extract the signature from the request
///
/// Returns the signature extracted from the request, or the reason why it
/// failed.
pub(crate) fn extract_signature(req: &Request) -> Result<Signature, String> {
req.headers()
.get(SIGNATURE_HEADER)
.map(|v| {
Signature::from_str(v.to_str().map_err(|err| err.to_string())?)
.map_err(|err| err.to_string())
})
.ok_or_else(|| "The signature is missing".to_owned())?
}

View file

@ -0,0 +1,71 @@
[package]
name = "oxidetalis_config"
description = "A library for managing configurations of Oxidetalis homeserver"
edition = "2021"
license = "MIT"
authors.workspace = true
readme.workspace = true
repository.workspace = true
version.workspace = true
rust-version.workspace = true
[dependencies]
oxidetalis_core = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true }
log = { workspace = true }
salvo_core = { workspace = true }
salvo-oapi = { workspace = true }
clap = { version = "4.5.7", features = ["derive", "env"] }
base58 = "0.2.0"
toml = "0.8.14"
derivative = "2.2.0"
[lints.rust]
unsafe_code = "deny"
missing_docs = "warn"
[lints.clippy]
wildcard_imports = "deny"
manual_let_else = "deny"
match_bool = "deny"
match_on_vec_items = "deny"
or_fun_call = "deny"
panic = "deny"
unwrap_used = "deny"
missing_assert_message = "warn"
missing_const_for_fn = "warn"
missing_errors_doc = "warn"
absolute_paths = "warn"
cast_lossless = "warn"
clone_on_ref_ptr = "warn"
cloned_instead_of_copied = "warn"
dbg_macro = "warn"
default_trait_access = "warn"
empty_enum_variants_with_brackets = "warn"
empty_line_after_doc_comments = "warn"
empty_line_after_outer_attr = "warn"
empty_structs_with_brackets = "warn"
enum_glob_use = "warn"
equatable_if_let = "warn"
explicit_iter_loop = "warn"
filetype_is_file = "warn"
filter_map_next = "warn"
flat_map_option = "warn"
float_cmp = "warn"
format_push_string = "warn"
future_not_send = "warn"
if_not_else = "warn"
if_then_some_else_none = "warn"
implicit_clone = "warn"
inconsistent_struct_constructor = "warn"
indexing_slicing = "warn"
iter_filter_is_ok = "warn"
iter_filter_is_some = "warn"
iter_not_returning_iterator = "warn"
manual_is_variant_and = "warn"
option_if_let_else = "warn"
option_option = "warn"

View file

@ -0,0 +1,25 @@
# Oxidetalis configurations
A library for managing configurations of Oxidetalis homeserver.
## Key Features
- **Load and write configurations**: Load configurations from a file and write
configurations to a file.
- **Multiple configuration entries**: The configurations are collected from CLI
arguments, environment variables, and configuration files.
- **Configuration validation**: Validate the configurations before using them.
- **Configuration defaults**: Set default values for configurations.
## Must to know
- The configurations are loaded in the following order (from highest priority to
lowest priority)
1. Command-line options
2. Environment variables
3. Configuration file
4. Default values (or ask you to provide the value)
- The configurations are written to the configuration file every time you run
the server, even if you don't change any configuration. This is to ensure that
the configuration file is always up-to-date.
## License
This crate is licensed under the MIT license.

View file

@ -0,0 +1,98 @@
// OxideTalis homeserver configurations
// Copyright (c) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Command-line arguments parser
use std::{net::IpAddr, path::PathBuf};
use clap::Parser;
use oxidetalis_core::types::Size;
use crate::{types::OpenApiViewer, IpOrUrl};
#[derive(Parser)]
#[clap(version)]
/// Command-line arguments for the Oxidetalis server.
pub struct CliArgs {
/// Path to the configuration file, toml format.
#[clap(long, env = "OXIDETALIS_CONFIG")]
pub config: PathBuf,
/// Server name, for example, `example.com`.
#[clap(long, env = "OXIDETALIS_SERVER_NAME")]
pub server_name: Option<String>,
/// Local IP address to bind the server to.
#[clap(long, env = "OXIDETALIS_SERVER_HOST")]
pub server_host: Option<IpAddr>,
/// Port to bind the server to.
#[clap(long, env = "OXIDETALIS_SERVER_PORT")]
pub server_port: Option<u16>,
/// Nonce cache size
///
/// e.g. "50B", "300KB", "1MB", "1GB"
#[clap(long, env = "OXIDETALIS_SERVER_NONCE_CACHE_SIZE")]
pub server_nonce_cache_size: Option<Size>,
/// Enable or disable user registration.
#[clap(long, env = "OXIDETALIS_REGISTER_ENABLE")]
pub register_enable: Option<bool>,
/// Hostname or IP address of the PostgreSQL database.
#[clap(long, env = "OXIDETALIS_DB_HOST")]
pub postgres_host: Option<IpOrUrl>,
/// Port number of the PostgreSQL database.
#[clap(long, env = "OXIDETALIS_DB_PORT")]
pub postgres_port: Option<u16>,
/// Username for the PostgreSQL database.
#[clap(long, env = "OXIDETALIS_DB_USER")]
pub postgres_user: Option<String>,
/// Password for the PostgreSQL database.
#[clap(long, env = "OXIDETALIS_DB_PASSWORD")]
pub postgres_password: Option<String>,
/// Name of the PostgreSQL database.
#[clap(long, env = "OXIDETALIS_DB_NAME")]
pub postgres_name: Option<String>,
/// Enable or disable rate limiting.
#[clap(long, env = "OXIDETALIS_RATELIMIT_ENABLE")]
pub ratelimit_enable: Option<bool>,
/// Maximum number of requests allowed within a given time period for rate
/// limiting.
#[clap(long, env = "OXIDETALIS_RATELIMIT_LIMIT")]
pub ratelimit_limit: Option<usize>,
/// Time period in seconds for rate limiting.
#[clap(long, env = "OXIDETALIS_RATELIMIT_PREIOD")]
pub ratelimit_preiod: Option<usize>,
/// Enable or disable OpenAPI documentation generation.
#[clap(long, env = "OXIDETALIS_OPENAPI_ENABLE")]
pub openapi_enable: Option<bool>,
/// Title for the OpenAPI documentation.
#[clap(long, env = "OXIDETALIS_OPENAPI_TITLE")]
pub openapi_title: Option<String>,
/// Description for the OpenAPI documentation.
#[clap(long, env = "OXIDETALIS_OPENAPI_DESCRIPTION")]
pub openapi_description: Option<String>,
/// Path to serve the OpenAPI documentation.
#[clap(long, env = "OXIDETALIS_OPENAPI_PATH")]
pub openapi_path: Option<String>,
/// OpenAPI viewer to use for rendering the documentation.
#[clap(long, env = "OXIDETALIS_OPENAPI_VIEWER")]
pub openapi_viewer: Option<OpenApiViewer>,
/// Path to the OpenAPI viewer HTML file.
#[clap(long, env = "OXIDETALIS_OPENAPI_VIEWER_PATH")]
pub openapi_viewer_path: Option<String>,
}

View file

@ -0,0 +1,109 @@
// OxideTalis homeserver configurations
// Copyright (c) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! The config defaults value
/// Server default configs
pub(crate) mod server {
use std::net::{IpAddr, Ipv4Addr};
use oxidetalis_core::{
cipher::K256Secret,
types::{PrivateKey, Size},
};
pub fn name() -> String {
"example.com".to_owned()
}
pub const fn host() -> IpAddr {
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))
}
pub const fn port() -> u16 {
3873
}
pub fn private_key() -> PrivateKey {
K256Secret::new().privkey()
}
pub const fn nonce_cache_size() -> Size {
Size::MB(1)
}
}
/// Ratelimit default configs
pub(crate) mod ratelimit {
pub const fn limit() -> usize {
1500
}
pub const fn period_secs() -> usize {
60
}
}
/// OpenApi default configs
pub(crate) mod openapi {
use crate::types;
pub fn title() -> String {
"Oxidetalis homeserver".to_owned()
}
pub fn description() -> String {
"OxideTalis Messaging Protocol homeserver".to_owned()
}
pub fn path() -> String {
"/openapi.json".to_owned()
}
pub const fn viewer() -> types::OpenApiViewer {
types::OpenApiViewer::SwaggerUi
}
pub fn viewer_path() -> String {
"/swagger-ui".to_owned()
}
}
/// Postgres default configs
pub(crate) mod postgres {
use std::str::FromStr;
pub fn user() -> String {
"oxidetalis".to_owned()
}
pub fn password() -> String {
"oxidetalis".to_owned()
}
pub fn host() -> crate::IpOrUrl {
crate::IpOrUrl::from_str("localhost").expect("Is a valid localhost")
}
pub fn name() -> String {
"oxidetalis_db".to_owned()
}
pub const fn port() -> u16 {
5432
}
}
pub(crate) const fn bool_true() -> bool {
true
}
pub(crate) const fn bool_false() -> bool {
false
}

View file

@ -0,0 +1,247 @@
// OxideTalis homeserver configurations
// Copyright (c) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#![doc = include_str!("../README.md")]
use std::{fs, io::Error as IoError, net::IpAddr, path::Path};
use derivative::Derivative;
use oxidetalis_core::types::{PrivateKey, Size};
use serde::{Deserialize, Serialize};
use toml::{de::Error as TomlDeError, ser::Error as TomlSerError};
mod commandline;
mod defaults;
mod serde_with;
mod types;
pub use clap::Parser;
pub use commandline::CliArgs;
pub use types::*;
/// Configuration errors
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error("IO: {0}")]
IO(#[from] IoError),
#[error("Toml error: {0}")]
DeToml(#[from] TomlDeError),
#[error("Toml error: {0}")]
SeToml(#[from] TomlSerError),
#[error("Missing required option `--{0}`")]
RequiredConfiguration(String),
}
/// Server startup configuration
#[derive(Deserialize, Serialize, Derivative, Clone)]
#[derivative(Default)]
#[serde(default)]
pub struct Server {
/// Name of the server, for example, `example.com`
#[derivative(Default(value = "defaults::server::name()"))]
pub server_name: String,
/// Host that the server will listen in
#[derivative(Default(value = "defaults::server::host()"))]
pub host: IpAddr,
/// Port that the server will listen in
#[derivative(Default(value = "defaults::server::port()"))]
pub port: u16,
/// Server keypair
#[derivative(Default(value = "defaults::server::private_key()"))]
pub private_key: PrivateKey,
/// Nonce cache limit
#[derivative(Default(value = "defaults::server::nonce_cache_size()"))]
pub nonce_cache_size: Size,
}
/// Registration config
#[derive(Debug, Deserialize, Serialize, Derivative, Clone)]
#[derivative(Default)]
#[serde(default)]
pub struct Register {
/// Whether to enable the registration or not
#[derivative(Default(value = "defaults::bool_false()"))]
pub enable: bool,
}
/// Database configuration
#[derive(Debug, Deserialize, Serialize, Derivative, Clone)]
#[derivative(Default)]
#[serde(default)]
pub struct Postgres {
/// Username
#[derivative(Default(value = "defaults::postgres::user()"))]
pub user: String,
/// User password
#[derivative(Default(value = "defaults::postgres::password()"))]
pub password: String,
/// Database host
#[derivative(Default(value = "defaults::postgres::host()"))]
pub host: IpOrUrl,
/// Database port
#[derivative(Default(value = "defaults::postgres::port()"))]
pub port: u16,
/// Database name
#[derivative(Default(value = "defaults::postgres::name()"))]
pub name: String,
}
/// Ratelimit configuration
#[derive(Debug, Deserialize, Serialize, Derivative, Clone)]
#[derivative(Default)]
#[serde(default)]
pub struct Ratelimit {
/// Whether to enable the ratelimit or not
#[derivative(Default(value = "defaults::bool_true()"))]
pub enable: bool,
/// The limit of requests.
#[derivative(Default(value = "defaults::ratelimit::limit()"))]
pub limit: usize,
/// The period of requests.
#[derivative(Default(value = "defaults::ratelimit::period_secs()"))]
pub period_secs: usize,
}
/// OpenApi configuration
#[derive(Debug, Deserialize, Serialize, Derivative, Clone)]
#[derivative(Default)]
#[serde(default)]
pub struct OpenApi {
/// Whether to enable the openapi or not
#[derivative(Default(value = "defaults::bool_false()"))]
pub enable: bool,
/// Title of the openapi
#[derivative(Default(value = "defaults::openapi::title()"))]
pub title: String,
/// Description of the openapi
#[derivative(Default(value = "defaults::openapi::description()"))]
pub description: String,
/// Location to serve openapi json in
#[derivative(Default(value = "defaults::openapi::path()"))]
#[serde(deserialize_with = "serde_with::deserialize_url_path")]
pub path: String,
/// The openapi viewer
#[derivative(Default(value = "defaults::openapi::viewer()"))]
pub viewer: types::OpenApiViewer,
/// Location to server the viewer in
#[derivative(Default(value = "defaults::openapi::viewer_path()"))]
#[serde(deserialize_with = "serde_with::deserialize_url_path")]
pub viewer_path: String,
}
#[derive(Deserialize, Serialize, Default, Clone)]
/// Oxidetalis homeserver configurations
pub struct Config {
/// Server configuration (server startup configuration)
#[serde(default)]
pub server: Server,
/// Server registration configuration
#[serde(default)]
pub register: Register,
/// Database configuration
pub postgresdb: Postgres,
/// Ratelimit configuration
#[serde(default)]
pub ratelimit: Ratelimit,
/// OpenApi configuration
#[serde(default)]
pub openapi: OpenApi,
}
/// Check if required new configuration options are provided
fn check_required_new_config(args: &CliArgs) -> Result<(), Error> {
log::info!("Checking the required options for the new configuration");
if args.server_name.is_none() {
return Err(Error::RequiredConfiguration("server-name".to_owned()));
}
Ok(())
}
impl Config {
/// Load the config from toml file and command-line options
///
/// The priority is:
/// 1. Command-line options
/// 2. Environment variables
/// 3. Configuration file
/// 4. Default values (or ask you to provide the value)
///
/// ## Errors
/// - Failed to read the config file
/// - Invalid toml file
pub fn load(args: CliArgs) -> Result<Self, Error> {
let mut config = if args.config.exists() {
log::info!("Loading configuration from {}", args.config.display());
toml::from_str(&fs::read_to_string(&args.config)?)?
} else {
log::info!("Configuration file not found, creating a new one");
check_required_new_config(&args)?;
if let Some(parent) = args.config.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
Config::default()
};
assign_option(&mut config.server.server_name, args.server_name);
assign_option(&mut config.server.host, args.server_host);
assign_option(&mut config.server.port, args.server_port);
assign_option(
&mut config.server.nonce_cache_size,
args.server_nonce_cache_size,
);
assign_option(&mut config.register.enable, args.register_enable);
assign_option(&mut config.postgresdb.host, args.postgres_host);
assign_option(&mut config.postgresdb.port, args.postgres_port);
assign_option(&mut config.postgresdb.user, args.postgres_user);
assign_option(&mut config.postgresdb.password, args.postgres_password);
assign_option(&mut config.postgresdb.name, args.postgres_name);
assign_option(&mut config.ratelimit.enable, args.ratelimit_enable);
assign_option(&mut config.ratelimit.limit, args.ratelimit_limit);
assign_option(&mut config.ratelimit.period_secs, args.ratelimit_preiod);
assign_option(&mut config.openapi.enable, args.openapi_enable);
assign_option(&mut config.openapi.title, args.openapi_title);
assign_option(&mut config.openapi.description, args.openapi_description);
assign_option(&mut config.openapi.path, args.openapi_path);
assign_option(&mut config.openapi.viewer, args.openapi_viewer);
assign_option(&mut config.openapi.viewer_path, args.openapi_viewer_path);
config.write(&args.config)?;
Ok(config)
}
/// Write the configs to the config file
///
/// ## Errors
/// - Failed to write to the config file
pub fn write(&self, config_file: impl AsRef<Path>) -> Result<(), Error> {
fs::write(config_file, toml::to_string_pretty(self)?)?;
Ok(())
}
}
/// Assign the command-line option to the config if it is not None
fn assign_option<T>(config: &mut T, arg: Option<T>) {
if let Some(value) = arg {
*config = value
}
}

View file

@ -0,0 +1,63 @@
// OxideTalis homeserver configurations
// Copyright (c) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Serialize and deserialize some oxidetalis configurations
use serde::{de::Error as DeError, Deserialize, Deserializer};
/// Serialize and deserialze the string of IpOrUrl struct
pub(crate) mod ip_or_url {
use std::str::FromStr;
use serde::{de::Error as DeError, Deserialize, Deserializer, Serializer};
use crate::IpOrUrl;
pub fn serialize<S>(value: &str, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(value)
}
pub fn deserialize<'de, D>(de: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
Ok(IpOrUrl::from_str(&String::deserialize(de)?)
.map_err(DeError::custom)?
.as_str()
.to_owned())
}
}
pub fn deserialize_url_path<'de, D>(de: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
let url_path = String::deserialize(de)?;
if !url_path.starts_with('/') || url_path.ends_with('/') {
return Err(DeError::custom(
"Invalid url path, must start with `/` and not ends with `/`",
));
}
Ok(url_path)
}

View file

@ -0,0 +1,126 @@
// OxideTalis homeserver configurations
// Copyright (c) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Oxidetalis config types
use std::{net::IpAddr, str::FromStr};
use salvo_oapi::{rapidoc::RapiDoc, redoc::ReDoc, scalar::Scalar, swagger_ui::SwaggerUi};
use serde::{Deserialize, Serialize};
/// OpenApi viewers, the viewers that can be used to view the OpenApi
/// documentation
#[derive(Debug, Clone, Deserialize, Serialize, clap::ValueEnum)]
#[serde(rename_all = "PascalCase")]
pub enum OpenApiViewer {
/// Redoc viewer <https://github.com/rapi-doc/RapiDoc>
RapiDoc,
/// Redoc viewer <https://github.com/Redocly/redoc>
ReDoc,
/// Scalar viewer <https://github.com/ScalaR/ScalaR>
Scalar,
/// Swagger-UI viewer <https://github.com/swagger-api/swagger-ui>
SwaggerUi,
}
impl OpenApiViewer {
/// Create a router for the viewer
pub fn into_router(&self, config: &crate::Config) -> salvo_core::Router {
let spec_url = config.openapi.path.clone();
let title = config.openapi.title.clone();
let description = config.openapi.description.clone();
match self {
OpenApiViewer::RapiDoc => {
RapiDoc::new(spec_url)
.title(title)
.description(description)
.into_router(&config.openapi.viewer_path)
}
OpenApiViewer::ReDoc => {
ReDoc::new(spec_url)
.title(title)
.description(description)
.into_router(&config.openapi.viewer_path)
}
OpenApiViewer::Scalar => {
Scalar::new(spec_url)
.title(title)
.description(description)
.into_router(&config.openapi.viewer_path)
}
OpenApiViewer::SwaggerUi => {
SwaggerUi::new(spec_url)
.title(title)
.description(description)
.into_router(&config.openapi.viewer_path)
}
}
}
}
/// Type hold url or ip (used for database host)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IpOrUrl(#[serde(with = "crate::serde_with::ip_or_url")] String);
impl Default for IpOrUrl {
fn default() -> Self {
IpOrUrl("localhost".to_owned())
}
}
impl IpOrUrl {
/// Returns &str ip or url
pub fn as_str(&self) -> &str {
&self.0
}
}
impl FromStr for IpOrUrl {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(IpOrUrl(
if let Ok(res) = IpAddr::from_str(s).map(|i| i.to_string()) {
res
} else {
validate_domain(s)?
},
))
}
}
fn validate_domain(domain: &str) -> Result<String, String> {
if domain != "localhost" {
let subs = domain.split('.');
for sub in subs {
let length = sub.chars().count();
if !sub.chars().all(|c| c.is_alphanumeric() || c == '-')
|| sub.starts_with('-')
|| sub.ends_with('-')
|| (length > 0 && length <= 64)
{
return Err("Invalid domain name".to_owned());
}
}
}
Ok(domain.to_owned())
}

View file

@ -0,0 +1,74 @@
[package]
name = "oxidetalis_core"
description = "OxideTalis server core"
edition = "2021"
license = "MIT"
authors.workspace = true
readme.workspace = true
repository.workspace = true
version.workspace = true
rust-version.workspace = true
[dependencies]
base58 = { workspace = true }
thiserror = { workspace = true }
salvo_core = { workspace = true }
salvo-oapi = { workspace = true }
serde = { workspace = true }
log = { workspace = true }
logcall = { workspace = true }
cbc = { version = "0.1.2", features = ["alloc", "std"] }
k256 = { version = "0.13.3", default-features = false, features = ["ecdh"] }
rand = { version = "0.8.5", default-features = false, features = ["std_rng", "std"] }
aes = "0.8.4"
hex = "0.4.3"
hmac = "0.12.1"
sha2 = "0.10.8"
[lints.rust]
unsafe_code = "deny"
missing_docs = "warn"
[lints.clippy]
wildcard_imports = "deny"
manual_let_else = "deny"
match_bool = "deny"
match_on_vec_items = "deny"
or_fun_call = "deny"
panic = "deny"
unwrap_used = "deny"
missing_assert_message = "warn"
missing_const_for_fn = "warn"
missing_errors_doc = "warn"
absolute_paths = "warn"
cast_lossless = "warn"
clone_on_ref_ptr = "warn"
cloned_instead_of_copied = "warn"
dbg_macro = "warn"
default_trait_access = "warn"
empty_enum_variants_with_brackets = "warn"
empty_line_after_doc_comments = "warn"
empty_line_after_outer_attr = "warn"
empty_structs_with_brackets = "warn"
enum_glob_use = "warn"
equatable_if_let = "warn"
explicit_iter_loop = "warn"
filetype_is_file = "warn"
filter_map_next = "warn"
flat_map_option = "warn"
float_cmp = "warn"
format_push_string = "warn"
future_not_send = "warn"
if_not_else = "warn"
if_then_some_else_none = "warn"
implicit_clone = "warn"
inconsistent_struct_constructor = "warn"
iter_filter_is_ok = "warn"
iter_filter_is_some = "warn"
iter_not_returning_iterator = "warn"
manual_is_variant_and = "warn"
option_if_let_else = "warn"
option_option = "warn"

View file

@ -0,0 +1,224 @@
// OxideTalis Messaging Protocol homeserver core implementation
// Copyright (c) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! The `cipher` module contains the encryption and decryption functions for the
//! OxideTalis protocol.
use std::time::{SystemTime, UNIX_EPOCH};
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use hmac::Mac;
use k256::{
ecdh::diffie_hellman,
elliptic_curve::sec1::ToEncodedPoint,
FieldBytes,
NonZeroScalar,
PublicKey,
};
use logcall::logcall;
use rand::{thread_rng, RngCore};
use crate::types::{
PrivateKey as CorePrivateKey,
PublicKey as CorePublicKey,
Signature as CoreSignature,
};
/// The errors that can occur during in the cipher module.
#[derive(Debug, thiserror::Error)]
pub enum CipherError {
/// The public key is invalid.
#[error("Invalid Public Key")]
InvalidPublicKey,
/// The private key is invalid.
#[error("Invalid Private Key")]
InvalidPrivateKey,
/// The signature is invalid
#[error("Invalid signature")]
InvalidSignature,
/// A decryption error
#[error("Decryption Error")]
Decryption,
/// Invalid base58 string
#[error("Invalid base58 string `{0}`")]
InvalidBase58(String),
/// Invalid hex string
#[error("Invalid hex string `{0}`")]
InvalidHex(String),
}
#[allow(clippy::absolute_paths)]
type Result<T> = std::result::Result<T, CipherError>;
type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
type HmacSha256 = hmac::Hmac<sha2::Sha256>;
/// An wrapper around the k256 crate to provide a simple API for ecdh key
/// exchange and keypair generation.
pub struct K256Secret {
/// The private key scalar
scalar: NonZeroScalar,
/// The public key
public_key: PublicKey,
}
impl From<NonZeroScalar> for K256Secret {
fn from(scalar: NonZeroScalar) -> Self {
Self {
public_key: PublicKey::from_secret_scalar(&scalar),
scalar,
}
}
}
impl K256Secret {
/// Generate a new random keypair, using the system random number generator.
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self::from(NonZeroScalar::random(&mut rand::thread_rng()))
}
/// Restore a keypair from a private key.
pub fn from_privkey(private_key: &CorePrivateKey) -> Self {
Self::from(
Option::<NonZeroScalar>::from(NonZeroScalar::from_repr(*FieldBytes::from_slice(
private_key.as_bytes(),
)))
.expect("The private key is correct"),
)
}
/// Returns the public key.
pub fn pubkey(&self) -> CorePublicKey {
CorePublicKey::try_from(
<[u8; 33]>::try_from(self.public_key.to_encoded_point(true).as_bytes())
.expect("The length is correct"),
)
.expect("Is correct public key")
}
/// Returns the private key.
pub fn privkey(&self) -> CorePrivateKey {
CorePrivateKey::try_from(<[u8; 32]>::from(FieldBytes::from(self.scalar)))
.expect("Correct private key")
}
/// Compute the shared secret with the given public key.
pub fn shared_secret(&self, with: &CorePublicKey) -> [u8; 32] {
let mut secret_buf = [0u8; 32];
diffie_hellman(
self.scalar,
PublicKey::from_sec1_bytes(with.as_bytes())
.expect("Correct public key")
.as_affine(),
)
.extract::<sha2::Sha256>(None)
.expand(&[], &mut secret_buf)
.expect("The buffer size is correct");
secret_buf
}
/// Encrypt a data with the shared secret.
///
/// The data is encrypted using AES-256-CBC with a random IV (last 16 bytes
/// of the ciphertext).
pub fn encrypt_data(&self, encrypt_to: &CorePublicKey, data: &[u8]) -> Vec<u8> {
let mut iv = [0u8; 16];
thread_rng().fill_bytes(&mut iv);
let mut ciphertext =
Aes256CbcEnc::new(self.shared_secret(encrypt_to).as_slice().into(), &iv.into())
.encrypt_padded_vec_mut::<Pkcs7>(data);
ciphertext.extend(&iv);
ciphertext
}
/// Decrypt a data with the shared secret.
///
/// The data is decrypted using AES-256-CBC with the IV being the last 16
/// bytes of the ciphertext.
///
/// ## Errors
/// - If the data less then 16 bytes.
/// - If the iv less then 16 bytes.
/// - Falid to decrypt the data (invalid encrypted data)
pub fn decrypt_data(&self, decrypt_from: &CorePublicKey, data: &[u8]) -> Result<Vec<u8>> {
let (ciphertext, iv) =
data.split_at(data.len().checked_sub(16).ok_or(CipherError::Decryption)?);
if iv.len() != 16 {
return Err(CipherError::Decryption);
}
Aes256CbcDec::new(
self.shared_secret(decrypt_from).as_slice().into(),
iv.into(),
)
.decrypt_padded_vec_mut::<Pkcs7>(ciphertext)
.map_err(|_| CipherError::Decryption)
}
/// Sign a data with the shared secret.
///
/// The signature is exiplained in the OTMP specification.
#[logcall]
pub fn sign(&self, data: &[u8], sign_to: &CorePublicKey) -> CoreSignature {
let mut time_and_nonce = [0u8; 24];
time_and_nonce[0..=7].copy_from_slice(
&SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("SystemTime before UNIX EPOCH!")
.as_secs()
.to_be_bytes(),
);
thread_rng().fill_bytes(&mut time_and_nonce[8..=23]);
let mut hmac_secret = [0u8; 56];
hmac_secret[0..=31].copy_from_slice(&self.shared_secret(sign_to));
hmac_secret[32..=55].copy_from_slice(&time_and_nonce);
let mut signature = [0u8; 56];
signature[0..=31].copy_from_slice(&hmac_sha256(data, &hmac_secret));
signature[32..=55].copy_from_slice(&time_and_nonce);
CoreSignature::from(signature)
}
/// Verify a signature with the shared secret.
///
/// Note:
/// The time and the nonce will not be checked here
#[logcall]
pub fn verify(&self, data: &[u8], signature: &CoreSignature, signer: &CorePublicKey) -> bool {
let mut hmac_secret = [0u8; 56];
hmac_secret[0..=31].copy_from_slice(&self.shared_secret(signer));
hmac_secret[32..=39].copy_from_slice(signature.timestamp());
hmac_secret[40..=55].copy_from_slice(signature.nonce());
&hmac_sha256(data, &hmac_secret) == signature.hmac_output()
}
}
fn hmac_sha256(data: &[u8], secret: &[u8]) -> [u8; 32] {
let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC can take key of any size");
mac.update(data);
mac.finalize().into_bytes().into()
}

View file

@ -0,0 +1,33 @@
// OxideTalis Messaging Protocol homeserver core implementation
// Copyright (c) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! The core library for the OxideTalis homeserver implementation.
pub mod cipher;
pub mod types;
/// The header name for the signature. The signature is a hex encoded string.
pub const SIGNATURE_HEADER: &str = "X-OTMP-SIGNATURE";
/// The header name of the request sender public key. The public key is a base58
/// encoded string.
pub const PUBLIC_KEY_HEADER: &str = "X-OTMP-PUBLIC";
/// Server name header name
pub const SERVER_NAME_HEADER: &str = "X-OTMP-SERVER";

View file

@ -0,0 +1,213 @@
// OxideTalis Messaging Protocol homeserver core implementation
// Copyright (c) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use std::{fmt, str::FromStr};
use base58::{FromBase58, ToBase58};
use salvo_oapi::{
schema::{
Schema as OapiSchema,
SchemaFormat as OapiSchemaFormat,
SchemaType as OapiSchemaType,
},
ToSchema,
};
use crate::cipher::CipherError;
/// Correct length except message
const CORRECT_LENGTH: &str = "The length is correct";
/// K256 public key
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PublicKey([u8; 33]);
/// K256 private key
#[derive(Clone, Copy)]
pub struct PrivateKey([u8; 32]);
/// OTMP signature
#[derive(Clone, Copy, Debug)]
pub struct Signature {
hmac_output: [u8; 32],
timestamp: [u8; 8],
nonce: [u8; 16],
}
impl PublicKey {
/// Returns the public key as bytes
pub const fn as_bytes(&self) -> &[u8; 33] {
&self.0
}
}
impl PrivateKey {
/// Returns the private key as bytes
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
impl Signature {
/// Returns the hmac output from the signature
pub const fn hmac_output(&self) -> &[u8; 32] {
&self.hmac_output
}
/// Returns the timestamp from the signature
pub const fn timestamp(&self) -> &[u8; 8] {
&self.timestamp
}
/// Returns the nonce from the signature
pub const fn nonce(&self) -> &[u8; 16] {
&self.nonce
}
/// Returns the signature as bytes
pub fn as_bytes(&self) -> [u8; 56] {
let mut sig = [0u8; 56];
sig[0..=31].copy_from_slice(&self.hmac_output);
sig[32..=39].copy_from_slice(&self.timestamp);
sig[40..=55].copy_from_slice(&self.nonce);
sig
}
}
/// Public key to base58 string
impl fmt::Display for PublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.to_base58())
}
}
/// Public key to base58 string
impl fmt::Display for PrivateKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.to_base58())
}
}
/// Signature to hex string
impl fmt::Display for Signature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", hex::encode(self.as_bytes()))
}
}
/// Public key from base58 string
impl FromStr for PublicKey {
type Err = CipherError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let public_key = s
.from_base58()
.map_err(|_| CipherError::InvalidBase58(s.to_owned()))?;
if public_key.len() != 33 {
return Err(CipherError::InvalidPublicKey);
}
Self::try_from(<[u8; 33]>::try_from(public_key).expect(CORRECT_LENGTH))
}
}
/// Private key from base58 string
impl FromStr for PrivateKey {
type Err = CipherError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let private_key = s
.from_base58()
.map_err(|_| CipherError::InvalidBase58(s.to_owned()))?;
if private_key.len() != 32 {
return Err(CipherError::InvalidPrivateKey);
}
Self::try_from(<[u8; 32]>::try_from(private_key).expect(CORRECT_LENGTH))
}
}
/// Signature from hex string
impl FromStr for Signature {
type Err = CipherError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let signature = hex::decode(s).map_err(|_| CipherError::InvalidHex(s.to_owned()))?;
if signature.len() != 56 {
return Err(CipherError::InvalidSignature);
}
Ok(Signature::from(
<[u8; 56]>::try_from(signature).expect(CORRECT_LENGTH),
))
}
}
impl TryFrom<[u8; 33]> for PublicKey {
type Error = CipherError;
fn try_from(public_key: [u8; 33]) -> Result<Self, Self::Error> {
if k256::PublicKey::from_sec1_bytes(&public_key).is_err() {
return Err(CipherError::InvalidPublicKey);
}
Ok(Self(public_key))
}
}
impl TryFrom<[u8; 32]> for PrivateKey {
type Error = CipherError;
fn try_from(private_key: [u8; 32]) -> Result<Self, Self::Error> {
if k256::NonZeroScalar::from_repr(*k256::FieldBytes::from_slice(&private_key))
.is_none()
.into()
{
return Err(CipherError::InvalidPrivateKey);
}
Ok(Self(private_key))
}
}
impl From<[u8; 56]> for Signature {
fn from(signature: [u8; 56]) -> Self {
Self {
hmac_output: signature[0..=31].try_into().expect(CORRECT_LENGTH),
timestamp: signature[32..=39].try_into().expect(CORRECT_LENGTH),
nonce: signature[40..=55].try_into().expect(CORRECT_LENGTH),
}
}
}
impl ToSchema for PublicKey {
fn to_schema(_components: &mut salvo_oapi::Components) -> salvo_oapi::RefOr<OapiSchema> {
salvo_oapi::Object::new()
.schema_type(OapiSchemaType::String)
.format(OapiSchemaFormat::Custom("base58".to_owned()))
.into()
}
}
impl ToSchema for Signature {
fn to_schema(_components: &mut salvo_oapi::Components) -> salvo_oapi::RefOr<OapiSchema> {
salvo_oapi::Object::new()
.schema_type(OapiSchemaType::String)
.format(OapiSchemaFormat::Custom("hex".to_owned()))
.into()
}
}

View file

@ -0,0 +1,99 @@
// OxideTalis Messaging Protocol homeserver core implementation
// Copyright (c) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use base58::FromBase58;
use serde::{de::Error as DeError, Deserialize, Serialize};
use super::{PrivateKey, PublicKey, Signature};
impl<'de> Deserialize<'de> for PrivateKey {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let private_key = String::deserialize(deserializer)?
.from_base58()
.map_err(|_| DeError::custom("Invalid base58"))?;
Self::try_from(
<[u8; 32]>::try_from(private_key)
.map_err(|_| DeError::custom("Invalid private key length, must be 32 bytes"))?,
)
.map_err(|_| DeError::custom("Invalid private key"))
}
}
impl Serialize for PrivateKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.to_string().as_str())
}
}
impl<'de> Deserialize<'de> for PublicKey {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let public_key = String::deserialize(deserializer)?
.from_base58()
.map_err(|_| DeError::custom("Invalid base58"))?;
Self::try_from(
<[u8; 33]>::try_from(public_key)
.map_err(|_| DeError::custom("Invalid public key length, must be 33 bytes"))?,
)
.map_err(|_| DeError::custom("Invalid public key"))
}
}
impl Serialize for PublicKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.to_string().as_str())
}
}
impl<'de> Deserialize<'de> for Signature {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let signature = hex::decode(String::deserialize(deserializer)?)
.map_err(|_| DeError::custom("Invalid hex string"))?;
Ok(Self::from(<[u8; 56]>::try_from(signature).map_err(
|_| DeError::custom("Invalid signature length, must be 56 bytes"),
)?))
}
}
impl Serialize for Signature {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.to_string().as_str())
}
}

View file

@ -0,0 +1,29 @@
// OxideTalis Messaging Protocol homeserver core implementation
// Copyright (c) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Oxidetalis server types
mod cipher;
mod impl_serde;
mod size;
pub use cipher::*;
pub use size::*;

View file

@ -0,0 +1,125 @@
// OxideTalis Messaging Protocol homeserver core implementation
// Copyright (c) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Size type. Used to represent sizes in bytes, kilobytes, megabytes, and
//! gigabytes.
use std::{fmt, str::FromStr};
use logcall::logcall;
use serde::{de::Error as DeError, Deserialize, Serialize};
/// Size type. Used to represent sizes in bytes, kilobytes, megabytes, and
/// gigabytes.
#[derive(Copy, Clone, Debug)]
pub enum Size {
/// Byte
B(usize),
/// Kilobyte
KB(usize),
/// Megabyte
MB(usize),
/// Gigabyte
GB(usize),
}
impl Size {
/// Returns the size in bytes, regardless of the unit
pub const fn as_bytes(&self) -> usize {
match self {
Size::B(n) => *n,
Size::KB(n) => *n * 1e+3 as usize,
Size::MB(n) => *n * 1e+6 as usize,
Size::GB(n) => *n * 1e+9 as usize,
}
}
/// Returns the unit name of the size (e.g. `B`, `KB`, `MB`, `GB`)
pub const fn unit_name(&self) -> &'static str {
match self {
Size::B(_) => "B",
Size::KB(_) => "KB",
Size::MB(_) => "MB",
Size::GB(_) => "GB",
}
}
/// Returns the size in the unit (e.g. `2MB` -> `2`, `2GB` -> `2`)
pub const fn size(&self) -> usize {
match self {
Size::B(n) | Size::KB(n) | Size::MB(n) | Size::GB(n) => *n,
}
}
}
impl fmt::Display for Size {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}", self.size(), self.unit_name())
}
}
impl FromStr for Size {
type Err = String;
#[logcall]
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some(first_alpha) = s.find(|c: char| c.is_alphabetic()) else {
return Err("Missing unit, e.g. `2MB`".to_owned());
};
let (size, unit) = s.split_at(first_alpha);
let Ok(size) = size.parse() else {
return Err(format!("Invalid size `{size}`"));
};
Ok(match unit {
"B" => Self::B(size),
"KB" => Self::KB(size),
"MB" => Self::MB(size),
"GB" => Self::GB(size),
unknown_unit => {
return Err(format!(
"Unsupported unit `{unknown_unit}`, supported units are `B`, `KB`, `MB`, `GB`"
));
}
})
}
}
impl<'de> Deserialize<'de> for Size {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
String::deserialize(deserializer)?
.as_str()
.parse()
.map_err(DeError::custom)
}
}
impl Serialize for Size {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.to_string().as_str())
}
}

View file

@ -0,0 +1,60 @@
[package]
name = "oxidetalis_entities"
description = "Database entities for the Oxidetalis homeserver"
edition = "2021"
license = "MIT"
authors.workspace = true
readme.workspace = true
repository.workspace = true
version.workspace = true
rust-version.workspace = true
[dependencies]
sea-orm = {workspace = true }
[lints.rust]
unsafe_code = "deny"
[lints.clippy]
wildcard_imports = "deny"
manual_let_else = "deny"
match_bool = "deny"
match_on_vec_items = "deny"
or_fun_call = "deny"
panic = "deny"
unwrap_used = "deny"
missing_assert_message = "warn"
missing_const_for_fn = "warn"
missing_errors_doc = "warn"
absolute_paths = "warn"
cast_lossless = "warn"
clone_on_ref_ptr = "warn"
cloned_instead_of_copied = "warn"
dbg_macro = "warn"
default_trait_access = "warn"
empty_enum_variants_with_brackets = "warn"
empty_line_after_doc_comments = "warn"
empty_line_after_outer_attr = "warn"
empty_structs_with_brackets = "warn"
enum_glob_use = "warn"
equatable_if_let = "warn"
explicit_iter_loop = "warn"
filetype_is_file = "warn"
filter_map_next = "warn"
flat_map_option = "warn"
float_cmp = "warn"
format_push_string = "warn"
future_not_send = "warn"
if_not_else = "warn"
if_then_some_else_none = "warn"
implicit_clone = "warn"
inconsistent_struct_constructor = "warn"
indexing_slicing = "warn"
iter_filter_is_ok = "warn"
iter_filter_is_some = "warn"
iter_not_returning_iterator = "warn"
manual_is_variant_and = "warn"
option_if_let_else = "warn"
option_option = "warn"

View file

@ -0,0 +1,16 @@
# Oxidetalis database entities
This crate contains the database entities for the Oxidetalis homeserver, using
SeaORM.
## Must to know
- Don't import sea_orm things in another crates, import the entities and sea_orm
things from this crate, from `prelude` module.
## How to write a new entity
Check the [SeaORM
documentation](https://www.sea-ql.org/SeaORM/docs/generate-entity/entity-structure/)
for more information about how to write entities.
## License
This crate is licensed under the MIT license.

View file

@ -0,0 +1,23 @@
// OxideTalis Messaging Protocol homeserver core implementation
// Copyright (c) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
pub mod prelude;
pub mod users;

View file

@ -0,0 +1,41 @@
// OxideTalis Messaging Protocol homeserver core implementation
// Copyright (c) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
pub use sea_orm::{
ActiveModelTrait,
ColumnTrait,
EntityTrait,
IntoActiveModel,
Order,
PaginatorTrait,
QueryFilter,
QueryOrder,
QuerySelect,
Set,
SqlErr,
};
pub use super::users::{
ActiveModel as UserActiveModel,
Column as UserColumn,
Entity as UserEntity,
Model as UserModel,
};

View file

@ -0,0 +1,36 @@
// OxideTalis Messaging Protocol homeserver core implementation
// Copyright (c) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "users")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub public_key: String,
pub is_admin: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -0,0 +1,61 @@
[package]
name = "oxidetalis_migrations"
description = "Database migrations for the Oxidetalis homeserver"
edition = "2021"
license = "MIT"
authors.workspace = true
readme.workspace = true
repository.workspace = true
version.workspace = true
rust-version.workspace = true
[dependencies]
sea-orm = { workspace = true }
sea-orm-migration = { version = "0.12.15", default-features = false, features = ["runtime-tokio-rustls", "sqlx-postgres"] }
[lints.rust]
unsafe_code = "deny"
[lints.clippy]
wildcard_imports = "deny"
manual_let_else = "deny"
match_bool = "deny"
match_on_vec_items = "deny"
or_fun_call = "deny"
panic = "deny"
unwrap_used = "deny"
missing_assert_message = "warn"
missing_const_for_fn = "warn"
missing_errors_doc = "warn"
absolute_paths = "warn"
cast_lossless = "warn"
clone_on_ref_ptr = "warn"
cloned_instead_of_copied = "warn"
dbg_macro = "warn"
default_trait_access = "warn"
empty_enum_variants_with_brackets = "warn"
empty_line_after_doc_comments = "warn"
empty_line_after_outer_attr = "warn"
empty_structs_with_brackets = "warn"
enum_glob_use = "warn"
equatable_if_let = "warn"
explicit_iter_loop = "warn"
filetype_is_file = "warn"
filter_map_next = "warn"
flat_map_option = "warn"
float_cmp = "warn"
format_push_string = "warn"
future_not_send = "warn"
if_not_else = "warn"
if_then_some_else_none = "warn"
implicit_clone = "warn"
inconsistent_struct_constructor = "warn"
indexing_slicing = "warn"
iter_filter_is_ok = "warn"
iter_filter_is_some = "warn"
iter_not_returning_iterator = "warn"
manual_is_variant_and = "warn"
option_if_let_else = "warn"
option_option = "warn"

View file

@ -0,0 +1,52 @@
# Oxidetalis database migrations
This crate contains the database migrations for the Oxidetalis homeserver, using
SeaORM.
## How to run the migrations
The migrations are run when the server starts. The server will check if the
database is up-to-date and run the migrations if needed. So, you don't need to
run the migrations manually.
## How to create a new migration
The migrations will saved in the database, so SeaORM will track the migrations,
and you don't need to worry about the migration files, just write the migration
and SeaORM will take care of the rest.
To create a new migration, you need to create a new migration file in the `src`
directory. You can name the file anything you want, for example,
`create_users_table.rs`. The file should contain the migration code, you can
take this as a template:
```rust
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// Here you can write the migration code, the `manager` can do anything you want.
// When the homeserver starts, it will run the `up` function for each migration that is not run yet.
}
}
#[derive(DeriveIden)]
enum TableName {
Table, // Required for the table name
Id, // Required for the primary key
// Add more columns here
d}
```
> [!NOTE] Don't write the `down` function, I prefer to do each migration in a
> separate migration file, so you don't need to write the `down` function. If you
> want to delete a table later, you can create a new migration file that deletes
> the table.
After you write the migration code, you need to add the migration to the
`src/lib.rs` file.
## License
This crate is licensed under the MIT license.

View file

@ -0,0 +1,66 @@
// OxideTalis Messaging Protocol homeserver core implementation
// Copyright (c) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Users::Table)
.if_not_exists()
.col(
ColumnDef::new(Users::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(
ColumnDef::new(Users::PublicKey)
.string()
.not_null()
.unique_key(),
)
.col(
ColumnDef::new(Users::IsAdmin)
.boolean()
.not_null()
.default(false),
)
.to_owned(),
)
.await
}
}
#[derive(DeriveIden)]
enum Users {
Table,
Id,
PublicKey,
IsAdmin,
}

View file

@ -0,0 +1,33 @@
// OxideTalis Messaging Protocol homeserver core implementation
// Copyright (c) 2024 OxideTalis Developers <otmp@4rs.nl>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
pub use sea_orm_migration::prelude::*;
mod create_users_table;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(create_users_table::Migration)]
}
}