[Fix] Connections

This commit is contained in:
Alex Emmet 2026-08-30 19:18:03 +02:00
commit 8e709513c0
No known key found for this signature in database
19 changed files with 381 additions and 612 deletions

550
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -7,27 +7,28 @@ edition = "2024"
mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [ mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [
"crypto", "crypto",
"files", "files",
"raw",
"web-server", "web-server",
] } ] }
ansi_term = "0.12.1" ansi_term = "0.12.1"
base64 = "0.22.1" base64 = "0.23.1"
bytes = "1" bytes = "1"
dashmap = "6.2.1" dashmap = "6.2.1"
dotenv = "0.15.0" dotenv = "0.15.0"
http = "1" http = "1"
once_cell = "1.21.4" once_cell = "1.21.4"
rand = "0.10.2" rand = "0.10.2"
rustls = { version = "0.23.42", default-features = false, features = [ rustls = { version = "0.23.43", default-features = false, features = [
"std", "std",
"tls12", "tls12",
"aws-lc-rs", "aws-lc-rs",
"prefer-post-quantum", "prefer-post-quantum",
] } ] }
sqlx = { version = "0.8.6", features = ["mysql", "runtime-tokio", "migrate"] } sqlx = { version = "0.9.0", features = ["mysql", "runtime-tokio", "migrate"] }
tokio = { version = "*", features = ["full"] } tokio = { version = "*", features = ["full"] }
tokio-util = { version = "0.7.19", features = ["rt"] } tokio-util = { version = "0.7.19", features = ["rt"] }
uuid = { version = "1.24.0", features = ["v4", "v7"] } uuid = { version = "1.26.0", features = ["v4", "v7"] }
thiserror = "2.0.19" thiserror = "2.0.20"
serde = { version = "1.0.229", features = ["derive"] } serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151" serde_json = "1.0.151"

View file

@ -1,7 +1,7 @@
# Omega # Omega
The Omega is Tensamin's central Server. It maintains the centralized user Registry & manages Omikron useage. The Omega is Tensamin's central Server. It maintains the centralized user Registry & manages Omikron useage.
Omega requires the non-empty `OMEGA_IDENTITY_SECRET` environment variable at startup. On first start it creates the protected private identity at `./omega.mk` and the matching public bundle at `./omega.mpkb`. Existing identity files are loaded fail-closed, so a malformed keyring or mismatched public bundle stops startup. A missing public bundle is rebuilt from a valid private keyring without generating a new identity. On first start Omega creates an unencrypted private keyring at `./omega.mk` and the matching public bundle at `./omega.mpkb`. Existing identity files are loaded fail-closed, so a malformed keyring or mismatched public bundle stops startup. A missing public bundle is rebuilt from a valid private keyring without generating a new identity.
## MTP routing contract ## MTP routing contract

View file

@ -0,0 +1,4 @@
CREATE TABLE iota_snapshot_outbox (
iota_id BIGINT NOT NULL PRIMARY KEY,
updated_at DATETIME NOT NULL
);

View file

