chore: Initialize the project
Signed-off-by: Awiteb <a@4rs.nl>
This commit is contained in:
parent
a9ba2c4f31
commit
7e3558d776
42 changed files with 7458 additions and 0 deletions
1
.dockerignore
Normal file
1
.dockerignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/target
|
||||||
|
# Ignore the server configration file (Used for local development only)
|
||||||
|
config.toml
|
4461
Cargo.lock
generated
Normal file
4461
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
30
Cargo.toml
Normal file
30
Cargo.toml
Normal 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.
|
75
crates/oxidetalis/Cargo.toml
Normal file
75
crates/oxidetalis/Cargo.toml
Normal 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"
|
15
crates/oxidetalis/Dockerfile
Normal file
15
crates/oxidetalis/Dockerfile
Normal 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"]
|
21
crates/oxidetalis/src/database/mod.rs
Normal file
21
crates/oxidetalis/src/database/mod.rs
Normal 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::*;
|
60
crates/oxidetalis/src/database/user.rs
Normal file
60
crates/oxidetalis/src/database/user.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
81
crates/oxidetalis/src/errors.rs
Normal file
81
crates/oxidetalis/src/errors.rs
Normal 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()));
|
||||||
|
}
|
||||||
|
}
|
91
crates/oxidetalis/src/extensions.rs
Normal file
91
crates/oxidetalis/src/extensions.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
77
crates/oxidetalis/src/main.rs
Normal file
77
crates/oxidetalis/src/main.rs
Normal 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
|
||||||
|
}
|
58
crates/oxidetalis/src/middlewares/mod.rs
Normal file
58
crates/oxidetalis/src/middlewares/mod.rs
Normal 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();
|
||||||
|
}
|
32
crates/oxidetalis/src/middlewares/public_key.rs
Normal file
32
crates/oxidetalis/src/middlewares/public_key.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
86
crates/oxidetalis/src/middlewares/signature.rs
Normal file
86
crates/oxidetalis/src/middlewares/signature.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
162
crates/oxidetalis/src/routes/mod.rs
Normal file
162
crates/oxidetalis/src/routes/mod.rs
Normal 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),
|
||||||
|
)
|
||||||
|
}
|
88
crates/oxidetalis/src/routes/user.rs
Normal file
88
crates/oxidetalis/src/routes/user.rs
Normal 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)
|
||||||
|
}
|
71
crates/oxidetalis/src/schemas/mod.rs
Normal file
71
crates/oxidetalis/src/schemas/mod.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
27
crates/oxidetalis/src/schemas/user.rs
Normal file
27
crates/oxidetalis/src/schemas/user.rs
Normal 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,
|
||||||
|
}
|
95
crates/oxidetalis/src/utils.rs
Normal file
95
crates/oxidetalis/src/utils.rs
Normal 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())?
|
||||||
|
}
|
71
crates/oxidetalis_config/Cargo.toml
Normal file
71
crates/oxidetalis_config/Cargo.toml
Normal 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"
|
25
crates/oxidetalis_config/README.md
Normal file
25
crates/oxidetalis_config/README.md
Normal 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.
|
98
crates/oxidetalis_config/src/commandline.rs
Normal file
98
crates/oxidetalis_config/src/commandline.rs
Normal 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>,
|
||||||
|
}
|
109
crates/oxidetalis_config/src/defaults.rs
Normal file
109
crates/oxidetalis_config/src/defaults.rs
Normal 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
|
||||||
|
}
|
247
crates/oxidetalis_config/src/lib.rs
Normal file
247
crates/oxidetalis_config/src/lib.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
63
crates/oxidetalis_config/src/serde_with.rs
Normal file
63
crates/oxidetalis_config/src/serde_with.rs
Normal 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)
|
||||||
|
}
|
126
crates/oxidetalis_config/src/types.rs
Normal file
126
crates/oxidetalis_config/src/types.rs
Normal 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())
|
||||||
|
}
|
74
crates/oxidetalis_core/Cargo.toml
Normal file
74
crates/oxidetalis_core/Cargo.toml
Normal 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"
|
224
crates/oxidetalis_core/src/cipher.rs
Normal file
224
crates/oxidetalis_core/src/cipher.rs
Normal 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()
|
||||||
|
}
|
33
crates/oxidetalis_core/src/lib.rs
Normal file
33
crates/oxidetalis_core/src/lib.rs
Normal 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";
|
213
crates/oxidetalis_core/src/types/cipher.rs
Normal file
213
crates/oxidetalis_core/src/types/cipher.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
99
crates/oxidetalis_core/src/types/impl_serde.rs
Normal file
99
crates/oxidetalis_core/src/types/impl_serde.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
29
crates/oxidetalis_core/src/types/mod.rs
Normal file
29
crates/oxidetalis_core/src/types/mod.rs
Normal 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::*;
|
125
crates/oxidetalis_core/src/types/size.rs
Normal file
125
crates/oxidetalis_core/src/types/size.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
60
crates/oxidetalis_entities/Cargo.toml
Normal file
60
crates/oxidetalis_entities/Cargo.toml
Normal 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"
|
16
crates/oxidetalis_entities/README.md
Normal file
16
crates/oxidetalis_entities/README.md
Normal 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.
|
23
crates/oxidetalis_entities/src/lib.rs
Normal file
23
crates/oxidetalis_entities/src/lib.rs
Normal 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;
|
41
crates/oxidetalis_entities/src/prelude.rs
Normal file
41
crates/oxidetalis_entities/src/prelude.rs
Normal 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,
|
||||||
|
};
|
36
crates/oxidetalis_entities/src/users.rs
Normal file
36
crates/oxidetalis_entities/src/users.rs
Normal 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 {}
|
61
crates/oxidetalis_migrations/Cargo.toml
Normal file
61
crates/oxidetalis_migrations/Cargo.toml
Normal 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"
|
52
crates/oxidetalis_migrations/README.md
Normal file
52
crates/oxidetalis_migrations/README.md
Normal 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.
|
66
crates/oxidetalis_migrations/src/create_users_table.rs
Normal file
66
crates/oxidetalis_migrations/src/create_users_table.rs
Normal 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,
|
||||||
|
}
|
33
crates/oxidetalis_migrations/src/lib.rs
Normal file
33
crates/oxidetalis_migrations/src/lib.rs
Normal 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)]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue