Compare commits

...
Author SHA1 Message Date
07093a42b2
Merge branch 'main' of ssh://git.methanium.net/tensamin/omega 2026-07-27 20:37:38 +02:00
8082050170
[Fix] Stability 2026-07-27 20:36:23 +02:00
20 changed files with 563 additions and 464 deletions

View file

@ -1,2 +1,2 @@
[env] [env]
MTP_TYPE_MAPS = { value = "type-maps.yaml", relative = true } MTP_TYPE_MAPS = { value = "mtp-type-maps/type-maps.yaml", relative = true }

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "mtp-type-maps"]
path = mtp-type-maps
url = https://git.methanium.net/tensamin/mtp-type-maps

85
Cargo.lock generated
View file

@ -20,9 +20,9 @@ dependencies = [
[[package]] [[package]]
name = "aes" name = "aes"
version = "0.9.1" version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" checksum = "f8eb277bec05f56a0e0591f155a484cbd0f4f07ff2905051a48c72f004f7ed58"
dependencies = [ dependencies = [
"cipher 0.5.2", "cipher 0.5.2",
"cpubits", "cpubits",
@ -91,7 +91,7 @@ checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 3.0.2", "syn 3.0.3",
] ]
[[package]] [[package]]
@ -217,9 +217,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.3.0" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver", "jobserver",
@ -320,15 +320,6 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "const-oid" name = "const-oid"
version = "0.9.6" version = "0.9.6"
@ -641,9 +632,9 @@ dependencies = [
[[package]] [[package]]
name = "either" name = "either"
version = "1.16.0" version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -686,11 +677,10 @@ dependencies = [
[[package]] [[package]]
name = "event-listener" name = "event-listener"
version = "5.4.1" version = "5.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
dependencies = [ dependencies = [
"concurrent-queue",
"parking", "parking",
"pin-project-lite", "pin-project-lite",
] ]
@ -1124,6 +1114,12 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]] [[package]]
name = "hybrid-array" name = "hybrid-array"
version = "0.4.13" version = "0.4.13"
@ -1148,6 +1144,7 @@ dependencies = [
"http", "http",
"http-body", "http-body",
"httparse", "httparse",
"httpdate",
"itoa", "itoa",
"pin-project-lite", "pin-project-lite",
"smallvec", "smallvec",
@ -1444,9 +1441,9 @@ checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.186" version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]] [[package]]
name = "libm" name = "libm"
@ -1617,7 +1614,7 @@ dependencies = [
[[package]] [[package]]
name = "mtp" name = "mtp"
version = "0.2.0" version = "0.2.0"
source = "git+https://git.methanium.net/methanium/mtp#04760fd88d2bc3adf548a9ec532fa228227f0a49" source = "git+https://git.methanium.net/methanium/mtp#88ae866b91857c9b1650ea0ee2af2fdb70b92f10"
dependencies = [ dependencies = [
"mtp-client", "mtp-client",
"mtp-codec", "mtp-codec",
@ -1633,7 +1630,7 @@ dependencies = [
[[package]] [[package]]
name = "mtp-client" name = "mtp-client"
version = "0.2.0" version = "0.2.0"
source = "git+https://git.methanium.net/methanium/mtp#04760fd88d2bc3adf548a9ec532fa228227f0a49" source = "git+https://git.methanium.net/methanium/mtp#88ae866b91857c9b1650ea0ee2af2fdb70b92f10"
dependencies = [ dependencies = [
"mtp-codec", "mtp-codec",
"mtp-common", "mtp-common",
@ -1646,7 +1643,7 @@ dependencies = [
[[package]] [[package]]
name = "mtp-codec" name = "mtp-codec"
version = "0.2.0" version = "0.2.0"
source = "git+https://git.methanium.net/methanium/mtp#04760fd88d2bc3adf548a9ec532fa228227f0a49" source = "git+https://git.methanium.net/methanium/mtp#88ae866b91857c9b1650ea0ee2af2fdb70b92f10"
dependencies = [ dependencies = [
"base64", "base64",
"byteorder", "byteorder",
@ -1659,7 +1656,7 @@ dependencies = [
[[package]] [[package]]
name = "mtp-common" name = "mtp-common"
version = "0.2.0" version = "0.2.0"
source = "git+https://git.methanium.net/methanium/mtp#04760fd88d2bc3adf548a9ec532fa228227f0a49" source = "git+https://git.methanium.net/methanium/mtp#88ae866b91857c9b1650ea0ee2af2fdb70b92f10"
dependencies = [ dependencies = [
"quinn", "quinn",
"rustls", "rustls",
@ -1670,7 +1667,7 @@ dependencies = [
[[package]] [[package]]
name = "mtp-crypto" name = "mtp-crypto"
version = "0.2.0" version = "0.2.0"
source = "git+https://git.methanium.net/methanium/mtp#04760fd88d2bc3adf548a9ec532fa228227f0a49" source = "git+https://git.methanium.net/methanium/mtp#88ae866b91857c9b1650ea0ee2af2fdb70b92f10"
dependencies = [ dependencies = [
"base64", "base64",
"chacha20poly1305", "chacha20poly1305",
@ -1692,7 +1689,7 @@ dependencies = [
[[package]] [[package]]
name = "mtp-files" name = "mtp-files"
version = "0.2.0" version = "0.2.0"
source = "git+https://git.methanium.net/methanium/mtp#04760fd88d2bc3adf548a9ec532fa228227f0a49" source = "git+https://git.methanium.net/methanium/mtp#88ae866b91857c9b1650ea0ee2af2fdb70b92f10"
dependencies = [ dependencies = [
"mtp-crypto", "mtp-crypto",
"rand 0.10.2", "rand 0.10.2",
@ -1703,7 +1700,7 @@ dependencies = [
[[package]] [[package]]
name = "mtp-host" name = "mtp-host"
version = "0.2.0" version = "0.2.0"
source = "git+https://git.methanium.net/methanium/mtp#04760fd88d2bc3adf548a9ec532fa228227f0a49" source = "git+https://git.methanium.net/methanium/mtp#88ae866b91857c9b1650ea0ee2af2fdb70b92f10"
dependencies = [ dependencies = [
"mtp-codec", "mtp-codec",
"mtp-common", "mtp-common",
@ -1718,7 +1715,7 @@ dependencies = [
[[package]] [[package]]
name = "mtp-transport" name = "mtp-transport"
version = "0.2.0" version = "0.2.0"
source = "git+https://git.methanium.net/methanium/mtp#04760fd88d2bc3adf548a9ec532fa228227f0a49" source = "git+https://git.methanium.net/methanium/mtp#88ae866b91857c9b1650ea0ee2af2fdb70b92f10"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"mtp-codec", "mtp-codec",
@ -1736,7 +1733,7 @@ dependencies = [
[[package]] [[package]]
name = "mtp-type-map" name = "mtp-type-map"
version = "0.2.0" version = "0.2.0"
source = "git+https://git.methanium.net/methanium/mtp#04760fd88d2bc3adf548a9ec532fa228227f0a49" source = "git+https://git.methanium.net/methanium/mtp#88ae866b91857c9b1650ea0ee2af2fdb70b92f10"
dependencies = [ dependencies = [
"serde", "serde",
"serde_yaml", "serde_yaml",
@ -1745,7 +1742,7 @@ dependencies = [
[[package]] [[package]]
name = "mtp-webserver" name = "mtp-webserver"
version = "0.2.0" version = "0.2.0"
source = "git+https://git.methanium.net/methanium/mtp#04760fd88d2bc3adf548a9ec532fa228227f0a49" source = "git+https://git.methanium.net/methanium/mtp#88ae866b91857c9b1650ea0ee2af2fdb70b92f10"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bytes", "bytes",
@ -1753,6 +1750,9 @@ dependencies = [
"h3-quinn", "h3-quinn",
"h3-webtransport", "h3-webtransport",
"http", "http",
"http-body-util",
"hyper",
"hyper-util",
"mtp-codec", "mtp-codec",
"mtp-common", "mtp-common",
"mtp-crypto", "mtp-crypto",
@ -1763,6 +1763,8 @@ dependencies = [
"rustls", "rustls",
"thiserror 2.0.19", "thiserror 2.0.19",
"tokio", "tokio",
"tokio-rustls",
"tokio-stream",
"tracing", "tracing",
] ]
@ -2355,9 +2357,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.15.0" version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
dependencies = [ dependencies = [
"web-time", "web-time",
"zeroize", "zeroize",
@ -2494,7 +2496,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 3.0.2", "syn 3.0.3",
] ]
[[package]] [[package]]
@ -2966,9 +2968,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "3.0.2" version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3053,7 +3055,7 @@ checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 3.0.2", "syn 3.0.3",
] ]
[[package]] [[package]]
@ -3152,9 +3154,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-stream" name = "tokio-stream"
version = "0.1.18" version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"pin-project-lite", "pin-project-lite",
@ -3163,13 +3165,14 @@ dependencies = [
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.18" version = "0.7.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"libc",
"pin-project-lite", "pin-project-lite",
"tokio", "tokio",
] ]

View file

@ -1,3 +1,8 @@
CREATE TABLE IF NOT EXISTS iotas (
id BIGINT NOT NULL PRIMARY KEY,
public_key BLOB NOT NULL
);
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id BIGINT NOT NULL PRIMARY KEY, id BIGINT NOT NULL PRIMARY KEY,
iota_id BIGINT NOT NULL, iota_id BIGINT NOT NULL,
@ -10,14 +15,15 @@ CREATE TABLE IF NOT EXISTS users (
sub_end BIGINT NOT NULL DEFAULT 0, sub_end BIGINT NOT NULL DEFAULT 0,
public_key BLOB NOT NULL, public_key BLOB NOT NULL,
token BLOB NOT NULL, token BLOB NOT NULL,
UNIQUE KEY uk_users_username (username), UNIQUE KEY uk_users_username (username),
UNIQUE KEY uk_users_iota_id (iota_id) KEY idx_users_iota_id (iota_id),
CONSTRAINT fk_users_iota
FOREIGN KEY (iota_id)
REFERENCES iotas (id)
); );
CREATE TABLE IF NOT EXISTS iotas (
id BIGINT NOT NULL PRIMARY KEY,
public_key BLOB NOT NULL
);
CREATE TABLE IF NOT EXISTS omikrons ( CREATE TABLE IF NOT EXISTS omikrons (
id BIGINT NOT NULL PRIMARY KEY, id BIGINT NOT NULL PRIMARY KEY,

View file

@ -0,0 +1,13 @@
-- Registration IDs are allocated before the user row is created. Keep the
-- allocation durable and bound to the Iota that requested it so a completed
-- registration can safely be retried after any response is lost.
CREATE TABLE registration_leases (
token CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL PRIMARY KEY,
user_id BIGINT NOT NULL UNIQUE,
iota_id BIGINT NOT NULL,
expires_at DATETIME NOT NULL,
completed_at DATETIME NULL,
CONSTRAINT fk_registration_leases_iota
FOREIGN KEY (iota_id) REFERENCES iotas (id),
INDEX idx_registration_leases_expiry (expires_at)
);

View file

@ -0,0 +1,8 @@
-- A relay retry uses the original MTP request ID. Make allocation idempotent
-- for that authenticated Iota/request pair, so a lost response returns the
-- same user ID and lease rather than allocating another one.
ALTER TABLE registration_leases
-- Existing leases predate retry correlation; leave their request ID NULL
-- rather than assigning a shared sentinel that could violate uniqueness.
ADD COLUMN request_id INT UNSIGNED NULL,
ADD CONSTRAINT uk_registration_leases_iota_request UNIQUE (iota_id, request_id);

1
mtp-type-maps Submodule

@ -0,0 +1 @@
Subproject commit 594646ac39d986f0787aa614a99d580035a67318

View file

@ -9,8 +9,8 @@ pub struct RateLimitConfig {
pub transport_connections_per_ip: usize, pub transport_connections_per_ip: usize,
} }
pub fn cors_origin() -> String { pub const fn cors_origin() -> &'static str {
env::var("CORS_ORIGIN").unwrap_or_else(|_| "https://tensamin.net".to_string()) "*"
} }
impl Default for RateLimitConfig { impl Default for RateLimitConfig {

View file

@ -21,13 +21,31 @@ pub async fn get_iota_by_id(id: IotaId) -> Result<Iota> {
} }
pub async fn create_new_iota(public_key: PublicKeyBundle) -> Result<IotaId> { pub async fn create_new_iota(public_key: PublicKeyBundle) -> Result<IotaId> {
let id = crate::db::user_repo::get_register_id().await?; for _ in 0..16 {
let iota_id = IotaId::from(id.0); let id = crate::db::user_repo::get_register_id().await?;
register_complete_iota(iota_id, public_key).await?; let iota_id = IotaId::from(id.0);
Ok(iota_id) match register_complete_iota(iota_id, public_key.clone()).await {
Ok(()) => return Ok(iota_id),
Err(OmegaError::Database(error)) => {
if crate::db::user_repo::is_duplicate_key(&error) {
continue;
}
return Err(OmegaError::Database(error));
}
Err(error) => return Err(error),
}
}
Err(OmegaError::Validation(
"could not allocate a unique Iota ID".into(),
))
} }
pub async fn register_complete_iota(id: IotaId, public_key: PublicKeyBundle) -> Result<()> { pub async fn register_complete_iota(id: IotaId, public_key: PublicKeyBundle) -> Result<()> {
if !crate::db::user_repo::valid_protocol_id(id.0) {
return Err(OmegaError::Validation(
"Iota ID is outside the 48-bit protocol range".into(),
));
}
sqlx::query("INSERT INTO iotas (id, public_key) VALUES (?, ?)") sqlx::query("INSERT INTO iotas (id, public_key) VALUES (?, ?)")
.bind(id.0) .bind(id.0)
.bind(public_key.as_bytes()) .bind(public_key.as_bytes())

View file

@ -4,17 +4,94 @@ use crate::{
models::{IotaId, User, UserId}, models::{IotaId, User, UserId},
}; };
use mtp::crypto::PublicKeyBundle; use mtp::crypto::PublicKeyBundle;
use sqlx::FromRow; use sqlx::{FromRow, Row};
pub const MAX_PROTOCOL_ID: i64 = (1_i64 << 48) - 1;
const ID_ALLOCATION_ATTEMPTS: usize = 16;
pub async fn get_register_id() -> Result<UserId> { pub async fn get_register_id() -> Result<UserId> {
let bytes = *uuid::Uuid::now_v7().as_bytes(); use std::time::{SystemTime, UNIX_EPOCH};
let id =
i64::from_be_bytes(bytes[8..].try_into().map_err(|_| { let timestamp = SystemTime::now()
OmegaError::Validation("generated ID has an invalid length".to_string()) .duration_since(UNIX_EPOCH)
})?) & i64::MAX; .map(|d| d.as_secs())
.unwrap_or(0);
let ts = timestamp as i64;
if ts >= 1 && ts <= MAX_PROTOCOL_ID {
return Ok(UserId::from(ts));
}
// Fall back to random if the timestamp is outside the 48-bit range.
let id = (rand::random::<u64>() & ((1_u64 << 48) - 1)) as i64;
Ok(UserId::from(id.max(1))) Ok(UserId::from(id.max(1)))
} }
pub fn valid_protocol_id(id: i64) -> bool {
(1..=MAX_PROTOCOL_ID).contains(&id)
}
/// Allocate an ID that is durable, short-lived, and bound to the connected
/// Iota. `token` is presented again when completing the registration.
pub async fn allocate_registration(iota_id: IotaId, request_id: u32) -> Result<(UserId, String)> {
if !valid_protocol_id(iota_id.0) {
return Err(OmegaError::Validation(
"Iota ID is outside the 48-bit protocol range".into(),
));
}
for _ in 0..ID_ALLOCATION_ATTEMPTS {
let id = get_register_id().await?;
let token = uuid::Uuid::new_v4().to_string();
let result = sqlx::query(
"INSERT INTO registration_leases (token, user_id, iota_id, request_id, expires_at) \
VALUES (?, ?, ?, ?, DATE_ADD(UTC_TIMESTAMP(), INTERVAL 10 MINUTE))",
)
.bind(&token)
.bind(id.0)
.bind(iota_id.0)
.bind(request_id)
.execute(&pool().await?)
.await;
match result {
Ok(_) => return Ok((id, token)),
Err(error) if is_duplicate_key(&error) => {
let existing = sqlx::query(
"SELECT user_id, token, expires_at >= UTC_TIMESTAMP() AS current \
FROM registration_leases WHERE iota_id = ? AND request_id = ?",
)
.bind(iota_id.0)
.bind(request_id)
.fetch_optional(&pool().await?)
.await?;
if let Some(existing) = existing {
let current: i8 = existing.get("current");
if current != 0 {
return Ok((
UserId::from(existing.get::<i64, _>("user_id")),
existing.get::<String, _>("token"),
));
}
}
continue;
}
Err(error) => return Err(error.into()),
}
}
Err(OmegaError::Validation(
"could not allocate a unique registration ID".into(),
))
}
pub(crate) fn is_duplicate_key(error: &sqlx::Error) -> bool {
error.as_database_error().is_some_and(|database| {
// MySQL's generic database-error API exposes SQLSTATE (23000) as
// `code()`. The driver-specific duplicate-key number is retained in
// the diagnostic message.
database.code().as_deref() == Some("23000") && database.message().contains("1062")
})
}
const USER_BY_USERNAME_QUERY: &str = "SELECT id, iota_id, username, display, status, about, avatar, sub_level, sub_end, public_key, token FROM users WHERE username = ?"; const USER_BY_USERNAME_QUERY: &str = "SELECT id, iota_id, username, display, status, about, avatar, sub_level, sub_end, public_key, token FROM users WHERE username = ?";
const USER_BY_ID_QUERY: &str = "SELECT id, iota_id, username, display, status, about, avatar, sub_level, sub_end, public_key, token FROM users WHERE id = ?"; const USER_BY_ID_QUERY: &str = "SELECT id, iota_id, username, display, status, about, avatar, sub_level, sub_end, public_key, token FROM users WHERE id = ?";
const USERS_BY_IOTA_ID_QUERY: &str = "SELECT id, iota_id, username, display, status, about, avatar, sub_level, sub_end, public_key, token FROM users WHERE iota_id = ?"; const USERS_BY_IOTA_ID_QUERY: &str = "SELECT id, iota_id, username, display, status, about, avatar, sub_level, sub_end, public_key, token FROM users WHERE iota_id = ?";
@ -179,16 +256,99 @@ pub async fn register_complete_user(
public_key: PublicKeyBundle, public_key: PublicKeyBundle,
iota_id: IotaId, iota_id: IotaId,
token: String, token: String,
registration_token: String,
) -> Result<()> { ) -> Result<()> {
sqlx::query( if !valid_protocol_id(id.0) || !valid_protocol_id(iota_id.0) {
return Err(OmegaError::Validation(
"user or Iota ID is outside the 48-bit protocol range".into(),
));
}
if !valid_username(&username) {
return Err(OmegaError::Validation("invalid username".into()));
}
let mut transaction = pool().await?.begin().await?;
let lease = sqlx::query(
"SELECT iota_id, completed_at IS NOT NULL AS completed, \
expires_at >= UTC_TIMESTAMP() AS current FROM registration_leases \
WHERE token = ? AND user_id = ? FOR UPDATE",
)
.bind(&registration_token)
.bind(id.0)
.fetch_optional(&mut *transaction)
.await?
.ok_or_else(|| OmegaError::Validation("unknown registration lease".into()))?;
let lease_iota_id: i64 = lease.get("iota_id");
let completed: i8 = lease.get("completed");
let current: i8 = lease.get("current");
if lease_iota_id != iota_id.0 || (completed == 0 && current == 0) {
return Err(OmegaError::Validation(
"expired or mismatched registration lease".into(),
));
}
let insert_result = sqlx::query(
"INSERT INTO users (id, username, public_key, iota_id, token) VALUES (?, ?, ?, ?, ?)", "INSERT INTO users (id, username, public_key, iota_id, token) VALUES (?, ?, ?, ?, ?)",
) )
.bind(id.0) .bind(id.0)
.bind(username.into_bytes()) .bind(username.as_bytes())
.bind(public_key.as_bytes()) .bind(public_key.as_bytes())
.bind(iota_id.0) .bind(iota_id.0)
.bind(token.into_bytes()) .bind(token.as_bytes())
.execute(&pool().await?) .execute(&mut *transaction)
.await?; .await;
let result: Result<()> = match insert_result {
Ok(_) => Ok(()),
Err(insert_error) => {
let existing = sqlx::query_as::<_, UserRow>(USER_BY_ID_QUERY)
.bind(id.0)
.fetch_optional(&mut *transaction)
.await?;
match existing
.map(User::try_from)
.transpose()
.map_err(OmegaError::from)?
{
Some(existing)
if existing.iota_id == iota_id
&& existing.username == username
&& existing.public_key.as_bytes() == public_key.as_bytes()
&& existing.token == token =>
{
Ok(())
}
_ => Err(insert_error.into()),
}
}
};
result?;
sqlx::query("UPDATE registration_leases SET completed_at = UTC_TIMESTAMP() WHERE token = ?")
.bind(&registration_token)
.execute(&mut *transaction)
.await?;
transaction.commit().await?;
Ok(()) Ok(())
} }
fn valid_username(username: &str) -> bool {
!username.is_empty()
&& username.chars().count() <= 15
&& !username.chars().any(char::is_control)
&& !username.contains(['/', '\\'])
}
#[cfg(test)]
mod tests {
use super::{MAX_PROTOCOL_ID, get_register_id, valid_protocol_id};
#[tokio::test]
async fn generated_registration_ids_fit_the_mtp_wire_range() {
for _ in 0..128 {
assert!(valid_protocol_id(get_register_id().await.unwrap().0));
}
assert!(!valid_protocol_id(0));
assert!(valid_protocol_id(MAX_PROTOCOL_ID));
assert!(!valid_protocol_id(MAX_PROTOCOL_ID + 1));
}
}

View file

@ -15,12 +15,12 @@ use crate::server::{
validation::{parse_positive_id, validate_non_empty}, validation::{parse_positive_id, validate_non_empty},
}; };
use crate::sql::user_online_tracker::{get_all_connections, get_iota_primary_omikron_connection}; use crate::sql::user_online_tracker::{get_all_connections, get_iota_primary_omikron_connection};
use crate::transport::omikron_manager::get_random_omikron; use crate::transport::omikron_manager::{get_connected_omikron, get_random_omikron};
use crate::util::file_util::get_directory; use crate::util::file_util::get_directory;
use base64::Engine as _; use base64::Engine as _;
use bytes::Bytes; use bytes::Bytes;
use http::{Method, StatusCode}; use http::{Method, StatusCode};
use mtp::webserver::{Http3Request, Http3Response, RouteParams}; use mtp::webserver::{HttpRequest, HttpResponse, RouteParams};
use std::collections::BTreeMap; use std::collections::BTreeMap;
fn error_body(error: &OmegaError) -> String { fn error_body(error: &OmegaError) -> String {
@ -75,20 +75,19 @@ async fn route(path_parts: &[&str]) -> Result<(StatusCode, String)> {
} }
["api", "get", "omikron", id] => { ["api", "get", "omikron", id] => {
let id = parse_positive_id(id)?; let id = parse_positive_id(id)?;
let omikron = match get_omikron_by_id(id.into()).await { let omikron_id = if get_connected_omikron(id).is_some() {
Ok(value) => value, id
Err(_) => { } else if let Some(omikron_id) = get_iota_primary_omikron_connection(id) {
let fallback_id = omikron_id
if let Some(fallback_id) = get_iota_primary_omikron_connection(id) { } else {
fallback_id let user = get_by_user_id(UserId::from(id)).await?;
} else { get_iota_primary_omikron_connection(user.iota_id.0).ok_or(OmegaError::NotFound)?
let user = get_by_user_id(UserId::from(id)).await?;
get_iota_primary_omikron_connection(user.iota_id.0)
.ok_or(OmegaError::NotFound)?
};
get_omikron_by_id(fallback_id.into()).await?
}
}; };
// Database rows describe registered Omikrons. The public discovery
// API must expose only routes backed by a currently live transport.
get_connected_omikron(omikron_id).ok_or(OmegaError::NotFound)?;
let omikron = get_omikron_by_id(omikron_id.into()).await?;
Ok(( Ok((
StatusCode::OK, StatusCode::OK,
json(&OmikronResponse { json(&OmikronResponse {
@ -178,7 +177,7 @@ async fn route(path_parts: &[&str]) -> Result<(StatusCode, String)> {
} }
} }
pub async fn handle(request: Http3Request, response: Http3Response) -> Http3Response { pub async fn handle(request: HttpRequest, response: HttpResponse) -> HttpResponse {
let method = request.method; let method = request.method;
let path = request.uri.path().to_string(); let path = request.uri.path().to_string();
if method != Method::OPTIONS && !middleware::allow(request.remote_addr.ip(), &path) { if method != Method::OPTIONS && !middleware::allow(request.remote_addr.ip(), &path) {
@ -238,9 +237,9 @@ pub async fn handle(request: Http3Request, response: Http3Response) -> Http3Resp
} }
pub async fn handle_pattern( pub async fn handle_pattern(
request: Http3Request, request: HttpRequest,
response: Http3Response, response: HttpResponse,
_params: RouteParams, _params: RouteParams,
) -> Http3Response { ) -> HttpResponse {
handle(request, response).await handle(request, response).await
} }

View file

@ -1,13 +1,34 @@
use http::StatusCode; use http::StatusCode;
use mtp::webserver::Http3Response; use mtp::webserver::HttpResponse;
pub fn index_handler(response: Http3Response) -> Http3Response { pub fn index_handler(response: HttpResponse) -> HttpResponse {
let documentation = r#" let documentation = r#"
Omega API Server Omega API Server
Available Routes: Available Routes:
- /api/* : API endpoints for the Omega server. - /api/get/omikron
- /direct/* : Resolution for shortened links. Returns a randomly selected connected Omikron.
- /api/get/omikron/{id}
Returns an Omikron by ID. If {id} is a connected Iota ID or user ID,
returns that account's primary connected Omikron instead.
- /api/get/connections
Returns the current live connection map as
{ "status": "success", "connections": { omikron_id: { iota_id: [user_id] } } }.
- /api/get/iota/{id}
Returns the Iota's public identity data.
- /api/get/user/{id}
Returns user profile and public identity data.
- /api/get/id/{username}
Resolves a username to its user and Iota IDs.
- /api/get/public_key
Returns Omega's public key.
- /api/download/iota_frontend
Downloads the Iota frontend archive.
- /direct/{short_key}
Resolves a shortened link with a temporary redirect.
All IDs must be positive decimal integers. The connections endpoint reports
live in-memory state; it is empty after an Omega restart until Omikrons sync.
All other routes will return this documentation. All other routes will return this documentation.
"#; "#;

View file

@ -3,10 +3,6 @@ use mtp::webserver::WebServerConfig;
pub fn build_web_config() -> Result<WebServerConfig, mtp::webserver::RouterError> { pub fn build_web_config() -> Result<WebServerConfig, mtp::webserver::RouterError> {
WebServerConfig::new() WebServerConfig::new()
.route(
"/",
|_request, response| async move { index_handler(response) },
)?
.route("/api/download/iota_frontend", api::handle)? .route("/api/download/iota_frontend", api::handle)?
.route("/api/get/omikron", api::handle)? .route("/api/get/omikron", api::handle)?
.route("/api/get/connections", api::handle)? .route("/api/get/connections", api::handle)?
@ -15,5 +11,6 @@ pub fn build_web_config() -> Result<WebServerConfig, mtp::webserver::RouterError
.route_pattern("/api/get/iota/{id}", api::handle_pattern)? .route_pattern("/api/get/iota/{id}", api::handle_pattern)?
.route_pattern("/api/get/id/{username}", api::handle_pattern)? .route_pattern("/api/get/id/{username}", api::handle_pattern)?
.route_pattern("/api/get/user/{id}", api::handle_pattern)? .route_pattern("/api/get/user/{id}", api::handle_pattern)?
.route_pattern("/direct/{short}", api::handle_pattern) .route_pattern("/direct/{short}", api::handle_pattern)?
.fallback(|_request, response| async move { index_handler(response) })
} }

View file

@ -19,6 +19,11 @@ static IOTA_OMIKRON_CONNECTIONS: Lazy<DashMap<i64, Vec<i64>>> = Lazy::new(DashMa
// UserID -> UserStatus // UserID -> UserStatus
static USER_STATUS_MAP: Lazy<DashMap<i64, UserConnection>> = Lazy::new(DashMap::new); static USER_STATUS_MAP: Lazy<DashMap<i64, UserConnection>> = Lazy::new(DashMap::new);
// The legacy account map is kept for Iota-only/offline compatibility. Client
// transports are tracked independently: two devices must never overwrite each
// other's route merely because they authenticate as the same account.
static USER_SESSION_STATUS_MAP: Lazy<DashMap<(i64, i64), UserConnection>> = Lazy::new(DashMap::new);
pub fn track_iota_connection(iota_id: i64, omikron_id: i64, primary: bool) { pub fn track_iota_connection(iota_id: i64, omikron_id: i64, primary: bool) {
let mut entry = IOTA_OMIKRON_CONNECTIONS let mut entry = IOTA_OMIKRON_CONNECTIONS
.entry(iota_id) .entry(iota_id)
@ -121,13 +126,63 @@ pub fn track_user_status(user_id: i64, status: UserStatus, omikron_id: i64) {
); );
} }
pub fn track_user_session_status(
user_id: i64,
session_id: i64,
status: UserStatus,
omikron_id: i64,
) {
USER_SESSION_STATUS_MAP.insert(
(user_id, session_id),
UserConnection {
connection_type: status,
omikron_id,
},
);
}
pub fn untrack_user_status(user_id: i64, omikron_id: i64) { pub fn untrack_user_status(user_id: i64, omikron_id: i64) {
USER_STATUS_MAP.remove_if(&user_id, |_, connection| { USER_STATUS_MAP.remove_if(&user_id, |_, connection| {
connection.omikron_id == omikron_id connection.omikron_id == omikron_id
}); });
} }
pub fn untrack_user_session_status(user_id: i64, session_id: i64, omikron_id: i64) {
USER_SESSION_STATUS_MAP.remove_if(&(user_id, session_id), |_, connection| {
connection.omikron_id == omikron_id
});
}
pub fn update_user_session_status(user_id: i64, status: UserStatus) {
for mut entry in USER_SESSION_STATUS_MAP.iter_mut() {
if entry.key().0 == user_id {
entry.connection_type = status.clone();
}
}
// Preserve the account preference for legacy routes as well.
if let Some(mut entry) = USER_STATUS_MAP.get_mut(&user_id) {
entry.connection_type = status;
}
}
pub fn get_user_status(user_id: i64) -> Option<UserConnection> { pub fn get_user_status(user_id: i64) -> Option<UserConnection> {
// A connected visible session is preferred. Invisible sessions remain
// routable but are intentionally presented as offline when they are the
// only active routes.
let sessions: Vec<UserConnection> = USER_SESSION_STATUS_MAP
.iter()
.filter(|entry| entry.key().0 == user_id)
.map(|entry| entry.value().clone())
.collect();
if let Some(status) = sessions
.iter()
.find(|status| status.connection_type != UserStatus::user_invisible)
{
return Some(status.clone());
}
if let Some(status) = sessions.first() {
return Some(status.clone());
}
USER_STATUS_MAP.get(&user_id).map(|v| v.clone()) USER_STATUS_MAP.get(&user_id).map(|v| v.clone())
} }
@ -164,6 +219,7 @@ pub async fn untrack_omikron(omikron_id: i64) {
} }
USER_STATUS_MAP.retain(|_, status| status.omikron_id != omikron_id); USER_STATUS_MAP.retain(|_, status| status.omikron_id != omikron_id);
USER_SESSION_STATUS_MAP.retain(|_, status| status.omikron_id != omikron_id);
for iota_id in offline_iotas { for iota_id in offline_iotas {
if let Ok(users) = user_repo::get_users_by_iota_id(IotaId::from(iota_id)).await { if let Ok(users) = user_repo::get_users_by_iota_id(IotaId::from(iota_id)).await {

View file

@ -21,7 +21,18 @@ pub async fn user_connected(
.and_then(UserStatus::from_str) .and_then(UserStatus::from_str)
.unwrap_or(UserStatus::user_online); .unwrap_or(UserStatus::user_online);
if let Ok(user_id) = i64::try_from(user_id) { if let Ok(user_id) = i64::try_from(user_id) {
user_online_tracker::track_user_status(user_id, status, omikron_id); if let Some(session_id) = value
.get_data(DataType::SessionId)
.as_number()
.and_then(|id| i64::try_from(id).ok())
.filter(|id| *id > 0)
{
user_online_tracker::track_user_session_status(
user_id, session_id, status, omikron_id,
);
} else {
user_online_tracker::track_user_status(user_id, status, omikron_id);
}
} }
} }
Ok(()) Ok(())
@ -34,11 +45,55 @@ pub async fn user_disconnected(
) -> OmikronResult<()> { ) -> OmikronResult<()> {
log_in!(crate::util::logger::PrintType::Omega, "User disconnected"); log_in!(crate::util::logger::PrintType::Omega, "User disconnected");
if let Some(user_id) = value.get_data(DataType::UserId).as_number() { if let Some(user_id) = value.get_data(DataType::UserId).as_number() {
user_online_tracker::untrack_user_status(user_id as i64, omikron_id); if let Some(session_id) = value
.get_data(DataType::SessionId)
.as_number()
.and_then(|id| i64::try_from(id).ok())
.filter(|id| *id > 0)
{
user_online_tracker::untrack_user_session_status(
user_id as i64,
session_id,
omikron_id,
);
} else {
user_online_tracker::untrack_user_status(user_id as i64, omikron_id);
}
} }
Ok(()) Ok(())
} }
pub async fn client_changed(
_: Arc<OmikronConnection>,
value: CommunicationValue,
_: i64,
) -> OmikronResult<()> {
let Some(user_id) = value
.get_data(DataType::UserId)
.as_number()
.and_then(|id| i64::try_from(id).ok())
else {
return Ok(());
};
let Some(status) = value
.get_data(DataType::UserState)
.as_str()
.and_then(UserStatus::from_str)
else {
return Ok(());
};
// Connectivity is derived from routes. Clients may choose only public
// presence preferences, never server/offline states.
if matches!(
status,
UserStatus::user_offline | UserStatus::iota_offline | UserStatus::iota_online
) {
return Ok(());
}
user_online_tracker::update_user_session_status(user_id, status);
Ok(())
}
pub async fn iota_connected( pub async fn iota_connected(
connection: Arc<OmikronConnection>, connection: Arc<OmikronConnection>,
value: CommunicationValue, value: CommunicationValue,

View file

@ -13,13 +13,25 @@ pub async fn get_register(
connection: Arc<OmikronConnection>, connection: Arc<OmikronConnection>,
value: CommunicationValue, value: CommunicationValue,
) -> OmikronResult<()> { ) -> OmikronResult<()> {
let register_id = user_repo::get_register_id().await?; let iota_id = value
.get_data(DataType::IotaId)
.as_number()
.and_then(|id| i64::try_from(id).ok())
.filter(|id| user_repo::valid_protocol_id(*id));
let Some(iota_id) = iota_id else {
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidData)
.await;
};
let (register_id, registration_token) =
user_repo::allocate_registration(IotaId::from(iota_id), value.get_id()).await?;
let response = CommunicationValue::new(CommunicationType::GetRegister) let response = CommunicationValue::new(CommunicationType::GetRegister)
.with_id(value.get_id()) .with_id(value.get_id())
.add_typed_default( .add_typed_default(
DataType::UserId, DataType::UserId,
DataValue::SignedNumber(register_id.0.into()), DataValue::SignedNumber(register_id.0.into()),
); )
.add_typed_default(DataType::RegisterId, DataValue::Str(registration_token));
connection.send(&response).await connection.send(&response).await
} }
@ -27,10 +39,6 @@ pub async fn complete_iota(
connection: Arc<OmikronConnection>, connection: Arc<OmikronConnection>,
value: CommunicationValue, value: CommunicationValue,
) -> OmikronResult<()> { ) -> OmikronResult<()> {
let iota_id = value
.get_data(DataType::IotaId)
.as_number()
.map(|id| id as i64);
let public_key = value let public_key = value
.get_data(DataType::PublicKey) .get_data(DataType::PublicKey)
.as_str() .as_str()
@ -40,57 +48,25 @@ pub async fn complete_iota(
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData)
.await; .await;
}; };
match iota_id { match iota_repo::create_new_iota(public_key).await {
Some(iota_id) => { Ok(id) => {
match iota_repo::register_complete_iota(IotaId::from(iota_id), public_key).await { connection
Ok(()) => { .send(
connection &CommunicationValue::new(CommunicationType::CompleteRegisterIota)
.send( .with_id(value.get_id())
&CommunicationValue::new(CommunicationType::Success) .add_typed_default(DataType::IotaId, DataValue::SignedNumber(id.0.into())),
.with_id(value.get_id()), )
) .await
.await }
} Err(error) => {
Err(error) => { connection
connection .send(
.send( &CommunicationValue::new(CommunicationType::ErrorInternal)
&CommunicationValue::new(CommunicationType::ErrorInternal) .with_id(value.get_id())
.with_id(value.get_id()) .add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())),
.add_typed_default( )
DataType::ErrorType, .await
DataValue::Str(error.to_string()),
),
)
.await
}
}
} }
None => match iota_repo::create_new_iota(public_key).await {
Ok(id) => {
connection
.send(
&CommunicationValue::new(CommunicationType::CompleteRegisterIota)
.with_id(value.get_id())
.add_typed_default(
DataType::IotaId,
DataValue::SignedNumber(id.0.into()),
),
)
.await
}
Err(error) => {
connection
.send(
&CommunicationValue::new(CommunicationType::ErrorInternal)
.with_id(value.get_id())
.add_typed_default(
DataType::ErrorType,
DataValue::Str(error.to_string()),
),
)
.await
}
},
} }
} }
@ -101,7 +77,8 @@ pub async fn complete_user(
let user_id = value let user_id = value
.get_data(DataType::UserId) .get_data(DataType::UserId)
.as_number() .as_number()
.map(|id| id as i64); .and_then(|id| i64::try_from(id).ok())
.filter(|id| user_repo::valid_protocol_id(*id));
let username = value let username = value
.get_data(DataType::Username) .get_data(DataType::Username)
.as_str() .as_str()
@ -114,22 +91,44 @@ pub async fn complete_user(
.get_data(DataType::ResetToken) .get_data(DataType::ResetToken)
.as_str() .as_str()
.map(str::to_owned); .map(str::to_owned);
let Some((user_id, username, public_key, reset_token)) = user_id let registration_token = value
.get_data(DataType::RegisterId)
.as_str()
.filter(|token| uuid::Uuid::parse_str(token).is_ok())
.map(str::to_owned);
let Some((user_id, username, public_key, reset_token, registration_token)) = user_id
.zip(username) .zip(username)
.zip(public_key) .zip(public_key)
.zip(reset_token) .zip(reset_token)
.map(|(((id, name), key), token)| (id, name, key, token)) .zip(registration_token)
.map(|((((id, name), key), token), registration_token)| {
(id, name, key, token, registration_token)
})
else { else {
return connection return connection
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidData) .send_error_response(value.get_id(), CommunicationType::ErrorInvalidData)
.await; .await;
}; };
// Omikron supplies the authenticated Iota ID in the payload. The lease
// check below binds completion to that Iota rather than trusting sender.
let iota_id = value
.get_data(DataType::IotaId)
.as_number()
.and_then(|id| i64::try_from(id).ok())
.filter(|id| user_repo::valid_protocol_id(*id));
let Some(iota_id) = iota_id else {
return connection
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidData)
.await;
};
match user_repo::register_complete_user( match user_repo::register_complete_user(
UserId::from(user_id), UserId::from(user_id),
username, username,
public_key, public_key,
IotaId::from(value.get_sender() as i64), IotaId::from(iota_id),
reset_token, reset_token,
registration_token,
) )
.await .await
{ {

View file

@ -91,10 +91,29 @@ impl OmikronConnection {
.retain(|_, task| task.inserted_at.elapsed() < MAX_WAITING_AGE); .retain(|_, task| task.inserted_at.elapsed() < MAX_WAITING_AGE);
} }
})); }));
while let Ok(value) = receiver.receive().await { loop {
if let Err(error) = self.clone().process_message(value).await { match receiver.receive().await {
log_err!(0, PrintType::Omega, "Error processing message: {}", error); Ok(value) => {
if matches!(error, crate::error::OmegaError::NotConnected) { if let Err(error) = self.clone().process_message(value).await {
log_err!(
self.id as i64,
PrintType::Omega,
"Error processing Omikron message: {}",
error
);
if matches!(error, crate::error::OmegaError::NotConnected) {
break;
}
}
}
Err(error) => {
log_err!(
self.id as i64,
PrintType::Omega,
"Omikron receive loop ended: {}; transport close reason: {:?}",
error,
receiver.close_reason()
);
break; break;
} }
} }
@ -133,6 +152,9 @@ impl OmikronConnection {
Some(CommunicationType::UserDisconnected) => { Some(CommunicationType::UserDisconnected) => {
crate::transport::handlers::presence::user_disconnected(self, value, id).await crate::transport::handlers::presence::user_disconnected(self, value, id).await
} }
Some(CommunicationType::ClientChanged) => {
crate::transport::handlers::presence::client_changed(self, value, id).await
}
Some(CommunicationType::IotaConnected) => { Some(CommunicationType::IotaConnected) => {
crate::transport::handlers::presence::iota_connected(self, value, id).await crate::transport::handlers::presence::iota_connected(self, value, id).await
} }
@ -259,40 +281,38 @@ pub async fn complete_register(_: PublicKeyBundle, _: Option<String>) -> u64 {
pub async fn start(port: u16) -> Result<(), Box<dyn std::error::Error>> { pub async fn start(port: u16) -> Result<(), Box<dyn std::error::Error>> {
let cert_pem = load_file_vec("certs", "cert.pem")?; let cert_pem = load_file_vec("certs", "cert.pem")?;
let key_pem = load_file_vec("certs", "key.pem")?; let key_pem = load_file_vec("certs", "key.pem")?;
let web_config = server::server::build_web_config()?; let web_config = server::server::build_web_config()?
let host_config = HostConfig::new( .serve_tcp_https(true)
IpAddr::from(Ipv4Addr::new(0, 0, 0, 0)), .max_tcp_connections(256);
port, let ip = IpAddr::from(Ipv4Addr::new(0, 0, 0, 0));
cert_pem, let host_config = HostConfig::new(ip, port, cert_pem, key_pem)
key_pem, .with_policy(Policy {
) send_mode: SendMode::SingleStreamPerMessage,
.with_policy(Policy { max_message_size: 1_000_000_000,
send_mode: SendMode::SingleStreamPerMessage, handshake_max_message_size: 1_000_000,
max_message_size: 1_000_000_000, close_frame_len: u32::MAX,
handshake_max_message_size: 1_000_000, application_close_code: 0,
close_frame_len: u32::MAX, open_stream_timeout: Duration::from_millis(5_000),
application_close_code: 0, write_timeout: Duration::from_millis(5_000),
open_stream_timeout: Duration::from_millis(2_000), accept_stream_timeout: Duration::from_millis(10_000),
write_timeout: Duration::from_millis(2_000), read_timeout: Duration::from_millis(30_000),
accept_stream_timeout: Duration::from_millis(10_000), keep_alive_interval: Some(Duration::from_secs(6)),
read_timeout: Duration::from_millis(30_000), max_idle_timeout: Some(Duration::from_secs(30)),
keep_alive_interval: Some(Duration::from_secs(6)), force_close_delay: Duration::from_millis(300),
max_idle_timeout: Some(Duration::from_secs(30)), receiver_queue_capacity: 1000,
force_close_delay: Duration::from_millis(300), max_concurrent_stream_tasks: 64,
receiver_queue_capacity: 1000, persistent_stream_max_retries: 5,
max_concurrent_stream_tasks: 10, persistent_stream_retry_backoff: Duration::from_secs(5),
persistent_stream_max_retries: 5, max_frames_per_stream: None,
persistent_stream_retry_backoff: Duration::from_secs(5), })
max_frames_per_stream: None, .with_authentication(
}) load_keyring(),
.with_authentication( Box::new(|id, description| Box::pin(get_by_omikron_id(id, description))),
load_keyring(), Box::new(|key, description| Box::pin(complete_register(key, description))),
Box::new(|id, description| Box::pin(get_by_omikron_id(id, description))), )
Box::new(|key, description| Box::pin(complete_register(key, description))), .with_authentication_policy(AuthenticationPolicy::ForceAuthentication);
)
.with_authentication_policy(AuthenticationPolicy::ForceAuthentication);
let mut server = MTPWebServer::new(host_config, web_config).await?; let mut server = MTPWebServer::new(host_config, web_config).await?;
log!("OmegaServer listening on port {}", port); log!("OmegaServer listening on {}:{}", ip.to_string(), port);
loop { loop {
let mut conn = match server.accept().await { let mut conn = match server.accept().await {
Ok(Some(conn)) => conn, Ok(Some(conn)) => conn,

View file

@ -26,12 +26,18 @@ pub async fn remove_omikron(omikron_id: i64) {
OMIKRON_CONNECTIONS.remove(&omikron_id); OMIKRON_CONNECTIONS.remove(&omikron_id);
} }
pub fn get_connected_omikron(omikron_id: i64) -> Option<Arc<OmikronConnection>> {
OMIKRON_CONNECTIONS
.get(&omikron_id)
.map(|connection| connection.clone())
}
pub async fn get_random_omikron() -> Result<Arc<OmikronConnection>, ()> { pub async fn get_random_omikron() -> Result<Arc<OmikronConnection>, ()> {
let keys: Vec<_> = OMIKRON_CONNECTIONS.iter().map(|e| *e.key()).collect(); let keys: Vec<_> = OMIKRON_CONNECTIONS.iter().map(|e| *e.key()).collect();
if let Some(key) = keys.into_iter().choose(&mut rand::rng()) { if let Some(key) = keys.into_iter().choose(&mut rand::rng()) {
if let Some(entry) = OMIKRON_CONNECTIONS.get(&key) { if let Some(connection) = get_connected_omikron(key) {
return Ok(entry.clone()); return Ok(connection);
} }
} }

View file

@ -244,8 +244,11 @@ pub fn format_cv(cv: &CommunicationValue) -> String {
parts.push(format!("> {}", receiver)); parts.push(format!("> {}", receiver));
} }
let comm_type = cv.get_type().to_string(); let comm_type = cv
parts.push(format!("{}", comm_type)); .get_comm_type_enum()
.map(|kind| kind.to_string())
.unwrap_or_else(|| cv.get_type().to_string());
parts.push(format!("{} (id={})", comm_type, cv.get_id()));
let data = cv.data(); let data = cv.data();

View file

@ -1,269 +0,0 @@
protocol_version: "1.0"
# Note that markers 0 to 31 are reserved for default use, manually working with them is not recommended
# Fixed CommunicationType markers are:
# Error: 0
# ErrorParsing: 1
# ErrorBadVersion: 2
# Disconnect: 3
# Redirect: 4
# Shutdown: 5
# BadRequest: 6
# Unauthorized: 7
# Forbidden: 8
# NotFound: 9
# TooManyRequests: 10
# InternalServerError: 11
# BadGateway: 12
# ServiceUnavailable: 13
# GatewayTimeout: 14
# Identification: 15
# IdentificationResponse: 16
# Register: 17
# RegisterResponse: 18
# Ping: 19
# Pong: 20
# Fixed Data Type markers are:
# Error: 0
# ErrorParsing: 1
# ErrorMessage: 2
# Version: 3
# Description: 4
# Timestamp: 5
# Id: 6
# ClientNonce: 7
# ServerNonce: 8
# PublicKeys: 9
# Signature: 10
# Connected: 11
type_maps:
"1.0":
CommunicationTypes:
ErrorProtocol: 33
ErrorAnonymous: 34
ErrorInternal: 35
ErrorInvalidData: 36
ErrorInvalidUserId: 37
ErrorInvalidOmikronId: 38
ErrorNotFound: 39
ErrorNotAuthenticated: 40
ErrorNoIota: 41
ErrorInvalidChallenge: 42
ErrorInvalidSecret: 43
ErrorInvalidPrivateKey: 44
ErrorInvalidPublicKey: 45
ErrorNoUserId: 46
ErrorNoCallId: 47
ErrorInvalidCallId: 48
Success: 49
ShortenLink: 50
SettingsSave: 51
SettingsLoad: 52
SettingsList: 53
GlobalSettingsSave: 54
GlobalSettingsLoad: 55
Message: 56
MessageState: 57
MessageSend: 58
MessageLive: 59
MessageOtherIota: 60
MessageChunk: 61
MessageGet: 143
MessagesGet: 62
PushNotification: 63
ReadNotification: 64
GetNotifications: 65
TauriIdentification: 66
ChangeConfirm: 67
ConfirmReceive: 68
ConfirmRead: 69
GetChats: 70
GetStates: 71
AddCommunity: 72
RemoveCommunity: 73
GetCommunities: 74
RegisterIota: 81
RegisterIotaSuccess: 82
AddConversation: 85
SendChat: 86
ClientChanged: 87
ClientConnected: 88
ClientDisconnected: 89
ClientClosed: 90
PublicKey: 91
PrivateKey: 92
WebrtcSdp: 93
WebrtcIce: 94
StartStream: 95
EndStream: 96
WatchStream: 97
CallToken: 98
CallInvite: 99
CallDisconnectUser: 100
CallTimeoutUser: 101
CallSetAnonymousJoining: 102
CallData: 103
EndCall: 104
Function: 105
Update: 106
CreateUser: 107
RhoUpdate: 108
UserConnected: 109
UserDisconnected: 110
IotaConnected: 111
IotaDisconnected: 112
SyncClientIotaStatus: 113
GetUserData: 114
GetIotaData: 115
IotaUserData: 116
ChangeUserData: 117
ChangeIotaData: 118
GetRegister: 119
CompleteRegisterUser: 120
CompleteRegisterIota: 121
DeleteUser: 122
DeleteIota: 123
StartRegister: 124
CompleteRegister: 125
GetApp: 126
CreateApp: 127
DeleteApp: 128
SaveAppData: 129
LoadAppData: 130
AppIdentification: 131
AppChallenge: 132
AppChallengeResponse: 133
AppIdentificationResponse: 134
LoadTxtRecord: 135
ErrorNotSet: 136
SetChatSecret: 139
GetChatSecret: 140
ChatSecretResponse: 141
ChatSecretForward: 142
MessageEditLive: 144
MessageEdit: 145
MessageReactionAdd: 146
MessageReactionRemove: 147
MessageReactionLive: 148
MessageDeleteLive: 150
DataTypes:
ErrorType: 32
ErrorProtocol: 33
AcceptedIds: 34
Uuid: 35
RegisterId: 36
Link: 37
Settings: 38
SettingsName: 39
ChatPartnerId: 40
ChatPartnerName: 41
IotaId: 42
UserId: 43
UserIds: 44
IotaIds: 45
UserState: 46
UserStates: 47
UserPings: 48
CallState: 49
ScreenShare: 50
PrivateKeyHash: 51
# Accepted: 52 now part of default MTP
AcceptedProfiles: 53
DeniedProfiles: 54
Content: 55
Messages: 56
Notifications: 57
SendTime: 58
GetTime: 59
GetVariant: 60
SharedSecretOwn: 61
SharedSecretOther: 62
SharedSecretSign: 63
SharedSecret: 64
CallId: 65
CallToken: 66
CallSecret: 67
Untill: 68
Enabled: 69
StartDate: 70
EndDate: 71
ReceiverId: 72
SenderId: 73
Signed: 75
Message: 76
MessageState: 77
LastPing: 78
PingIota: 79
PingClients: 80
Matches: 81
Omikron: 82
Offset: 83
Amount: 84
Position: 85
Name: 86
Path: 87
Codec: 88
Function: 89
Payload: 90
Result: 91
Interactables: 92
WantToWatch: 93
Watcher: 94
CreatedAt: 95
Username: 96
Display: 97
Avatar: 98
About: 99
Status: 100
PublicKey: 101
SubLevel: 102
SubEnd: 103
CommunityAddress: 104
CommunityTitle: 106
Communities: 107
RhoConnections: 108
User: 109
OnlineStatus: 110
OmikronId: 111
OmikronConnections: 112
ResetToken: 113
NewToken: 114
CallInvited: 115
CallMembers: 116
Calls: 117
Timeout: 118
HasAdmin: 119
LastMessageAt: 120
Height: 121
SentBySelf: 122
SessionId: 123
Contacts: 124
LastMessage: 125
AppIdentifier: 127
AppPrivateKey: 128
AppPublicKey: 129
AppSession: 130
AppData: 131
TauriToken: 132
Challenge: 133
EncryptedPayload: 134
SecurePayload: 135
DeviceId: 136
ClientId: 137
SecretId: 142
VersionNumber: 143
EncryptedSecret: 144
WrappingScheme: 146
UpdatedAt: 147
ChatId: 148
KemCiphertext: 149
SenderUserId: 152
RecipientUserId: 153
Recipients: 154
Edited: 155
Reactions: 156
Reaction: 157
ReplyId: 158