This commit is contained in:
Alois 2026-04-02 22:28:41 +02:00
commit b9b1f1fc1c
22 changed files with 8234 additions and 0 deletions

275
src/server/api.rs Normal file
View file

@ -0,0 +1,275 @@
use crate::get_public_key;
use crate::sql::sql;
use crate::sql::user_online_tracker::get_iota_primary_omikron_connection;
use crate::transport::omikron_manager::get_random_omikron;
use crate::util::file_util::get_directory;
use crate::{
sql::sql::{get_by_user_id, get_omikron_by_id},
util::crypto_helper::public_key_to_base64,
};
use actix_web::HttpResponse;
use actix_web::http::{StatusCode, header};
use base64::Engine as _;
use json::JsonValue;
pub async fn handle(path: &str, body_string: Option<String>) -> HttpResponse {
if path == "OPTIONS" {
return HttpResponse::Ok()
.insert_header(("Access-Control-Allow-Origin", "*"))
.insert_header(("Access-Control-Allow-Methods", "GET, POST, OPTIONS"))
.insert_header(("Access-Control-Allow-Headers", "*"))
.finish();
}
let path_parts: Vec<&str> = path.split("/").filter(|s| !s.is_empty()).collect();
let _body: Option<JsonValue> = if body_string.is_some() {
if let Ok(body_json) = json::parse(&body_string.unwrap()) {
Some(body_json)
} else {
None
}
} else {
None
};
let (status, body_text) = match path_parts.as_slice() {
// ==================================================
// DOWNLOAD IOTA FRONTEND
// ==================================================
["api", "download", "iota_frontend"] => {
let file_path = format!("{}/downloads/iota_frontend.zip", get_directory());
match std::fs::read(file_path) {
Ok(file_bytes) => {
return HttpResponse::Ok()
.insert_header(("Access-Control-Allow-Origin", "*"))
.insert_header(("Content-Type", "application/zip"))
.insert_header((
"Content-Disposition",
"attachment; filename=\"iota_frontend.zip\"",
))
.body(file_bytes);
}
Err(_) => {
let mut res = JsonValue::new_object();
res["status"] = "error_not_found".into();
return HttpResponse::NotFound()
.insert_header(("Access-Control-Allow-Origin", "*"))
.body(res.dump());
}
}
}
// ==================================================
// GET RANDOM OMIKRON
// ==================================================
["api", "get", "omikron"] => {
if let Ok(omikron_conn) = get_random_omikron().await {
if let Some(id) = omikron_conn.get_omikron_id().await {
if let Ok((public_key, ip_address)) = sql::get_omikron_by_id(id).await {
let mut res = JsonValue::new_object();
res["status"] = "success".into();
res["id"] = id.into();
res["public_key"] = public_key.into();
res["ip_address"] = ip_address.into();
(StatusCode::OK, res.dump())
} else {
let mut res = JsonValue::new_object();
res["status"] = "error".into();
(StatusCode::INTERNAL_SERVER_ERROR, res.dump())
}
} else {
let mut res = JsonValue::new_object();
res["status"] = "error".into();
(StatusCode::INTERNAL_SERVER_ERROR, res.dump())
}
} else {
let mut res = JsonValue::new_object();
res["status"] = "error_not_found".into();
(StatusCode::NOT_FOUND, res.dump())
}
}
// ==================================================
// GET OMIKRON BY ID
// ==================================================
["api", "get", "omikron", id] => {
let id = id.parse::<i64>().unwrap_or(0);
if id == 0 {
let mut res = JsonValue::new_object();
res["status"] = "error_bad_request".into();
(StatusCode::BAD_REQUEST, res.dump())
} else if let Ok((public_key, ip_address)) = get_omikron_by_id(id).await {
let mut res = JsonValue::new_object();
res["status"] = "success".into();
res["id"] = id.into();
res["public_key"] = public_key.into();
res["ip_address"] = ip_address.into();
(StatusCode::OK, res.dump())
} else if let Some(omikron_id) = get_iota_primary_omikron_connection(id) {
if let Ok((public_key, ip_address)) = get_omikron_by_id(omikron_id).await {
let mut res = JsonValue::new_object();
res["status"] = "success".into();
res["id"] = omikron_id.into();
res["public_key"] = public_key.into();
res["ip_address"] = ip_address.into();
(StatusCode::OK, res.dump())
} else {
let mut res = JsonValue::new_object();
res["status"] = "error_not_found".into();
(StatusCode::NOT_FOUND, res.dump())
}
} else if let Ok((_, iota_id, _, _, _, _, _, _, _, _, _, _)) = get_by_user_id(id).await
{
if let Some(omikron_id) = get_iota_primary_omikron_connection(iota_id) {
if let Ok((public_key, ip_address)) = get_omikron_by_id(omikron_id).await {
let mut res = JsonValue::new_object();
res["status"] = "success".into();
res["id"] = omikron_id.into();
res["public_key"] = public_key.into();
res["ip_address"] = ip_address.into();
(StatusCode::OK, res.dump())
} else {
let mut res = JsonValue::new_object();
res["status"] = "error_not_found".into();
(StatusCode::NOT_FOUND, res.dump())
}
} else {
let mut res = JsonValue::new_object();
res["status"] = "error_not_found".into();
(StatusCode::NOT_FOUND, res.dump())
}
} else {
let mut res = JsonValue::new_object();
res["status"] = "error_not_found".into();
(StatusCode::NOT_FOUND, res.dump())
}
}
// ==================================================
// GET ID BY USERNAME
// ==================================================
["api", "get", "id", username] => {
if username.is_empty() {
let mut res = JsonValue::new_object();
res["status"] = "error_bad_request".into();
(StatusCode::BAD_REQUEST, res.dump())
} else if let Ok((
id,
iota_id,
username,
_,
_,
_,
_,
sub_level,
sub_end,
public_key,
_,
_,
)) = sql::get_by_username(username).await
{
let mut res = JsonValue::new_object();
res["status"] = "success".into();
res["username"] = username.into();
res["public_key"] = public_key.into();
res["user_id"] = id.into();
res["iota_id"] = iota_id.into();
res["sub_level"] = sub_level.into();
res["sub_end"] = sub_end.into();
(StatusCode::OK, res.dump())
} else {
let mut res = JsonValue::new_object();
res["status"] = "error_not_found".into();
(StatusCode::OK, res.dump())
}
}
// ==================================================
// GET SERVER PUBLIC KEY
// ==================================================
["api", "get", "public_key"] => {
let mut res = JsonValue::new_object();
res["status"] = "success".into();
res["public_key"] = public_key_to_base64(&get_public_key()).into();
(StatusCode::OK, res.dump())
}
// ==================================================
// GET USER BY ID
// ==================================================
["api", "get", "user", id] => {
let id: i64 = id.parse().unwrap_or(0);
if id == 0 {
let mut res = JsonValue::new_object();
res["status"] = "error_bad_request".into();
(StatusCode::BAD_REQUEST, res.dump())
} else if let Ok((
id,
iota_id,
username,
display,
status_msg,
about,
avatar,
sub_level,
sub_end,
public_key,
_,
_,
)) = sql::get_by_user_id(id).await
{
let mut res = JsonValue::new_object();
res["status"] = "success".into();
res["username"] = username.into();
res["public_key"] = public_key.into();
res["user_id"] = id.into();
res["iota_id"] = iota_id.into();
res["sub_level"] = sub_level.into();
res["sub_end"] = sub_end.into();
if let Some(display) = display {
res["display"] = display.into();
}
if let Some(status_msg) = status_msg {
res["status_message"] = status_msg.into();
}
if let Some(about) = about {
res["about"] = about.into();
}
if let Some(avatar) = avatar {
res["avatar"] = base64::engine::general_purpose::STANDARD
.encode(avatar)
.into();
}
(StatusCode::OK, res.dump())
} else {
let mut res = JsonValue::new_object();
res["status"] = "error_not_found".into();
(StatusCode::OK, res.dump())
}
}
// ==================================================
// DEFAULT
// ==================================================
_ => {
let mut res = JsonValue::new_object();
res["status"] = "error".into();
(StatusCode::INTERNAL_SERVER_ERROR, res.dump())
}
};
let body_bytes = body_text.into_bytes();
HttpResponse::build(status)
.insert_header((header::ACCESS_CONTROL_ALLOW_ORIGIN, "*"))
.insert_header((header::ACCESS_CONTROL_ALLOW_HEADERS, "*"))
.insert_header((header::ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, OPTIONS"))
.body(body_bytes)
}

3
src/server/mod.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod api;
pub mod server;
pub mod short_link;

67
src/server/server.rs Normal file
View file

@ -0,0 +1,67 @@
use crate::{
log,
server::{api, short_link::get_short_link},
util::file_util::load_file_buf,
};
use actix_web::{App, HttpRequest, HttpResponse, HttpServer, Responder, http::header, web};
use rustls::ServerConfig;
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use rustls_pemfile::{certs, pkcs8_private_keys};
pub async fn start(port: u16) -> anyhow::Result<()> {
let mut cert_reader = load_file_buf("certs", "server_cert.pem")?;
let mut key_reader = load_file_buf("certs", "server_key.pem")?;
let cert_chain: Vec<CertificateDer<'static>> =
certs(&mut cert_reader).collect::<Result<_, _>>()?;
let mut keys: Vec<PrivateKeyDer<'static>> = pkcs8_private_keys(&mut key_reader)
.map(|res| res.map(Into::into))
.collect::<Result<_, _>>()?;
let key = keys.remove(0);
let mut config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(cert_chain, key)?;
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
let addr = format!("0.0.0.0:{port}");
log!(" Server on {}", addr);
HttpServer::new(move || {
App::new()
.route("/api/{path:.*}", web::to(api_handler))
.route("/direct/{path:.*}", web::to(direct_handler))
})
.bind_rustls_0_23(addr, config)?
.run()
.await?;
Ok(())
}
async fn direct_handler(req: HttpRequest) -> impl Responder {
let path = req.uri().path().to_string();
let short = path.replace("/direct/", "");
if let Ok(long) = get_short_link(&short).await {
HttpResponse::TemporaryRedirect()
.append_header((header::LOCATION, long))
.finish()
} else {
HttpResponse::TemporaryRedirect()
.append_header((header::LOCATION, "https://tensamin.net"))
.finish()
}
}
async fn api_handler(req: HttpRequest, body: web::Bytes) -> HttpResponse {
let path = req.uri().path().to_string();
let body_string = String::from_utf8_lossy(&body).to_string();
api::handle(&path, Some(body_string)).await
}

87
src/server/short_link.rs Normal file
View file

@ -0,0 +1,87 @@
use dashmap::DashMap;
use once_cell::sync::Lazy;
use rand::{Rng, thread_rng};
static LINKS: Lazy<DashMap<String, String>> = Lazy::new(DashMap::new);
const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPRSTUVWXYZ1234567890";
pub async fn add_short_link(long: &str) -> Result<String, ()> {
let raw = generate_unique_short_link().await;
LINKS.insert(raw.clone(), long.to_string());
Ok(format!(
"https://omega.tensamin.net/direct/{}",
format_with_dashes(&raw)
))
}
async fn generate_unique_short_link() -> String {
loop {
let short = generate_short_link().await;
if !LINKS.contains_key(&short) {
return short;
}
}
}
pub async fn generate_short_link() -> String {
let len = short_length();
let mut rng = thread_rng();
(0..len)
.map(|_| {
let idx = rng.gen_range(0..CHARSET.len());
CHARSET[idx] as char
})
.collect()
}
pub async fn get_short_link(short: &str) -> Result<String, ()> {
let key = if short.contains("/") {
short.split("/").nth(1).unwrap_or_default()
} else {
short
};
let frag = short.replace(key, "");
let normalized = normalize_short(&key);
if let Ok(t) = LINKS.get(&normalized).map(|v| v.value().clone()).ok_or(()) {
Ok(format!("{}{}", t, frag))
} else {
Err(())
}
}
/* ---------------- helpers ---------------- */
fn short_length() -> usize {
let count = LINKS.len();
match count {
0..=1_999 => 4,
2_000..=999_999 => 8,
_ => 12,
}
}
fn format_with_dashes(s: &str) -> String {
s.chars()
.collect::<Vec<_>>()
.chunks(4)
.map(|c| c.iter().collect::<String>())
.collect::<Vec<_>>()
.join("-")
}
fn normalize_short(input: &str) -> String {
input
.chars()
.filter(|c| *c != '-')
.map(|c| match c {
'Q' | 'O' => '0',
'I' => 'l',
_ => c,
})
.collect()
}