refactor: Use PublicKey
and Signature
as parameters
### Changes - Implement `salvo_oapi::Extractible` and `salvo_oapi::EndpointArgRegister` for `PublicKey` - Implement `salvo_oapi::Extractible` and `salvo_oapi::ToParameters` for `Signature` ### Notes I did not implement `salvo_oapi::ToParameters` for `PublicKey` because it will not be used as an OpenAPI parameter. Instead, Salvo will register it using the `EndpointArgRegister` trait. Similarly, I did not implement `salvo_oapi::EndpointArgRegister` for `Signature` because it will not be used as an argument in the endpoint. Instead, the signature will be verified by the signature middleware, and we will only use it as a parameter. Reviewed-by: Amjad Alsharafi <me@amjad.alsharafi.dev> Reviewed-on: #33 Fixes: #21 Helped-by: Amjad Alsharafi <me@amjad.alsharafi.dev> Signed-off-by: Awiteb <a@4rs.nl>
This commit is contained in:
parent
bc00ac08b1
commit
20a8ac6715
13 changed files with 201 additions and 156 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1954,8 +1954,10 @@ dependencies = [
|
||||||
"k256",
|
"k256",
|
||||||
"rand",
|
"rand",
|
||||||
"salvo-oapi",
|
"salvo-oapi",
|
||||||
|
"salvo_core",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
|
@ -22,8 +22,9 @@ thiserror = "1.0.61"
|
||||||
log = "0.4.21"
|
log = "0.4.21"
|
||||||
logcall = "0.1.9"
|
logcall = "0.1.9"
|
||||||
chrono = "0.4.38"
|
chrono = "0.4.38"
|
||||||
|
serde_json = "1.0.117"
|
||||||
sea-orm = { version = "0.12.15", features = ["with-chrono", "macros"] }
|
sea-orm = { version = "0.12.15", features = ["with-chrono", "macros"] }
|
||||||
salvo_core = { version = "0.68.3", default-features = false, features = ["rustls"] }
|
salvo_core = { version = "0.68.3", default-features = false }
|
||||||
salvo-oapi = { version = "0.68.3", default-features = false, features = ["rapidoc","redoc","scalar","swagger-ui"] }
|
salvo-oapi = { version = "0.68.3", default-features = false, features = ["rapidoc","redoc","scalar","swagger-ui"] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|
|
@ -21,12 +21,12 @@ sea-orm = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
salvo = { version = "0.68.2", features = ["rustls", "affix", "logging", "oapi", "rate-limiter", "websocket"] }
|
salvo = { version = "0.68.2", features = ["rustls", "affix", "logging", "oapi", "rate-limiter", "websocket"] }
|
||||||
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
|
||||||
uuid = { version = "1.9.1", default-features = false, features = ["v4"] }
|
uuid = { version = "1.9.1", default-features = false, features = ["v4"] }
|
||||||
derive-new = "0.6.0"
|
derive-new = "0.6.0"
|
||||||
pretty_env_logger = "0.5.0"
|
pretty_env_logger = "0.5.0"
|
||||||
serde_json = "1.0.117"
|
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
rayon = "1.10.0"
|
rayon = "1.10.0"
|
||||||
|
|
|
@ -24,10 +24,8 @@ use salvo::{
|
||||||
Response,
|
Response,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod public_key;
|
|
||||||
mod signature;
|
mod signature;
|
||||||
|
|
||||||
pub use public_key::*;
|
|
||||||
pub use signature::*;
|
pub use signature::*;
|
||||||
|
|
||||||
use crate::{routes::write_json_body, schemas::MessageSchema};
|
use crate::{routes::write_json_body, schemas::MessageSchema};
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
// OxideTalis Messaging Protocol homeserver implementation
|
|
||||||
// Copyright (C) 2024 Awiteb <a@4rs.nl>, OxideTalis Contributors
|
|
||||||
//
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
//! Request signature middleware.
|
//! Request signature middleware.
|
||||||
|
|
||||||
|
use oxidetalis_core::types::{PublicKey, Signature};
|
||||||
use salvo::{
|
use salvo::{
|
||||||
handler,
|
handler,
|
||||||
http::{Body, StatusCode},
|
http::{Body, StatusCode},
|
||||||
|
@ -23,6 +24,7 @@ use salvo::{
|
||||||
FlowCtrl,
|
FlowCtrl,
|
||||||
Request,
|
Request,
|
||||||
Response,
|
Response,
|
||||||
|
Writer,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{extensions::DepotExt, utils};
|
use crate::{extensions::DepotExt, utils};
|
||||||
|
@ -37,6 +39,8 @@ pub async fn signature_check(
|
||||||
res: &mut Response,
|
res: &mut Response,
|
||||||
depot: &mut Depot,
|
depot: &mut Depot,
|
||||||
ctrl: &mut FlowCtrl,
|
ctrl: &mut FlowCtrl,
|
||||||
|
sender_public_key: PublicKey,
|
||||||
|
signature: Signature,
|
||||||
) {
|
) {
|
||||||
const UNAUTHORIZED: StatusCode = StatusCode::UNAUTHORIZED;
|
const UNAUTHORIZED: StatusCode = StatusCode::UNAUTHORIZED;
|
||||||
let mut write_err =
|
let mut write_err =
|
||||||
|
@ -54,22 +58,6 @@ pub async fn signature_check(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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()).await
|
if !utils::is_valid_nonce(&signature, &depot.nonce_cache()).await
|
||||||
|| !depot.config().server.private_key.verify(
|
|| !depot.config().server.private_key.verify(
|
||||||
data.as_bytes(),
|
data.as_bytes(),
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
//! REST API endpoints for user management
|
//! REST API endpoints for user management
|
||||||
|
|
||||||
use oxidetalis_core::types::{PublicKey, Signature};
|
use oxidetalis_core::types::{PublicKey, Signature};
|
||||||
use salvo::{http::StatusCode, oapi::endpoint, writing::Json, Depot, Request, Router, Writer};
|
use salvo::{http::StatusCode, oapi::endpoint, writing::Json, Depot, Router, Writer};
|
||||||
|
|
||||||
use super::{ApiError, ApiResult};
|
use super::{ApiError, ApiResult};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -26,7 +26,6 @@ use crate::{
|
||||||
middlewares,
|
middlewares,
|
||||||
parameters::Pagination,
|
parameters::Pagination,
|
||||||
schemas::{BlackListedUser, EmptySchema, MessageSchema, WhiteListedUser},
|
schemas::{BlackListedUser, EmptySchema, MessageSchema, WhiteListedUser},
|
||||||
utils,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// (🔓) Register a user
|
/// (🔓) Register a user
|
||||||
|
@ -38,22 +37,18 @@ use crate::{
|
||||||
tags("User"),
|
tags("User"),
|
||||||
responses(
|
responses(
|
||||||
(status_code = 201, description = "User registered"),
|
(status_code = 201, description = "User registered"),
|
||||||
(status_code = 400, description = "The entered public key is already registered", content_type = "application/json", body = MessageSchema),
|
(status_code = 400, description = "Invalid public key", content_type = "application/json", body = MessageSchema),
|
||||||
(status_code = 401, description = "The entered signature or public key is invalid", content_type = "application/json", body = MessageSchema),
|
(status_code = 401, description = "Invalid signature", content_type = "application/json", body = MessageSchema),
|
||||||
(status_code = 403, description = "Server registration is closed", content_type = "application/json", body = MessageSchema),
|
(status_code = 403, description = "Server registration is closed", content_type = "application/json", body = MessageSchema),
|
||||||
|
(status_code = 409, description = "The entered public key is already registered", content_type = "application/json", body = MessageSchema),
|
||||||
(status_code = 429, description = "Too many requests", 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),
|
(status_code = 500, description = "Internal server error", content_type = "application/json", body = MessageSchema),
|
||||||
),
|
),
|
||||||
parameters(
|
parameters(Signature),
|
||||||
("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(req: &Request, depot: &mut Depot) -> ApiResult<EmptySchema> {
|
pub async fn register(public_key: PublicKey, depot: &mut Depot) -> ApiResult<EmptySchema> {
|
||||||
let db = depot.db_conn();
|
let db = depot.db_conn();
|
||||||
let config = depot.config();
|
let config = depot.config();
|
||||||
let public_key =
|
|
||||||
utils::extract_public_key(req).expect("Public key should be checked in the middleware");
|
|
||||||
|
|
||||||
if !db.users_exists_in_database().await? {
|
if !db.users_exists_in_database().await? {
|
||||||
db.register_user(&public_key, true).await?;
|
db.register_user(&public_key, true).await?;
|
||||||
|
@ -72,28 +67,22 @@ pub async fn register(req: &Request, depot: &mut Depot) -> ApiResult<EmptySchema
|
||||||
tags("User"),
|
tags("User"),
|
||||||
responses(
|
responses(
|
||||||
(status_code = 200, description = "Returns whitelisted users", content_type = "application/json", body = Vec<WhiteListedUser>),
|
(status_code = 200, description = "Returns whitelisted users", content_type = "application/json", body = Vec<WhiteListedUser>),
|
||||||
(status_code = 400, description = "Wrong query parameter", content_type = "application/json", body = MessageSchema),
|
(status_code = 400, description = "Invalid parameters or public key", content_type = "application/json", body = MessageSchema),
|
||||||
(status_code = 401, description = "The entered signature or public key is invalid", content_type = "application/json", body = MessageSchema),
|
(status_code = 401, description = "Invalid signature", content_type = "application/json", body = MessageSchema),
|
||||||
(status_code = 403, description = "Not registered user, must register first", content_type = "application/json", body = MessageSchema),
|
(status_code = 403, description = "Not registered user, must register first", content_type = "application/json", body = MessageSchema),
|
||||||
(status_code = 429, description = "Too many requests", 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),
|
(status_code = 500, description = "Internal server error", content_type = "application/json", body = MessageSchema),
|
||||||
),
|
),
|
||||||
parameters(
|
parameters(Signature),
|
||||||
("X-OTMP-PUBLIC" = PublicKey, Header, description = "Public key of the sender"),
|
|
||||||
("X-OTMP-SIGNATURE" = Signature, Header, description = "Signature of the request"),
|
|
||||||
),
|
|
||||||
)]
|
)]
|
||||||
async fn user_whitelist(
|
async fn user_whitelist(
|
||||||
req: &mut Request,
|
|
||||||
depot: &mut Depot,
|
depot: &mut Depot,
|
||||||
pagination: Pagination,
|
pagination: Pagination,
|
||||||
|
public_key: PublicKey,
|
||||||
) -> ApiResult<Json<Vec<WhiteListedUser>>> {
|
) -> ApiResult<Json<Vec<WhiteListedUser>>> {
|
||||||
let conn = depot.db_conn();
|
let conn = depot.db_conn();
|
||||||
let user = conn
|
let user = conn
|
||||||
.get_user_by_pubk(
|
.get_user_by_pubk(&public_key)
|
||||||
&utils::extract_public_key(req)
|
|
||||||
.expect("Public key should be checked in the middleware"),
|
|
||||||
)
|
|
||||||
.await?
|
.await?
|
||||||
.ok_or(ApiError::NotRegisteredUser)?;
|
.ok_or(ApiError::NotRegisteredUser)?;
|
||||||
Ok(Json(
|
Ok(Json(
|
||||||
|
@ -111,28 +100,22 @@ async fn user_whitelist(
|
||||||
tags("User"),
|
tags("User"),
|
||||||
responses(
|
responses(
|
||||||
(status_code = 200, description = "Returns blacklisted users", content_type = "application/json", body = Vec<BlackListedUser>),
|
(status_code = 200, description = "Returns blacklisted users", content_type = "application/json", body = Vec<BlackListedUser>),
|
||||||
(status_code = 400, description = "Wrong query parameter", content_type = "application/json", body = MessageSchema),
|
(status_code = 400, description = "Invalid parameters or public key", content_type = "application/json", body = MessageSchema),
|
||||||
(status_code = 401, description = "The entered signature or public key is invalid", content_type = "application/json", body = MessageSchema),
|
(status_code = 401, description = "Invalid signature", content_type = "application/json", body = MessageSchema),
|
||||||
(status_code = 403, description = "Not registered user, must register first", content_type = "application/json", body = MessageSchema),
|
(status_code = 403, description = "Not registered user, must register first", content_type = "application/json", body = MessageSchema),
|
||||||
(status_code = 429, description = "Too many requests", 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),
|
(status_code = 500, description = "Internal server error", content_type = "application/json", body = MessageSchema),
|
||||||
),
|
),
|
||||||
parameters(
|
parameters(Signature),
|
||||||
("X-OTMP-PUBLIC" = PublicKey, Header, description = "Public key of the sender"),
|
|
||||||
("X-OTMP-SIGNATURE" = Signature, Header, description = "Signature of the request"),
|
|
||||||
),
|
|
||||||
)]
|
)]
|
||||||
async fn user_blacklist(
|
async fn user_blacklist(
|
||||||
req: &mut Request,
|
|
||||||
depot: &mut Depot,
|
depot: &mut Depot,
|
||||||
pagination: Pagination,
|
pagination: Pagination,
|
||||||
|
public_key: PublicKey,
|
||||||
) -> ApiResult<Json<Vec<BlackListedUser>>> {
|
) -> ApiResult<Json<Vec<BlackListedUser>>> {
|
||||||
let conn = depot.db_conn();
|
let conn = depot.db_conn();
|
||||||
let user = conn
|
let user = conn
|
||||||
.get_user_by_pubk(
|
.get_user_by_pubk(&public_key)
|
||||||
&utils::extract_public_key(req)
|
|
||||||
.expect("Public key should be checked in the middleware"),
|
|
||||||
)
|
|
||||||
.await?
|
.await?
|
||||||
.ok_or(ApiError::NotRegisteredUser)?;
|
.ok_or(ApiError::NotRegisteredUser)?;
|
||||||
Ok(Json(
|
Ok(Json(
|
||||||
|
@ -150,6 +133,5 @@ pub fn route() -> Router {
|
||||||
.push(Router::with_path("register").post(register))
|
.push(Router::with_path("register").post(register))
|
||||||
.push(Router::with_path("whitelist").get(user_whitelist))
|
.push(Router::with_path("whitelist").get(user_whitelist))
|
||||||
.push(Router::with_path("blacklist").get(user_blacklist))
|
.push(Router::with_path("blacklist").get(user_blacklist))
|
||||||
.hoop(middlewares::public_key_check)
|
|
||||||
.hoop(middlewares::signature_check)
|
.hoop(middlewares::signature_check)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,17 +16,10 @@
|
||||||
|
|
||||||
//! Oxidetalis server utilities, utilities shared across the crate.
|
//! Oxidetalis server utilities, utilities shared across the crate.
|
||||||
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use logcall::logcall;
|
use logcall::logcall;
|
||||||
use oxidetalis_config::Postgres;
|
use oxidetalis_config::Postgres;
|
||||||
use oxidetalis_core::{
|
use oxidetalis_core::types::Signature;
|
||||||
types::{PublicKey, Signature},
|
|
||||||
PUBLIC_KEY_HEADER,
|
|
||||||
SIGNATURE_HEADER,
|
|
||||||
};
|
|
||||||
use salvo::Request;
|
|
||||||
|
|
||||||
use crate::nonce::NonceCache;
|
use crate::nonce::NonceCache;
|
||||||
|
|
||||||
|
@ -48,31 +41,3 @@ pub(crate) async fn is_valid_nonce(signature: &Signature, nonce_cache: &NonceCac
|
||||||
let unused_nonce = new_timestamp && nonce_cache.add_nonce(signature.nonce()).await;
|
let unused_nonce = new_timestamp && nonce_cache.add_nonce(signature.nonce()).await;
|
||||||
new_timestamp && unused_nonce
|
new_timestamp && unused_nonce
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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())?
|
|
||||||
}
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ use salvo::{
|
||||||
Request,
|
Request,
|
||||||
Response,
|
Response,
|
||||||
Router,
|
Router,
|
||||||
|
Writer,
|
||||||
};
|
};
|
||||||
use sea_orm::DatabaseConnection;
|
use sea_orm::DatabaseConnection;
|
||||||
use tokio::{sync::RwLock, task::spawn as tokio_spawn, time::sleep as tokio_sleep};
|
use tokio::{sync::RwLock, task::spawn as tokio_spawn, time::sleep as tokio_sleep};
|
||||||
|
@ -49,7 +50,6 @@ use crate::{
|
||||||
extensions::{DepotExt, OnlineUsersExt},
|
extensions::{DepotExt, OnlineUsersExt},
|
||||||
middlewares,
|
middlewares,
|
||||||
nonce::NonceCache,
|
nonce::NonceCache,
|
||||||
utils,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Online users type
|
/// Online users type
|
||||||
|
@ -96,12 +96,11 @@ impl SocketUserData {
|
||||||
pub async fn user_connected(
|
pub async fn user_connected(
|
||||||
req: &mut Request,
|
req: &mut Request,
|
||||||
res: &mut Response,
|
res: &mut Response,
|
||||||
|
public_key: PublicKey,
|
||||||
depot: &Depot,
|
depot: &Depot,
|
||||||
) -> Result<(), StatusError> {
|
) -> Result<(), StatusError> {
|
||||||
let nonce_cache = depot.nonce_cache();
|
let nonce_cache = depot.nonce_cache();
|
||||||
let db_conn = depot.db_conn();
|
let db_conn = depot.db_conn();
|
||||||
let public_key =
|
|
||||||
utils::extract_public_key(req).expect("The public key was checked in the middleware");
|
|
||||||
let shared_secret = depot.config().server.private_key.shared_secret(&public_key);
|
let shared_secret = depot.config().server.private_key.shared_secret(&public_key);
|
||||||
|
|
||||||
WebSocketUpgrade::new()
|
WebSocketUpgrade::new()
|
||||||
|
@ -259,5 +258,4 @@ pub fn route() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.push(Router::with_path("chat").get(user_connected))
|
.push(Router::with_path("chat").get(user_connected))
|
||||||
.hoop(middlewares::signature_check)
|
.hoop(middlewares::signature_check)
|
||||||
.hoop(middlewares::public_key_check)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,8 @@ thiserror = { workspace = true }
|
||||||
salvo-oapi = { workspace = true, optional = true }
|
salvo-oapi = { workspace = true, optional = true }
|
||||||
serde = { workspace = true, optional = true }
|
serde = { workspace = true, optional = true }
|
||||||
sea-orm = { workspace = true, optional = true }
|
sea-orm = { workspace = true, optional = true }
|
||||||
|
salvo_core = { workspace = true, optional = true }
|
||||||
|
serde_json = { workspace = true, optional = true }
|
||||||
cbc = { version = "0.1.2", features = ["alloc", "std"] }
|
cbc = { version = "0.1.2", features = ["alloc", "std"] }
|
||||||
k256 = { version = "0.13.3", default-features = false, features = ["ecdh"] }
|
k256 = { version = "0.13.3", default-features = false, features = ["ecdh"] }
|
||||||
rand = { version = "0.8.5", default-features = false, features = ["std_rng", "std"] }
|
rand = { version = "0.8.5", default-features = false, features = ["std_rng", "std"] }
|
||||||
|
@ -25,7 +27,7 @@ hmac = "0.12.1"
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
openapi = ["dep:salvo-oapi"]
|
openapi = ["dep:salvo-oapi", "dep:salvo_core", "dep:serde_json"]
|
||||||
serde = ["dep:serde"]
|
serde = ["dep:serde"]
|
||||||
sea-orm = ["dep:sea-orm"]
|
sea-orm = ["dep:sea-orm"]
|
||||||
|
|
||||||
|
|
|
@ -24,15 +24,6 @@
|
||||||
use std::{fmt, str::FromStr};
|
use std::{fmt, str::FromStr};
|
||||||
|
|
||||||
use base58::{FromBase58, ToBase58};
|
use base58::{FromBase58, ToBase58};
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use salvo_oapi::{
|
|
||||||
schema::{
|
|
||||||
Schema as OapiSchema,
|
|
||||||
SchemaFormat as OapiSchemaFormat,
|
|
||||||
SchemaType as OapiSchemaType,
|
|
||||||
},
|
|
||||||
ToSchema,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::cipher::{hmac_sha256, CipherError};
|
use crate::cipher::{hmac_sha256, CipherError};
|
||||||
|
|
||||||
|
@ -206,23 +197,3 @@ impl From<[u8; 56]> for Signature {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
168
crates/oxidetalis_core/src/types/impl_openapi.rs
Normal file
168
crates/oxidetalis_core/src/types/impl_openapi.rs
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
// OxideTalis Messaging Protocol homeserver core implementation
|
||||||
|
// Copyright (C) 2024 Awiteb <a@4rs.nl>, OxideTalis Contributors
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
//! OpenAPI schema for some core types.
|
||||||
|
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use salvo_core::{extract::Metadata as ExtractMetadata, http::StatusError, Extractible, Request};
|
||||||
|
use salvo_oapi::{
|
||||||
|
schema::{
|
||||||
|
Schema as OapiSchema,
|
||||||
|
SchemaFormat as OapiSchemaFormat,
|
||||||
|
SchemaType as OapiSchemaType,
|
||||||
|
},
|
||||||
|
Components as OapiComponents,
|
||||||
|
EndpointArgRegister,
|
||||||
|
Parameter,
|
||||||
|
ParameterIn,
|
||||||
|
Parameters,
|
||||||
|
ToParameters,
|
||||||
|
ToSchema,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{PublicKey as CorePublicKey, Signature};
|
||||||
|
|
||||||
|
impl ToSchema for CorePublicKey {
|
||||||
|
fn to_schema(_components: &mut salvo_oapi::Components) -> salvo_oapi::RefOr<OapiSchema> {
|
||||||
|
salvo_oapi::Object::new()
|
||||||
|
.name(crate::PUBLIC_KEY_HEADER)
|
||||||
|
.description("User's public key")
|
||||||
|
.schema_type(OapiSchemaType::String)
|
||||||
|
.format(OapiSchemaFormat::Custom("base58".to_owned()))
|
||||||
|
.required(crate::PUBLIC_KEY_HEADER)
|
||||||
|
// A 33-byte base58 string can be either 44 or 45 characters long
|
||||||
|
.example("rW8FMG5D75NVNJV3Wd498dEh65BgUuhwY1Yk5zYJPpRe".into())
|
||||||
|
.max_length(45)
|
||||||
|
.min_length(44)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToSchema for Signature {
|
||||||
|
fn to_schema(_components: &mut salvo_oapi::Components) -> salvo_oapi::RefOr<OapiSchema> {
|
||||||
|
salvo_oapi::Object::new()
|
||||||
|
.name(crate::SIGNATURE_HEADER)
|
||||||
|
.description("Signature of the request")
|
||||||
|
.schema_type(OapiSchemaType::String)
|
||||||
|
.format(OapiSchemaFormat::Custom("hex".to_owned()))
|
||||||
|
.required(crate::SIGNATURE_HEADER)
|
||||||
|
// 56 bytes in hex (valid signature)
|
||||||
|
.example("0".repeat(112).into())
|
||||||
|
.max_length(112)
|
||||||
|
.min_length(112)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'ex> Extractible<'ex> for CorePublicKey {
|
||||||
|
fn metadata() -> &'ex ExtractMetadata {
|
||||||
|
static METADATA: ExtractMetadata = ExtractMetadata::new("");
|
||||||
|
&METADATA
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(refining_impl_trait)]
|
||||||
|
async fn extract(req: &'ex mut Request) -> Result<Self, StatusError> {
|
||||||
|
extract_header(req, crate::PUBLIC_KEY_HEADER).and_then(|public_key| {
|
||||||
|
CorePublicKey::from_str(public_key).map_err(|err| {
|
||||||
|
StatusError::bad_request()
|
||||||
|
.brief("Invalid public key")
|
||||||
|
.cause(err.to_string())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(refining_impl_trait)]
|
||||||
|
async fn extract_with_arg(req: &'ex mut Request, _: &str) -> Result<Self, StatusError> {
|
||||||
|
Self::extract(req).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EndpointArgRegister for CorePublicKey {
|
||||||
|
fn register(components: &mut OapiComponents, operation: &mut salvo_oapi::Operation, _: &str) {
|
||||||
|
operation.parameters.insert(
|
||||||
|
Parameter::new(crate::PUBLIC_KEY_HEADER)
|
||||||
|
.parameter_in(ParameterIn::Header)
|
||||||
|
.required(true)
|
||||||
|
.description("User's public key")
|
||||||
|
.example("2BiUSWkJUy5bcdJB8qszq9K6a5EXVHvK41vQWZVkUBUM8".into())
|
||||||
|
.schema(CorePublicKey::to_schema(components)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'ex> Extractible<'ex> for Signature {
|
||||||
|
fn metadata() -> &'ex ExtractMetadata {
|
||||||
|
static METADATA: ExtractMetadata = ExtractMetadata::new("");
|
||||||
|
&METADATA
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(refining_impl_trait)]
|
||||||
|
async fn extract(req: &'ex mut Request) -> Result<Self, StatusError> {
|
||||||
|
extract_header(req, crate::SIGNATURE_HEADER)
|
||||||
|
.and_then(|sig| {
|
||||||
|
Signature::from_str(sig).map_err(|err| {
|
||||||
|
StatusError::unauthorized()
|
||||||
|
.brief("Invalid signature")
|
||||||
|
.cause(err.to_string())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map_err(|err| {
|
||||||
|
StatusError::unauthorized().brief(err.brief).cause(
|
||||||
|
err.cause
|
||||||
|
.expect("The cause was set when we extract the header"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToParameters<'_> for Signature {
|
||||||
|
fn to_parameters(components: &mut OapiComponents) -> Parameters {
|
||||||
|
Parameters::new().parameter(
|
||||||
|
Parameter::new(crate::SIGNATURE_HEADER)
|
||||||
|
.parameter_in(ParameterIn::Header)
|
||||||
|
.required(true)
|
||||||
|
.description("Signature of the request")
|
||||||
|
.example("0".repeat(112).into())
|
||||||
|
.schema(Self::to_schema(components)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_header<'req>(req: &'req Request, name: &str) -> Result<&'req str, StatusError> {
|
||||||
|
req.headers()
|
||||||
|
.get(name)
|
||||||
|
.map(|v| {
|
||||||
|
v.to_str().map_err(|_| {
|
||||||
|
StatusError::bad_request()
|
||||||
|
.brief("Invalid header value")
|
||||||
|
.cause("Header value must be a valid ascii string")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
StatusError::bad_request()
|
||||||
|
.brief(format!("Could not find {name} in headers"))
|
||||||
|
.cause(format!(
|
||||||
|
"{name} is required to authenication and authorization"
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
|
@ -22,6 +22,8 @@
|
||||||
//! Oxidetalis server types
|
//! Oxidetalis server types
|
||||||
|
|
||||||
mod cipher;
|
mod cipher;
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
mod impl_openapi;
|
||||||
#[cfg(feature = "sea-orm")]
|
#[cfg(feature = "sea-orm")]
|
||||||
mod impl_sea_orm;
|
mod impl_sea_orm;
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
|
|
Loading…
Reference in a new issue