@ -22,7 +22,7 @@ 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> {
for _ in 0..16 { for _ in 0..16 {
let id = crate::db::user_repo::get_register_id().await?; let id = crate::db::user_repo::generate_protocol_id();
let iota_id = IotaId::from(id.0); let iota_id = IotaId::from(id.0);
match register_complete_iota(iota_id, public_key.clone()).await { match register_complete_iota(iota_id, public_key.clone()).await {
Ok(()) => return Ok(iota_id), Ok(()) => return Ok(iota_id),
@ -54,14 +54,6 @@ pub async fn register_complete_iota(id: IotaId, public_key: PublicKeyBundle) ->
Ok(()) Ok(())
} }
pub async fn change_iota_key(id: IotaId, key: PublicKeyBundle) -> Result<()> {
sqlx::query("UPDATE iotas SET public_key = ? WHERE id = ?")
.bind(key.try_as_bytes()?)
.bind(id.0)
.execute(&pool().await?)
.await?;
Ok(())
}
pub async fn delete_iota(id: IotaId) -> Result<()> { pub async fn delete_iota(id: IotaId) -> Result<()> {
sqlx::query("DELETE FROM iotas WHERE id = ?") sqlx::query("DELETE FROM iotas WHERE id = ?")
.bind(id.0) .bind(id.0)

View file

@ -11,22 +11,13 @@ use std::collections::HashMap;
pub const MAX_PROTOCOL_ID: i64 = (1_i64 << 48) - 1; pub const MAX_PROTOCOL_ID: i64 = (1_i64 << 48) - 1;
const ID_ALLOCATION_ATTEMPTS: usize = 16; const ID_ALLOCATION_ATTEMPTS: usize = 16;
pub async fn get_register_id() -> Result<UserId> { pub fn generate_protocol_id() -> UserId {
use std::time::{SystemTime, UNIX_EPOCH}; loop {
let value = rand::random::<u64>() & ((1_u64 << 48) - 1);
let timestamp = SystemTime::now() if value != 0 {
.duration_since(UNIX_EPOCH) return UserId::from(value as i64);
.map(|d| d.as_secs()) }
.unwrap_or(0);
let ts = timestamp as i64;
if (1..=MAX_PROTOCOL_ID).contains(&ts) {
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)))
} }
pub fn valid_protocol_id(id: i64) -> bool { pub fn valid_protocol_id(id: i64) -> bool {
@ -42,8 +33,24 @@ pub async fn allocate_registration(iota_id: IotaId, request_id: u32) -> Result<(
)); ));
} }
let database = pool().await?;
sqlx::query(
"UPDATE registration_leases SET request_id = NULL \
WHERE request_id IS NOT NULL AND expires_at < UTC_TIMESTAMP()",
)
.execute(&database)
.await?;
for _ in 0..ID_ALLOCATION_ATTEMPTS { for _ in 0..ID_ALLOCATION_ATTEMPTS {
let id = get_register_id().await?; let id = generate_protocol_id();
let user_exists = sqlx::query("SELECT 1 FROM users WHERE id = ? LIMIT 1")
.bind(id.0)
.fetch_optional(&database)
.await?
.is_some();
if user_exists {
continue;
}
let token = uuid::Uuid::new_v4().to_string(); let token = uuid::Uuid::new_v4().to_string();
let result = sqlx::query( let result = sqlx::query(
"INSERT INTO registration_leases (token, user_id, iota_id, request_id, expires_at) \ "INSERT INTO registration_leases (token, user_id, iota_id, request_id, expires_at) \
@ -53,7 +60,7 @@ pub async fn allocate_registration(iota_id: IotaId, request_id: u32) -> Result<(
.bind(id.0) .bind(id.0)
.bind(iota_id.0) .bind(iota_id.0)
.bind(request_id) .bind(request_id)
.execute(&pool().await?) .execute(&database)
.await; .await;
match result { match result {
Ok(_) => return Ok((id, token)), Ok(_) => return Ok((id, token)),
@ -64,7 +71,7 @@ pub async fn allocate_registration(iota_id: IotaId, request_id: u32) -> Result<(
) )
.bind(iota_id.0) .bind(iota_id.0)
.bind(request_id) .bind(request_id)
.fetch_optional(&pool().await?) .fetch_optional(&database)
.await?; .await?;
if let Some(existing) = existing { if let Some(existing) = existing {
let current: i8 = existing.get("current"); let current: i8 = existing.get("current");
@ -172,7 +179,7 @@ fn normalized_ids(ids: &[i64]) -> Vec<i64> {
ids ids
} }
fn append_in_clause(query: &mut QueryBuilder<'_, MySql>, ids: &[i64]) { fn append_in_clause(query: &mut QueryBuilder<MySql>, ids: &[i64]) {
query.push("("); query.push("(");
for (index, id) in ids.iter().enumerate() { for (index, id) in ids.iter().enumerate() {
if index > 0 { if index > 0 {
@ -183,7 +190,7 @@ fn append_in_clause(query: &mut QueryBuilder<'_, MySql>, ids: &[i64]) {
query.push(")"); query.push(")");
} }
async fn fetch_users(mut query: QueryBuilder<'_, MySql>) -> Result<Vec<User>> { async fn fetch_users(mut query: QueryBuilder<MySql>) -> Result<Vec<User>> {
query query
.build_query_as::<UserRow>() .build_query_as::<UserRow>()
.fetch_all(&pool().await?) .fetch_all(&pool().await?)
@ -336,21 +343,59 @@ pub async fn change_status(id: UserId, value: String) -> Result<()> {
} }
pub async fn change_iota_id(id: UserId, value: Option<IotaId>) -> Result<()> { pub async fn change_iota_id(id: UserId, value: Option<IotaId>) -> Result<()> {
let mut transaction = pool().await?.begin().await?;
let previous = sqlx::query("SELECT iota_id FROM users WHERE id = ? FOR UPDATE")
.bind(id.0)
.fetch_optional(&mut *transaction)
.await?
.ok_or(OmegaError::NotFound)?;
let previous_iota_id: Option<i64> = previous.get("iota_id");
sqlx::query("UPDATE users SET iota_id = ? WHERE id = ?") sqlx::query("UPDATE users SET iota_id = ? WHERE id = ?")
.bind(value.map(|id| id.0)) .bind(value.map(|id| id.0))
.bind(id.0) .bind(id.0)
.execute(&mut *transaction)
.await?;
if let Some(iota_id) = previous_iota_id {
enqueue_iota_snapshot(&mut transaction, iota_id).await?;
}
if let Some(iota_id) = value {
enqueue_iota_snapshot(&mut transaction, iota_id.0).await?;
}
transaction.commit().await?;
Ok(())
}
async fn enqueue_iota_snapshot(
transaction: &mut sqlx::Transaction<'_, MySql>,
iota_id: i64,
) -> Result<()> {
sqlx::query(
"INSERT INTO iota_snapshot_outbox (iota_id, updated_at) VALUES (?, UTC_TIMESTAMP()) \
ON DUPLICATE KEY UPDATE updated_at = VALUES(updated_at)",
)
.bind(iota_id)
.execute(&mut **transaction)
.await?;
Ok(())
}
pub async fn pending_iota_snapshots() -> Result<Vec<IotaId>> {
let rows = sqlx::query("SELECT iota_id FROM iota_snapshot_outbox ORDER BY updated_at")
.fetch_all(&pool().await?)
.await?;
Ok(rows
.into_iter()
.map(|row| IotaId::from(row.get::<i64, _>("iota_id")))
.collect())
}
pub async fn complete_iota_snapshot(iota_id: IotaId) -> Result<()> {
sqlx::query("DELETE FROM iota_snapshot_outbox WHERE iota_id = ?")
.bind(iota_id.0)
.execute(&pool().await?) .execute(&pool().await?)
.await?; .await?;
Ok(()) Ok(())
} }
pub async fn change_token(id: UserId, value: String) -> Result<()> {
update(
id,
"UPDATE users SET token = ? WHERE id = ?",
value.into_bytes(),
)
.await
}
/// Delete the central identity while retaining a durable instruction for the /// Delete the central identity while retaining a durable instruction for the
/// last hosting Iota. The pending row is intentionally independent of users: /// last hosting Iota. The pending row is intentionally independent of users:
/// it must outlive the account row. /// it must outlive the account row.
@ -488,6 +533,7 @@ pub async fn register_complete_user(
} }
}; };
result?; result?;
enqueue_iota_snapshot(&mut transaction, iota_id.0).await?;
sqlx::query("UPDATE registration_leases SET completed_at = UTC_TIMESTAMP() WHERE token = ?") sqlx::query("UPDATE registration_leases SET completed_at = UTC_TIMESTAMP() WHERE token = ?")
.bind(&registration_token) .bind(&registration_token)
.execute(&mut *transaction) .execute(&mut *transaction)
@ -498,19 +544,20 @@ pub async fn register_complete_user(
fn valid_username(username: &str) -> bool { fn valid_username(username: &str) -> bool {
!username.is_empty() !username.is_empty()
&& username.chars().count() <= 15 && username.len() <= 15
&& !username.chars().any(char::is_control) && username
&& !username.contains(['/', '\\']) .bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{MAX_PROTOCOL_ID, get_register_id, valid_protocol_id}; use super::{MAX_PROTOCOL_ID, generate_protocol_id, valid_protocol_id};
#[tokio::test] #[tokio::test]
async fn generated_registration_ids_fit_the_mtp_wire_range() { async fn generated_registration_ids_fit_the_mtp_wire_range() {
for _ in 0..128 { for _ in 0..128 {
assert!(valid_protocol_id(get_register_id().await.unwrap().0)); assert!(valid_protocol_id(generate_protocol_id().0));
} }
assert!(!valid_protocol_id(0)); assert!(!valid_protocol_id(0));
assert!(valid_protocol_id(MAX_PROTOCOL_ID)); assert!(valid_protocol_id(MAX_PROTOCOL_ID));

View file

@ -1,8 +1,7 @@
use crate::error::{IdentityError, Result}; use crate::error::{IdentityError, Result};
use mtp::crypto::{Keyring, PublicKeyBundle}; use mtp::crypto::{Keyring, PublicKeyBundle};
use mtp::files::{ use mtp::files::{
FileError, load_keyring_raw, load_public_key_bundle, save_keyring_raw, FileError, load_keyring_raw, load_public_key_bundle, save_keyring_raw, save_public_key_bundle,
save_public_key_bundle,
}; };
use std::{ use std::{
fs, fs,
@ -70,11 +69,9 @@ impl OmegaIdentity {
fn create_at(keyring_path: &Path, public_key_path: &Path) -> Result<Self> { fn create_at(keyring_path: &Path, public_key_path: &Path) -> Result<Self> {
let keyring = Keyring::generate(); let keyring = Keyring::generate();
save_keyring_raw(&keyring, keyring_path).map_err(|error| { save_keyring_raw(&keyring, keyring_path).map_err(|error| IdentityError::Storage {
IdentityError::Storage { path: keyring_path.to_path_buf(),
path: keyring_path.to_path_buf(), source: error,
source: error,
}
})?; })?;
Self::persist_public_bundle(&keyring.public_key_bundle(), public_key_path)?; Self::persist_public_bundle(&keyring.public_key_bundle(), public_key_path)?;
Ok(Self { Ok(Self {
@ -232,8 +229,7 @@ mod tests {
let directory = test_directory(); let directory = test_directory();
let keyring_path = directory.join("omega.mk"); let keyring_path = directory.join("omega.mk");
let public_key_path = directory.join("omega.mpkb"); let public_key_path = directory.join("omega.mpkb");
OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path) OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path).expect("create identity");
.expect("create identity");
let original_keyring = fs::read(&keyring_path).expect("read keyring"); let original_keyring = fs::read(&keyring_path).expect("read keyring");
fs::remove_file(&public_key_path).expect("remove bundle"); fs::remove_file(&public_key_path).expect("remove bundle");
@ -260,8 +256,7 @@ mod tests {
let directory = test_directory(); let directory = test_directory();
let keyring_path = directory.join("omega.mk"); let keyring_path = directory.join("omega.mk");
let public_key_path = directory.join("omega.mpkb"); let public_key_path = directory.join("omega.mpkb");
OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path) OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path).expect("create identity");
.expect("create identity");
let other_keyring = Keyring::generate(); let other_keyring = Keyring::generate();
save_public_key_bundle(&other_keyring.public_key_bundle(), &public_key_path) save_public_key_bundle(&other_keyring.public_key_bundle(), &public_key_path)
.expect("save mismatched bundle"); .expect("save mismatched bundle");
@ -293,10 +288,7 @@ mod tests {
original_keyring original_keyring
); );
assert_eq!( assert_eq!(
reloaded reloaded.public_key_bundle().try_as_bytes().expect("bundle"),
.public_key_bundle()
.try_as_bytes()
.expect("bundle"),
first.public_key_bundle().try_as_bytes().expect("bundle") first.public_key_bundle().try_as_bytes().expect("bundle")
); );

View file

@ -15,6 +15,7 @@ pub use error::{OmegaError, Result};
use crate::db::initialize; use crate::db::initialize;
use crate::state::OmegaState; use crate::state::OmegaState;
use crate::transport::omikron_connection; use crate::transport::omikron_connection;
use crate::transport::omikron_manager;
use crate::util::file_util::get_directory; use crate::util::file_util::get_directory;
use crate::util::logger::PrintType; use crate::util::logger::PrintType;
use crate::util::logger::startup; use crate::util::logger::startup;
@ -83,6 +84,7 @@ async fn main() {
} }
} }
}); });
let snapshot_outbox_worker = omikron_manager::spawn_iota_snapshot_outbox_worker();
let port: u16 = env::var("PORT") let port: u16 = env::var("PORT")
.ok() .ok()
.and_then(|s| s.parse().ok()) .and_then(|s| s.parse().ok())
@ -100,4 +102,5 @@ async fn main() {
} }
rate_limit_cleanup.abort(); rate_limit_cleanup.abort();
short_link_cleanup.abort(); short_link_cleanup.abort();
snapshot_outbox_worker.abort();
} }

View file

@ -13,6 +13,7 @@ use crate::models::UserId;
use crate::server::{ use crate::server::{
middleware, middleware,
validation::{parse_positive_id, validate_non_empty}, validation::{parse_positive_id, validate_non_empty},
with_cors,
}; };
use crate::transport::omikron_manager::{ use crate::transport::omikron_manager::{
get_all_connections, get_connected_omikron, get_iota_primary_omikron_connection, get_all_connections, get_connected_omikron, get_iota_primary_omikron_connection,
@ -193,39 +194,34 @@ pub async fn handle(
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) {
return response return with_cors(response.status(StatusCode::TOO_MANY_REQUESTS).body(json(
.status(StatusCode::TOO_MANY_REQUESTS) &StatusResponse {
.header("access-control-allow-origin", crate::config::cors_origin())
.body(json(&StatusResponse {
status: "error_rate_limited", status: "error_rate_limited",
})); },
)));
} }
if method == Method::OPTIONS { if method == Method::OPTIONS {
return response return with_cors(response.status(StatusCode::NO_CONTENT));
.status(StatusCode::OK)
.header("access-control-allow-origin", crate::config::cors_origin())
.header("access-control-allow-methods", "GET, POST, OPTIONS")
.header("access-control-allow-headers", "*");
} }
let path_parts: Vec<&str> = path.split('/').filter(|part| !part.is_empty()).collect(); let path_parts: Vec<&str> = path.split('/').filter(|part| !part.is_empty()).collect();
if let ["api", "download", "iota_frontend"] = path_parts.as_slice() { if let ["api", "download", "iota_frontend"] = path_parts.as_slice() {
let file_path = format!("{}/downloads/iota_frontend.zip", get_directory()); let file_path = format!("{}/downloads/iota_frontend.zip", get_directory());
return match std::fs::read(file_path) { return match std::fs::read(file_path) {
Ok(bytes) => response Ok(bytes) => with_cors(
.status(StatusCode::OK) response
.header("access-control-allow-origin", crate::config::cors_origin()) .status(StatusCode::OK)
.header("content-type", "application/zip") .header("content-type", "application/zip")
.header( .header(
"content-disposition", "content-disposition",
"attachment; filename=\"iota_frontend.zip\"", "attachment; filename=\"iota_frontend.zip\"",
) )
.body(Bytes::from(bytes)), .body(Bytes::from(bytes)),
Err(_) => response ),
.status(StatusCode::NOT_FOUND) Err(_) => with_cors(response.status(StatusCode::NOT_FOUND).body(json(
.header("access-control-allow-origin", crate::config::cors_origin()) &StatusResponse {
.body(json(&StatusResponse {
status: "error_not_found", status: "error_not_found",
})), },
))),
}; };
} }
if let ["direct", short @ ..] = path_parts.as_slice() { if let ["direct", short @ ..] = path_parts.as_slice() {
@ -233,19 +229,16 @@ pub async fn handle(
let location = crate::server::short_link::get_short_link(&short) let location = crate::server::short_link::get_short_link(&short)
.await .await
.unwrap_or_else(|_| "https://tensamin.net".to_string()); .unwrap_or_else(|_| "https://tensamin.net".to_string());
return response return with_cors(
.status(StatusCode::TEMPORARY_REDIRECT) response
.header("location", &location); .status(StatusCode::TEMPORARY_REDIRECT)
.header("location", &location),
);
} }
let (status, body) = route(&path_parts, &identity) let (status, body) = route(&path_parts, &identity)
.await .await
.unwrap_or_else(|error| (error.status_code(), error_body(&error))); .unwrap_or_else(|error| (error.status_code(), error_body(&error)));
response with_cors(response.status(status).body(body))
.status(status)
.header("access-control-allow-origin", crate::config::cors_origin())
.header("access-control-allow-headers", "*")
.header("access-control-allow-methods", "GET, POST, OPTIONS")
.body(body)
} }
pub async fn handle_pattern( pub async fn handle_pattern(

View file

@ -1,6 +1,8 @@
use http::StatusCode; use http::StatusCode;
use mtp::webserver::HttpResponse; use mtp::webserver::HttpResponse;
use crate::server::with_cors;
pub fn index_handler(response: HttpResponse) -> HttpResponse { pub fn index_handler(response: HttpResponse) -> HttpResponse {
let documentation = r#" let documentation = r#"
Omega API Server Omega API Server
@ -33,8 +35,10 @@ 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.
"#; "#;
response with_cors(
.status(StatusCode::OK) response
.header("content-type", "text/plain; charset=utf-8") .status(StatusCode::OK)
.body(documentation) .header("content-type", "text/plain; charset=utf-8")
.body(documentation),
)
} }

View file

@ -4,3 +4,29 @@ pub mod middleware;
pub mod short_link; pub mod short_link;
pub mod validation; pub mod validation;
pub mod web; pub mod web;
use mtp::webserver::HttpResponse;
pub(crate) fn with_cors(response: HttpResponse) -> HttpResponse {
response
.header("access-control-allow-origin", crate::config::cors_origin())
.header("access-control-allow-methods", "GET, POST, OPTIONS")
.header("access-control-allow-headers", "*")
}
#[cfg(test)]
mod tests {
use super::with_cors;
use mtp::webserver::HttpResponse;
#[test]
fn web_responses_allow_all_origins() {
let response = with_cors(HttpResponse::default());
assert_eq!(response.headers["access-control-allow-origin"], "*");
assert_eq!(
response.headers["access-control-allow-methods"],
"GET, POST, OPTIONS"
);
assert_eq!(response.headers["access-control-allow-headers"], "*");
}
}

View file

@ -1,5 +1,6 @@
use crate::identity::OmegaIdentity; use crate::identity::OmegaIdentity;
use crate::server::{api, index::index_handler}; use crate::server::{api, index::index_handler};
use http::{Method, StatusCode};
use mtp::webserver::{HttpRequest, HttpResponse, RouteParams, WebServerConfig}; use mtp::webserver::{HttpRequest, HttpResponse, RouteParams, WebServerConfig};
use std::{future::Future, pin::Pin, sync::Arc}; use std::{future::Future, pin::Pin, sync::Arc};
@ -43,5 +44,11 @@ pub fn build_web_config(
)? )?
.route_pattern("/api/get/user/{id}", api_pattern_handler(identity.clone()))? .route_pattern("/api/get/user/{id}", api_pattern_handler(identity.clone()))?
.route_pattern("/direct/{short}", api_pattern_handler(identity))? .route_pattern("/direct/{short}", api_pattern_handler(identity))?
.fallback(|_request, response| async move { index_handler(response) }) .fallback(|request, response| async move {
if request.method == Method::OPTIONS {
crate::server::with_cors(response.status(StatusCode::NO_CONTENT))
} else {
index_handler(response)
}
})
} }

View file

@ -73,7 +73,6 @@ pub(crate) fn validate_dispatch_fields(value: &CommunicationValue) -> OmikronRes
message_type, message_type,
mtp::codec::CommunicationType::GetUserData mtp::codec::CommunicationType::GetUserData
| mtp::codec::CommunicationType::ChangeUserData | mtp::codec::CommunicationType::ChangeUserData
| mtp::codec::CommunicationType::ChangeIotaData
| mtp::codec::CommunicationType::DeleteUser | mtp::codec::CommunicationType::DeleteUser
| mtp::codec::CommunicationType::AttachUserBegin | mtp::codec::CommunicationType::AttachUserBegin
| mtp::codec::CommunicationType::AttachUserComplete | mtp::codec::CommunicationType::AttachUserComplete
@ -147,7 +146,6 @@ mod tests {
for message_type in [ for message_type in [
CommunicationType::GetUserData, CommunicationType::GetUserData,
CommunicationType::ChangeUserData, CommunicationType::ChangeUserData,
CommunicationType::ChangeIotaData,
CommunicationType::DeleteUser, CommunicationType::DeleteUser,
CommunicationType::AttachUserBegin, CommunicationType::AttachUserBegin,
CommunicationType::AttachUserComplete, CommunicationType::AttachUserComplete,

View file

@ -84,7 +84,7 @@ pub async fn release_from_iota(
match user_repo::change_iota_id(user.id, None).await { match user_repo::change_iota_id(user.id, None).await {
Ok(()) => { Ok(()) => {
if let Some(iota) = previous_iota { if let Some(iota) = previous_iota {
crate::transport::omikron_manager::publish_iota_user_snapshot(iota.0).await; let _ = crate::transport::omikron_manager::publish_iota_user_snapshot(iota.0).await;
} }
connection connection
.send( .send(
@ -225,9 +225,9 @@ pub async fn attach_complete(
match user_repo::change_iota_id(user.id, Some(IotaId::from(requester))).await { match user_repo::change_iota_id(user.id, Some(IotaId::from(requester))).await {
Ok(()) => { Ok(()) => {
if let Some(iota) = previous_iota.filter(|id| id.0 != requester) { if let Some(iota) = previous_iota.filter(|id| id.0 != requester) {
crate::transport::omikron_manager::publish_iota_user_snapshot(iota.0).await; let _ = crate::transport::omikron_manager::publish_iota_user_snapshot(iota.0).await;
} }
crate::transport::omikron_manager::publish_iota_user_snapshot(requester).await; let _ = crate::transport::omikron_manager::publish_iota_user_snapshot(requester).await;
connection connection
.send( .send(
&CommunicationValue::new(CommunicationType::Success) &CommunicationValue::new(CommunicationType::Success)
@ -252,7 +252,8 @@ async fn complete_delete(
Ok(iota_id) => { Ok(iota_id) => {
let cleanup_pending = iota_id.is_some(); let cleanup_pending = iota_id.is_some();
if let Some(iota_id) = iota_id { if let Some(iota_id) = iota_id {
crate::transport::omikron_manager::publish_iota_user_snapshot(iota_id.0).await; let _ =
crate::transport::omikron_manager::publish_iota_user_snapshot(iota_id.0).await;
crate::transport::omikron_manager::deliver_pending_erasures(iota_id.0).await; crate::transport::omikron_manager::deliver_pending_erasures(iota_id.0).await;
} }
connection connection

View file

@ -562,6 +562,15 @@ pub async fn sync_status(
&affected_iota_ids.iter().copied().collect::<Vec<_>>(), &affected_iota_ids.iter().copied().collect::<Vec<_>>(),
) )
.await?; .await?;
if sessions.iter().any(|(user_id, _, iota_id)| {
!users
.iter()
.any(|user| user.id.0 == *user_id && user.iota_id == Some(IotaId::from(*iota_id)))
}) {
return connection
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
.await;
}
let user_ids = users.iter().map(|user| user.id.0).collect::<Vec<_>>(); let user_ids = users.iter().map(|user| user.id.0).collect::<Vec<_>>();
apply_preferences( apply_preferences(
&state, &state,

View file

@ -135,6 +135,17 @@ pub async fn complete_user(
.await .await
{ {
Ok(()) => { Ok(()) => {
if let Err(error) =
crate::transport::omikron_manager::publish_iota_user_snapshot(iota_id).await
{
crate::log_in!(
crate::util::logger::PrintType::General,
"Could not publish Iota snapshot after registering user {} on Iota {}: {}",
user_id,
iota_id,
error
);
}
connection connection
.send( .send(
&CommunicationValue::new(CommunicationType::Success) &CommunicationValue::new(CommunicationType::Success)

View file

@ -264,49 +264,3 @@ pub async fn change_user(
) -> OmikronResult<()> { ) -> OmikronResult<()> {
update_user(connection, value).await update_user(connection, value).await
} }
pub async fn change_iota(
connection: Arc<OmikronConnection>,
value: CommunicationValue,
) -> OmikronResult<()> {
let request_id = value.require_id()?;
let reset_data = value.get_data(DataType::ResetToken);
let Some(reset) = reset_data.as_str() else {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
.await;
};
let new_token_data = value.get_data(DataType::NewToken);
let Some(new_token) = new_token_data.as_str() else {
return connection
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
.await;
};
let user_id = UserId::from(value.require_sender_i64()?);
let user = match user_repo::get_by_user_id(user_id).await {
Ok(user) => user,
Err(_) => {
return connection
.send_error_response(request_id, CommunicationType::ErrorNotFound)
.await;
}
};
if user.token != reset {
return connection
.send_error_response(request_id, CommunicationType::ErrorInvalidChallenge)
.await;
}
let result =
match user_repo::change_iota_id(user_id, Some(IotaId::from(value.require_sender_i64()?)))
.await
{
Ok(()) => user_repo::change_token(user_id, new_token.to_owned()).await,
Err(error) => Err(error),
};
let response = match result {
Ok(()) => CommunicationValue::new(CommunicationType::Success),
Err(error) => CommunicationValue::new(CommunicationType::ErrorInternal)
.add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())),
};
connection.send(&response.with_id(request_id)).await
}

View file

@ -449,9 +449,6 @@ impl OmikronConnection {
Some(CommunicationType::ChangeUserData) => { Some(CommunicationType::ChangeUserData) => {
crate::transport::handlers::user_data::change_user(self, value).await crate::transport::handlers::user_data::change_user(self, value).await
} }
Some(CommunicationType::ChangeIotaData) => {
crate::transport::handlers::user_data::change_iota(self, value).await
}
Some(CommunicationType::GetRegister) => { Some(CommunicationType::GetRegister) => {
crate::transport::handlers::register::get_register(self, value).await crate::transport::handlers::register::get_register(self, value).await
} }

View file

@ -7,6 +7,9 @@ use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use rand::prelude::IteratorRandom; use rand::prelude::IteratorRandom;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use tokio::task::JoinHandle;
use tokio::time::interval;
pub static OMIKRON_CONNECTIONS: Lazy<DashMap<i64, Arc<OmikronConnection>>> = pub static OMIKRON_CONNECTIONS: Lazy<DashMap<i64, Arc<OmikronConnection>>> =
Lazy::new(DashMap::new); Lazy::new(DashMap::new);
@ -23,6 +26,7 @@ pub async fn add_omikron(conn: Arc<OmikronConnection>) {
if let Some(old) = OMIKRON_CONNECTIONS.insert(id, conn.clone()) { if let Some(old) = OMIKRON_CONNECTIONS.insert(id, conn.clone()) {
old.close().await; old.close().await;
} }
let _ = flush_iota_snapshot_outbox().await;
} }
pub async fn remove_omikron(omikron_id: i64, connection: &Arc<OmikronConnection>) -> bool { pub async fn remove_omikron(omikron_id: i64, connection: &Arc<OmikronConnection>) -> bool {
@ -63,12 +67,18 @@ pub async fn get_all_connections()
.await .await
.map_err(|_| ())?; .map_err(|_| ())?;
for user in users { for user in users {
for route in state.presence.routes_for_user(user.id.0) { if let Some(iota_id) = user.iota_id {
if let Some(iotas) = result.get_mut(&route.omikron_id) for omikron_id in state
&& let Some(iota_id) = user.iota_id .presence
&& let Some(users) = iotas.get_mut(&iota_id.0) .iota_connections(iota_id.0)
.unwrap_or_default()
{ {
users.push(user.id.0); if let Some(users) = result
.get_mut(&omikron_id)
.and_then(|iotas| iotas.get_mut(&iota_id.0))
{
users.push(user.id.0);
}
} }
} }
} }
@ -115,19 +125,18 @@ pub async fn send_to_user(user_id: i64, cv: &CommunicationValue) {
} }
} }
/// Publish the authoritative membership list after an attach, migration, or /*
/// release. Omikron replaces its full local index from this snapshot. * Publish each Iota membership snapshot to every live relay route. Each
pub async fn publish_iota_user_snapshot(iota_id: i64) { * Omikron keeps a local authorization index, so sending only a primary route
let Some(omikron_id) = get_iota_primary_omikron_connection(iota_id) else { * leaves the remaining relays stale after registration or migration.
return; */
}; pub async fn publish_iota_user_snapshot(iota_id: i64) -> OmikronResult<()> {
let Some(connection) = get_connected_omikron(omikron_id) else { let state = get_state().ok_or(crate::error::OmegaError::NotConnected)?;
return; let omikron_ids = state
}; .presence
let Ok(users) = user_repo::get_users_by_iota_id(crate::models::IotaId::from(iota_id)).await .iota_connections(iota_id)
else { .ok_or(crate::error::OmegaError::NotConnected)?;
return; let users = user_repo::get_users_by_iota_id(crate::models::IotaId::from(iota_id)).await?;
};
let user_ids = users let user_ids = users
.into_iter() .into_iter()
.map(|user| DataValue::SignedNumber(user.id.0.into())) .map(|user| DataValue::SignedNumber(user.id.0.into()))
@ -135,7 +144,54 @@ pub async fn publish_iota_user_snapshot(iota_id: i64) {
let snapshot = CommunicationValue::new(CommunicationType::IotaUserData) let snapshot = CommunicationValue::new(CommunicationType::IotaUserData)
.add_typed_default(DataType::IotaId, DataValue::SignedNumber(iota_id.into())) .add_typed_default(DataType::IotaId, DataValue::SignedNumber(iota_id.into()))
.add_typed_default(DataType::UserIds, DataValue::Array(user_ids)); .add_typed_default(DataType::UserIds, DataValue::Array(user_ids));
let _ = connection.send(&snapshot).await; for omikron_id in omikron_ids {
let connection =
get_connected_omikron(omikron_id).ok_or(crate::error::OmegaError::NotConnected)?;
connection.send(&snapshot).await?;
}
Ok(())
}
pub async fn flush_iota_snapshot_outbox() -> OmikronResult<()> {
for iota_id in user_repo::pending_iota_snapshots().await? {
match publish_iota_user_snapshot(iota_id.0).await {
Ok(()) => {
if let Err(error) = user_repo::complete_iota_snapshot(iota_id).await {
crate::log_in!(
crate::util::logger::PrintType::General,
"Could not complete Iota snapshot outbox entry for {}: {}",
iota_id.0,
error
);
}
}
Err(error) => {
crate::log_in!(
crate::util::logger::PrintType::General,
"Could not publish Iota snapshot for {}: {}",
iota_id.0,
error
);
}
}
}
Ok(())
}
pub fn spawn_iota_snapshot_outbox_worker() -> JoinHandle<()> {
tokio::spawn(async {
let mut retry = interval(Duration::from_secs(30));
loop {
retry.tick().await;
if let Err(error) = flush_iota_snapshot_outbox().await {
crate::log_in!(
crate::util::logger::PrintType::General,
"Could not load Iota snapshot outbox: {}",
error
);
}
}
})
} }
pub async fn deliver_pending_erasures(iota_id: i64) { pub async fn deliver_pending_erasures(iota_id: i64) {