Update Rust crate rustls to v0.23.43 - autoclosed #4
51 changed files with 2327 additions and 5224 deletions
|
|
@ -1,44 +0,0 @@
|
||||||
Dear Snap Data Protection Officer,
|
|
||||||
|
|
||||||
I am writing to exercise my right to erasure under Article 17 of the GDPR in relation to two Snapchat accounts that belong to me:
|
|
||||||
|
|
||||||
alexemmet
|
|
||||||
|
|
||||||
alex_emmet
|
|
||||||
|
|
||||||
I previously contacted Snapchat Support through X, formerly Twitter, because I was unable to identify a functioning general support email address or reach a human support representative through the available support channels.
|
|
||||||
|
|
||||||
In my message through X, I explained that I am requesting deletion of both accounts and the personal data associated with them.
|
|
||||||
|
|
||||||
I still have access to the login credentials for alexemmet and can verify ownership of that account through the normal account-access process.
|
|
||||||
|
|
||||||
For alex_emmet, I no longer have access to the old device, email address, or phone number that may have been associated with the account. The account was created when I was under 18.
|
|
||||||
|
|
||||||
Snapchat Support responded through X that it could not take action where a user does not know, or no longer has access to, the email address or phone number associated with an account.
|
|
||||||
|
|
||||||
I would like to clarify that I am not requesting account recovery, a password reset, or restored access to alex_emmet. I am making a data-subject request for erasure of my personal data.
|
|
||||||
|
|
||||||
I understand that Snap is entitled to verify my identity before acting on this request. Under Article 12(6) GDPR, where there are reasonable doubts concerning the identity of the person making a request, the controller may request additional information necessary to confirm the person's identity.
|
|
||||||
|
|
||||||
I am therefore asking Snap to provide an alternative means of verifying that I am the data subject associated with alex_emmet, given that I no longer have access to the original device, email address, or phone number. I am willing to provide reasonable information necessary for verification through a secure channel.
|
|
||||||
|
|
||||||
My request covers:
|
|
||||||
|
|
||||||
deletion of the Snapchat account alexemmet;
|
|
||||||
|
|
||||||
deletion of the Snapchat account alex_emmet;
|
|
||||||
|
|
||||||
erasure of personal data associated with both accounts;
|
|
||||||
|
|
||||||
erasure of personal data contained in previous support correspondence, support tickets, attachments, account-recovery records, and associated metadata concerning me.
|
|
||||||
|
|
||||||
If Snap considers that any of this data must be retained, please identify the categories of data being retained, the legal basis for retention, and the applicable retention period.
|
|
||||||
|
|
||||||
If Snap considers that it cannot comply with the erasure request for alex_emmet, please provide the specific reason for that decision and explain why no alternative method of verifying my identity is available.
|
|
||||||
|
|
||||||
I would also appreciate confirmation that this request is being handled as a GDPR data-subject request rather than as an account-recovery request.
|
|
||||||
|
|
||||||
I can provide screenshots of my correspondence with Snapchat Support through X if required.
|
|
||||||
|
|
||||||
Kind regards,
|
|
||||||
Alex
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -27,7 +27,6 @@ target
|
||||||
# Added by cargo
|
# Added by cargo
|
||||||
|
|
||||||
/target
|
/target
|
||||||
/certs
|
|
||||||
|
|
||||||
*.mk
|
*.mk
|
||||||
*.mpkb
|
*.mpkb
|
||||||
|
|
|
||||||
1417
Cargo.lock
generated
1417
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
18
Cargo.toml
18
Cargo.toml
|
|
@ -4,31 +4,33 @@ version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [
|
mtp = { git = "https://git.methanium.net/methanium/mtp", features = [
|
||||||
"crypto",
|
"crypto",
|
||||||
"files",
|
"files",
|
||||||
"raw",
|
|
||||||
"web-server",
|
"web-server",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
ansi_term = "0.12.1"
|
ansi_term = "0.12.1"
|
||||||
base64 = "0.23.1"
|
base64 = "0.22.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.43", default-features = false, features = [
|
reqwest = "0.13.4"
|
||||||
|
rustls = { version = "0.23.42", default-features = false, features = [
|
||||||
"std",
|
"std",
|
||||||
"tls12",
|
"tls12",
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"prefer-post-quantum",
|
"prefer-post-quantum",
|
||||||
] }
|
] }
|
||||||
sqlx = { version = "0.9.0", features = ["mysql", "runtime-tokio", "migrate"] }
|
sqlx = { version = "0.8.6", features = ["mysql", "runtime-tokio", "migrate"] }
|
||||||
|
strum = "0.28.0"
|
||||||
|
strum_macros = "0.28.0"
|
||||||
tokio = { version = "*", features = ["full"] }
|
tokio = { version = "*", features = ["full"] }
|
||||||
tokio-util = { version = "0.7.19", features = ["rt"] }
|
uuid = { version = "1.24.0", features = ["v4", "v7"] }
|
||||||
uuid = { version = "1.26.0", features = ["v4", "v7"] }
|
zip = "8.6.0"
|
||||||
thiserror = "2.0.20"
|
thiserror = "2.0.19"
|
||||||
serde = { version = "1.0.229", features = ["derive"] }
|
serde = { version = "1.0.229", features = ["derive"] }
|
||||||
serde_json = "1.0.151"
|
serde_json = "1.0.151"
|
||||||
|
|
|
||||||
13
README.md
13
README.md
|
|
@ -1,15 +1,2 @@
|
||||||
# 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.
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Omega preserves the MTP distinction between an absent routing field and a
|
|
||||||
present zero value on the wire. For Omega request and response operations,
|
|
||||||
both forms are rejected:
|
|
||||||
|
|
||||||
- request IDs are required and nonzero for correlation;
|
|
||||||
- authenticated sender and receiver IDs are required and nonzero;
|
|
||||||
- fields that are optional for a particular notification remain `Option` and
|
|
||||||
are not converted to zero.
|
|
||||||
|
|
|
||||||
29
certs/server_cert.pem
Normal file
29
certs/server_cert.pem
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
52
certs/server_key.pem
Normal file
52
certs/server_key.pem
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
23BF0xaeMGypYNeF29nvifeI0XJF
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
29
certs/transport_cert.pem
Normal file
29
certs/transport_cert.pem
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
52
certs/transport_key.pem
Normal file
52
certs/transport_key.pem
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
tXVzHj9k6OcU/WVyVApnTjE757+2yJ4=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
|
@ -144,7 +144,7 @@
|
||||||
bindAddress = lib.mkOption {
|
bindAddress = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "0.0.0.0";
|
default = "0.0.0.0";
|
||||||
description = "IP address to bind the MTP/QUIC server to.";
|
description = "IP address to bind the HTTP/API server to.";
|
||||||
};
|
};
|
||||||
|
|
||||||
openFirewall = lib.mkOption {
|
openFirewall = lib.mkOption {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ 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,
|
||||||
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
|
||||||
|
|
||||||
UNIQUE KEY uk_users_username (username),
|
UNIQUE KEY uk_users_username (username),
|
||||||
KEY idx_users_iota_id (iota_id),
|
KEY idx_users_iota_id (iota_id),
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
ALTER TABLE users
|
|
||||||
ADD COLUMN presence_preference VARBINARY(32) NOT NULL DEFAULT 'user_online';
|
|
||||||
|
|
||||||
UPDATE users
|
|
||||||
SET presence_preference = 'user_online'
|
|
||||||
WHERE presence_preference NOT IN (
|
|
||||||
'user_online',
|
|
||||||
'user_idle',
|
|
||||||
'user_dnd',
|
|
||||||
'user_wc',
|
|
||||||
'user_invisible'
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE users
|
|
||||||
ADD CONSTRAINT chk_users_presence_preference
|
|
||||||
CHECK (presence_preference IN (
|
|
||||||
'user_online',
|
|
||||||
'user_idle',
|
|
||||||
'user_dnd',
|
|
||||||
'user_wc',
|
|
||||||
'user_invisible'
|
|
||||||
));
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
ALTER TABLE users
|
|
||||||
DROP FOREIGN KEY fk_users_iota;
|
|
||||||
|
|
||||||
ALTER TABLE users
|
|
||||||
MODIFY iota_id BIGINT NULL;
|
|
||||||
|
|
||||||
ALTER TABLE users
|
|
||||||
ADD CONSTRAINT fk_users_iota
|
|
||||||
FOREIGN KEY (iota_id)
|
|
||||||
REFERENCES iotas (id)
|
|
||||||
ON DELETE SET NULL;
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
CREATE TABLE pending_iota_user_erasure (
|
|
||||||
user_id BIGINT NOT NULL,
|
|
||||||
iota_id BIGINT NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (user_id, iota_id),
|
|
||||||
CONSTRAINT fk_pending_iota_user_erasure_iota
|
|
||||||
FOREIGN KEY (iota_id) REFERENCES iotas(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
CREATE TABLE iota_snapshot_outbox (
|
|
||||||
iota_id BIGINT NOT NULL PRIMARY KEY,
|
|
||||||
updated_at DATETIME NOT NULL
|
|
||||||
);
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
ALTER TABLE users
|
|
||||||
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3);
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit f4e45aa3a3ad0e3c3a257f66857b904a1af7901c
|
Subproject commit ece6e2c3b4e925f3cefe46f4a048fbfc8f823093
|
||||||
|
|
@ -28,7 +28,7 @@ pub struct UserResponse {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub public_key: String,
|
pub public_key: String,
|
||||||
pub user_id: i64,
|
pub user_id: i64,
|
||||||
pub iota_id: Option<i64>,
|
pub iota_id: i64,
|
||||||
pub sub_level: i32,
|
pub sub_level: i32,
|
||||||
pub sub_end: i64,
|
pub sub_end: i64,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
|
@ -47,7 +47,7 @@ pub struct UsernameResponse {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub public_key: String,
|
pub public_key: String,
|
||||||
pub user_id: i64,
|
pub user_id: i64,
|
||||||
pub iota_id: Option<i64>,
|
pub iota_id: i64,
|
||||||
pub sub_level: i32,
|
pub sub_level: i32,
|
||||||
pub sub_end: i64,
|
pub sub_end: i64,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use std::{env, time::Duration};
|
use std::{env, time::Duration};
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct RateLimitConfig {
|
pub struct RateLimitConfig {
|
||||||
|
|
@ -10,26 +9,6 @@ pub struct RateLimitConfig {
|
||||||
pub transport_connections_per_ip: usize,
|
pub transport_connections_per_ip: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct DispatchConfig {
|
|
||||||
pub omikron_handler_concurrency: usize,
|
|
||||||
pub global_handler_concurrency: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
pub struct OmegaConfig {
|
|
||||||
pub rate_limits: RateLimitConfig,
|
|
||||||
pub dispatch: DispatchConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum ConfigError {
|
|
||||||
#[error("invalid configuration value for {name}: {value}")]
|
|
||||||
InvalidValue { name: String, value: String },
|
|
||||||
#[error("configuration value for {name} is not valid Unicode")]
|
|
||||||
InvalidUnicode { name: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn cors_origin() -> &'static str {
|
pub const fn cors_origin() -> &'static str {
|
||||||
"*"
|
"*"
|
||||||
}
|
}
|
||||||
|
|
@ -47,75 +26,40 @@ impl Default for RateLimitConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RateLimitConfig {
|
impl RateLimitConfig {
|
||||||
pub fn from_env() -> Result<Self, ConfigError> {
|
pub fn from_env() -> Self {
|
||||||
let defaults = Self::default();
|
let defaults = Self::default();
|
||||||
Ok(Self {
|
Self {
|
||||||
window: env_duration("RATE_LIMIT_WINDOW_SECONDS", defaults.window)?,
|
window: env_duration("RATE_LIMIT_WINDOW_SECONDS", defaults.window),
|
||||||
general_requests: env_usize("RATE_LIMIT_GENERAL_REQUESTS", defaults.general_requests)?,
|
general_requests: env_usize("RATE_LIMIT_GENERAL_REQUESTS", defaults.general_requests),
|
||||||
registration_requests: env_usize(
|
registration_requests: env_usize(
|
||||||
"RATE_LIMIT_REGISTRATION_REQUESTS",
|
"RATE_LIMIT_REGISTRATION_REQUESTS",
|
||||||
defaults.registration_requests,
|
defaults.registration_requests,
|
||||||
)?,
|
),
|
||||||
transport_connections: env_usize(
|
transport_connections: env_usize(
|
||||||
"RATE_LIMIT_TRANSPORT_CONNECTIONS",
|
"RATE_LIMIT_TRANSPORT_CONNECTIONS",
|
||||||
defaults.transport_connections,
|
defaults.transport_connections,
|
||||||
)?,
|
),
|
||||||
transport_connections_per_ip: env_usize(
|
transport_connections_per_ip: env_usize(
|
||||||
"RATE_LIMIT_TRANSPORT_CONNECTIONS_PER_IP",
|
"RATE_LIMIT_TRANSPORT_CONNECTIONS_PER_IP",
|
||||||
defaults.transport_connections_per_ip,
|
defaults.transport_connections_per_ip,
|
||||||
)?,
|
),
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_env_or_default() -> Self {
|
|
||||||
Self::from_env().unwrap_or_default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for DispatchConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
omikron_handler_concurrency: 32,
|
|
||||||
global_handler_concurrency: 256,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OmegaConfig {
|
fn env_usize(name: &str, fallback: usize) -> usize {
|
||||||
pub fn from_env() -> Result<Self, ConfigError> {
|
env::var(name)
|
||||||
let rate_limits = RateLimitConfig::from_env()?;
|
.ok()
|
||||||
let defaults = DispatchConfig::default();
|
.and_then(|value| value.parse::<usize>().ok())
|
||||||
Ok(Self {
|
.filter(|value| *value > 0)
|
||||||
rate_limits,
|
.unwrap_or(fallback)
|
||||||
dispatch: DispatchConfig {
|
|
||||||
omikron_handler_concurrency: env_usize(
|
|
||||||
"OMEGA_OMIKRON_HANDLER_CONCURRENCY",
|
|
||||||
defaults.omikron_handler_concurrency,
|
|
||||||
)?,
|
|
||||||
global_handler_concurrency: env_usize(
|
|
||||||
"OMEGA_GLOBAL_HANDLER_CONCURRENCY",
|
|
||||||
defaults.global_handler_concurrency,
|
|
||||||
)?,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn env_usize(name: &str, fallback: usize) -> Result<usize, ConfigError> {
|
fn env_duration(name: &str, fallback: Duration) -> Duration {
|
||||||
match env::var(name) {
|
env::var(name)
|
||||||
Ok(value) => value
|
.ok()
|
||||||
.parse::<usize>()
|
.and_then(|value| value.parse::<u64>().ok())
|
||||||
.ok()
|
.filter(|value| *value > 0)
|
||||||
.filter(|value| *value > 0)
|
.map(Duration::from_secs)
|
||||||
.ok_or_else(|| ConfigError::InvalidValue {
|
.unwrap_or(fallback)
|
||||||
name: name.into(),
|
|
||||||
value,
|
|
||||||
}),
|
|
||||||
Err(env::VarError::NotPresent) => Ok(fallback),
|
|
||||||
Err(env::VarError::NotUnicode(_)) => Err(ConfigError::InvalidUnicode { name: name.into() }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn env_duration(name: &str, fallback: Duration) -> Result<Duration, ConfigError> {
|
|
||||||
env_usize(name, fallback.as_secs() as usize).map(|value| Duration::from_secs(value as u64))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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::generate_protocol_id();
|
let id = crate::db::user_repo::get_register_id().await?;
|
||||||
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),
|
||||||
|
|
@ -48,12 +48,20 @@ pub async fn register_complete_iota(id: IotaId, public_key: PublicKeyBundle) ->
|
||||||
}
|
}
|
||||||
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.try_as_bytes()?)
|
.bind(public_key.as_bytes())
|
||||||
.execute(&pool().await?)
|
.execute(&pool().await?)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn change_iota_key(id: IotaId, key: PublicKeyBundle) -> Result<()> {
|
||||||
|
sqlx::query("UPDATE iotas SET public_key = ? WHERE id = ?")
|
||||||
|
.bind(key.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)
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,29 @@ use crate::{
|
||||||
db::pool,
|
db::pool,
|
||||||
error::{OmegaError, Result},
|
error::{OmegaError, Result},
|
||||||
models::{IotaId, User, UserId},
|
models::{IotaId, User, UserId},
|
||||||
sql::connection_status::UserStatus,
|
|
||||||
};
|
};
|
||||||
use mtp::crypto::PublicKeyBundle;
|
use mtp::crypto::PublicKeyBundle;
|
||||||
use sqlx::{FromRow, MySql, QueryBuilder, Row};
|
use sqlx::{FromRow, Row};
|
||||||
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 fn generate_protocol_id() -> UserId {
|
pub async fn get_register_id() -> Result<UserId> {
|
||||||
loop {
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
let value = rand::random::<u64>() & ((1_u64 << 48) - 1);
|
|
||||||
if value != 0 {
|
let timestamp = SystemTime::now()
|
||||||
return UserId::from(value as i64);
|
.duration_since(UNIX_EPOCH)
|
||||||
}
|
.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)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn valid_protocol_id(id: i64) -> bool {
|
pub fn valid_protocol_id(id: i64) -> bool {
|
||||||
|
|
@ -33,24 +40,8 @@ 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 = generate_protocol_id();
|
let id = get_register_id().await?;
|
||||||
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) \
|
||||||
|
|
@ -60,7 +51,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(&database)
|
.execute(&pool().await?)
|
||||||
.await;
|
.await;
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => return Ok((id, token)),
|
Ok(_) => return Ok((id, token)),
|
||||||
|
|
@ -71,7 +62,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(&database)
|
.fetch_optional(&pool().await?)
|
||||||
.await?;
|
.await?;
|
||||||
if let Some(existing) = existing {
|
if let Some(existing) = existing {
|
||||||
let current: i8 = existing.get("current");
|
let current: i8 = existing.get("current");
|
||||||
|
|
@ -101,25 +92,23 @@ pub(crate) fn is_duplicate_key(error: &sqlx::Error) -> bool {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const USER_BY_USERNAME_QUERY: &str = "SELECT id, iota_id, username, display, status, presence_preference, about, avatar, sub_level, sub_end, public_key, token, CAST(UNIX_TIMESTAMP(created_at) * 1000 AS SIGNED) AS created_at 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, presence_preference, about, avatar, sub_level, sub_end, public_key, token, CAST(UNIX_TIMESTAMP(created_at) * 1000 AS SIGNED) AS created_at 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 USER_COLUMNS: &str = "SELECT id, iota_id, username, display, status, presence_preference, about, avatar, sub_level, sub_end, public_key, token, CAST(UNIX_TIMESTAMP(created_at) * 1000 AS SIGNED) AS created_at FROM users";
|
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 = ?";
|
||||||
|
|
||||||
#[derive(FromRow)]
|
#[derive(FromRow)]
|
||||||
struct UserRow {
|
struct UserRow {
|
||||||
id: i64,
|
id: i64,
|
||||||
iota_id: Option<i64>,
|
iota_id: i64,
|
||||||
username: Vec<u8>,
|
username: Vec<u8>,
|
||||||
display: Option<Vec<u8>>,
|
display: Option<Vec<u8>>,
|
||||||
status: Option<Vec<u8>>,
|
status: Option<Vec<u8>>,
|
||||||
presence_preference: Vec<u8>,
|
|
||||||
about: Option<Vec<u8>>,
|
about: Option<Vec<u8>>,
|
||||||
avatar: Option<Vec<u8>>,
|
avatar: Option<Vec<u8>>,
|
||||||
sub_level: i32,
|
sub_level: i32,
|
||||||
sub_end: i64,
|
sub_end: i64,
|
||||||
public_key: Vec<u8>,
|
public_key: Vec<u8>,
|
||||||
token: Vec<u8>,
|
token: Vec<u8>,
|
||||||
created_at: i64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<UserRow> for User {
|
impl TryFrom<UserRow> for User {
|
||||||
|
|
@ -132,18 +121,16 @@ impl TryFrom<UserRow> for User {
|
||||||
|value| String::from_utf8(value).map_err(|error| sqlx::Error::Decode(Box::new(error)));
|
|value| String::from_utf8(value).map_err(|error| sqlx::Error::Decode(Box::new(error)));
|
||||||
Ok(User {
|
Ok(User {
|
||||||
id: row.id.into(),
|
id: row.id.into(),
|
||||||
iota_id: row.iota_id.map(IotaId::from),
|
iota_id: row.iota_id.into(),
|
||||||
username: decode(row.username)?,
|
username: decode(row.username)?,
|
||||||
display: row.display.map(decode).transpose()?,
|
display: row.display.map(decode).transpose()?,
|
||||||
status: row.status.map(decode).transpose()?,
|
status: row.status.map(decode).transpose()?,
|
||||||
presence_preference: decode(row.presence_preference)?,
|
|
||||||
about: row.about.map(decode).transpose()?,
|
about: row.about.map(decode).transpose()?,
|
||||||
avatar: row.avatar,
|
avatar: row.avatar,
|
||||||
sub_level: row.sub_level,
|
sub_level: row.sub_level,
|
||||||
sub_end: row.sub_end,
|
sub_end: row.sub_end,
|
||||||
public_key,
|
public_key,
|
||||||
token: decode(row.token)?,
|
token: decode(row.token)?,
|
||||||
created_at: row.created_at,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -167,127 +154,13 @@ pub async fn get_by_user_id(id: UserId) -> Result<User> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_users_by_iota_id(id: IotaId) -> Result<Vec<User>> {
|
pub async fn get_users_by_iota_id(id: IotaId) -> Result<Vec<User>> {
|
||||||
get_users_by_iota_ids(&[id.0]).await
|
let rows = sqlx::query_as::<_, UserRow>(USERS_BY_IOTA_ID_QUERY)
|
||||||
}
|
.bind(id.0)
|
||||||
|
|
||||||
fn normalized_ids(ids: &[i64]) -> Vec<i64> {
|
|
||||||
let mut ids = ids
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.filter(|id| valid_protocol_id(*id))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
ids.sort_unstable();
|
|
||||||
ids.dedup();
|
|
||||||
ids
|
|
||||||
}
|
|
||||||
|
|
||||||
fn append_in_clause(query: &mut QueryBuilder<MySql>, ids: &[i64]) {
|
|
||||||
query.push("(");
|
|
||||||
for (index, id) in ids.iter().enumerate() {
|
|
||||||
if index > 0 {
|
|
||||||
query.push(", ");
|
|
||||||
}
|
|
||||||
query.push_bind(*id);
|
|
||||||
}
|
|
||||||
query.push(")");
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_users(mut query: QueryBuilder<MySql>) -> Result<Vec<User>> {
|
|
||||||
query
|
|
||||||
.build_query_as::<UserRow>()
|
|
||||||
.fetch_all(&pool().await?)
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(|row| row.try_into().map_err(OmegaError::from))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_users_by_ids(ids: &[i64]) -> Result<Vec<User>> {
|
|
||||||
let ids = normalized_ids(ids);
|
|
||||||
if ids.is_empty() {
|
|
||||||
return Ok(Vec::new());
|
|
||||||
}
|
|
||||||
let mut query = QueryBuilder::<MySql>::new(USER_COLUMNS);
|
|
||||||
query.push(" WHERE id IN ");
|
|
||||||
append_in_clause(&mut query, &ids);
|
|
||||||
fetch_users(query).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_users_by_iota_ids(ids: &[i64]) -> Result<Vec<User>> {
|
|
||||||
let ids = normalized_ids(ids);
|
|
||||||
if ids.is_empty() {
|
|
||||||
return Ok(Vec::new());
|
|
||||||
}
|
|
||||||
let mut query = QueryBuilder::<MySql>::new(USER_COLUMNS);
|
|
||||||
query.push(" WHERE iota_id IN ");
|
|
||||||
append_in_clause(&mut query, &ids);
|
|
||||||
fetch_users(query).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_users_by_ids_and_iota_ids(
|
|
||||||
user_ids: &[i64],
|
|
||||||
iota_ids: &[i64],
|
|
||||||
) -> Result<Vec<User>> {
|
|
||||||
let user_ids = normalized_ids(user_ids);
|
|
||||||
let iota_ids = normalized_ids(iota_ids);
|
|
||||||
if user_ids.is_empty() && iota_ids.is_empty() {
|
|
||||||
return Ok(Vec::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut query = QueryBuilder::<MySql>::new(USER_COLUMNS);
|
|
||||||
query.push(" WHERE ");
|
|
||||||
if !user_ids.is_empty() {
|
|
||||||
query.push("id IN ");
|
|
||||||
append_in_clause(&mut query, &user_ids);
|
|
||||||
}
|
|
||||||
if !iota_ids.is_empty() {
|
|
||||||
if !user_ids.is_empty() {
|
|
||||||
query.push(" OR ");
|
|
||||||
}
|
|
||||||
query.push("iota_id IN ");
|
|
||||||
append_in_clause(&mut query, &iota_ids);
|
|
||||||
}
|
|
||||||
fetch_users(query).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(FromRow)]
|
|
||||||
struct PresencePreferenceRow {
|
|
||||||
id: i64,
|
|
||||||
presence_preference: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_presence_preferences(ids: &[i64]) -> Result<HashMap<i64, UserStatus>> {
|
|
||||||
let ids = normalized_ids(ids);
|
|
||||||
if ids.is_empty() {
|
|
||||||
return Ok(HashMap::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut query =
|
|
||||||
QueryBuilder::<MySql>::new("SELECT id, presence_preference FROM users WHERE id IN ");
|
|
||||||
append_in_clause(&mut query, &ids);
|
|
||||||
let rows = query
|
|
||||||
.build_query_as::<PresencePreferenceRow>()
|
|
||||||
.fetch_all(&pool().await?)
|
.fetch_all(&pool().await?)
|
||||||
.await?;
|
.await?;
|
||||||
let mut preferences = HashMap::with_capacity(rows.len());
|
rows.into_iter()
|
||||||
for row in rows {
|
.map(|row| row.try_into().map_err(OmegaError::from))
|
||||||
let status = String::from_utf8(row.presence_preference)
|
.collect()
|
||||||
.ok()
|
|
||||||
.and_then(|value| UserStatus::from_client_preference(&value));
|
|
||||||
let status = match status {
|
|
||||||
Some(status) => status,
|
|
||||||
None => {
|
|
||||||
crate::log_in!(
|
|
||||||
crate::util::logger::PrintType::General,
|
|
||||||
"Invalid persisted presence preference for user {}, using user_online",
|
|
||||||
row.id
|
|
||||||
);
|
|
||||||
UserStatus::user_online
|
|
||||||
}
|
|
||||||
};
|
|
||||||
preferences.insert(row.id, status);
|
|
||||||
}
|
|
||||||
Ok(preferences)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update(
|
async fn update(
|
||||||
|
|
@ -344,117 +217,33 @@ pub async fn change_status(id: UserId, value: String) -> Result<()> {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn change_iota_id(id: UserId, value: Option<IotaId>) -> Result<()> {
|
pub async fn change_iota_id(id: UserId, value: 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.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(())
|
||||||
}
|
}
|
||||||
/// Delete the central identity while retaining a durable instruction for the
|
pub async fn change_token(id: UserId, value: String) -> Result<()> {
|
||||||
/// last hosting Iota. The pending row is intentionally independent of users:
|
update(
|
||||||
/// it must outlive the account row.
|
id,
|
||||||
pub async fn delete_user_with_pending_erasure(id: UserId) -> Result<Option<IotaId>> {
|
"UPDATE users SET token = ? WHERE id = ?",
|
||||||
let mut tx = pool().await?.begin().await?;
|
value.into_bytes(),
|
||||||
let row = sqlx::query("SELECT iota_id FROM users WHERE id = ? FOR UPDATE")
|
)
|
||||||
.bind(id.0)
|
.await
|
||||||
.fetch_optional(&mut *tx)
|
}
|
||||||
.await?
|
pub async fn delete_user(id: UserId) -> Result<()> {
|
||||||
.ok_or(OmegaError::NotFound)?;
|
|
||||||
let iota_id: Option<i64> = row.get("iota_id");
|
|
||||||
if let Some(iota_id) = iota_id {
|
|
||||||
sqlx::query(
|
|
||||||
"INSERT IGNORE INTO pending_iota_user_erasure (user_id, iota_id) VALUES (?, ?)",
|
|
||||||
)
|
|
||||||
.bind(id.0)
|
|
||||||
.bind(iota_id)
|
|
||||||
.execute(&mut *tx)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
sqlx::query("DELETE FROM registration_leases WHERE user_id = ?")
|
|
||||||
.bind(id.0)
|
|
||||||
.execute(&mut *tx)
|
|
||||||
.await?;
|
|
||||||
sqlx::query("DELETE FROM users WHERE id = ?")
|
sqlx::query("DELETE FROM users WHERE id = ?")
|
||||||
.bind(id.0)
|
.bind(id.0)
|
||||||
.execute(&mut *tx)
|
.execute(&pool().await?)
|
||||||
.await?;
|
.await?;
|
||||||
tx.commit().await?;
|
Ok(())
|
||||||
Ok(iota_id.map(IotaId::from))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn pending_erasures_for_iota(iota_id: IotaId) -> Result<Vec<UserId>> {
|
|
||||||
let rows = sqlx::query("SELECT user_id FROM pending_iota_user_erasure WHERE iota_id = ?")
|
|
||||||
.bind(iota_id.0)
|
|
||||||
.fetch_all(&pool().await?)
|
|
||||||
.await?;
|
|
||||||
Ok(rows
|
|
||||||
.into_iter()
|
|
||||||
.map(|row| UserId::from(row.get::<i64, _>("user_id")))
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn acknowledge_pending_erasure(user_id: UserId, iota_id: IotaId) -> Result<bool> {
|
|
||||||
let result =
|
|
||||||
sqlx::query("DELETE FROM pending_iota_user_erasure WHERE user_id = ? AND iota_id = ?")
|
|
||||||
.bind(user_id.0)
|
|
||||||
.bind(iota_id.0)
|
|
||||||
.execute(&pool().await?)
|
|
||||||
.await?;
|
|
||||||
Ok(result.rows_affected() == 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn change_keys(id: UserId, public_key: PublicKeyBundle) -> Result<()> {
|
pub async fn change_keys(id: UserId, public_key: PublicKeyBundle) -> Result<()> {
|
||||||
let public_key = public_key.try_as_bytes()?;
|
|
||||||
sqlx::query("UPDATE users SET public_key = ? WHERE id = ?")
|
sqlx::query("UPDATE users SET public_key = ? WHERE id = ?")
|
||||||
.bind(public_key)
|
.bind(public_key.as_bytes())
|
||||||
.bind(id.0)
|
.bind(id.0)
|
||||||
.execute(&pool().await?)
|
.execute(&pool().await?)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -477,7 +266,6 @@ pub async fn register_complete_user(
|
||||||
if !valid_username(&username) {
|
if !valid_username(&username) {
|
||||||
return Err(OmegaError::Validation("invalid username".into()));
|
return Err(OmegaError::Validation("invalid username".into()));
|
||||||
}
|
}
|
||||||
let public_key_bytes = public_key.try_as_bytes()?;
|
|
||||||
|
|
||||||
let mut transaction = pool().await?.begin().await?;
|
let mut transaction = pool().await?.begin().await?;
|
||||||
let lease = sqlx::query(
|
let lease = sqlx::query(
|
||||||
|
|
@ -504,7 +292,7 @@ pub async fn register_complete_user(
|
||||||
)
|
)
|
||||||
.bind(id.0)
|
.bind(id.0)
|
||||||
.bind(username.as_bytes())
|
.bind(username.as_bytes())
|
||||||
.bind(&public_key_bytes)
|
.bind(public_key.as_bytes())
|
||||||
.bind(iota_id.0)
|
.bind(iota_id.0)
|
||||||
.bind(token.as_bytes())
|
.bind(token.as_bytes())
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
|
|
@ -517,25 +305,24 @@ pub async fn register_complete_user(
|
||||||
.bind(id.0)
|
.bind(id.0)
|
||||||
.fetch_optional(&mut *transaction)
|
.fetch_optional(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
let existing_matches = match existing {
|
match existing
|
||||||
Some(existing) => {
|
.map(User::try_from)
|
||||||
let existing = User::try_from(existing).map_err(OmegaError::from)?;
|
.transpose()
|
||||||
existing.iota_id == Some(iota_id)
|
.map_err(OmegaError::from)?
|
||||||
|
{
|
||||||
|
Some(existing)
|
||||||
|
if existing.iota_id == iota_id
|
||||||
&& existing.username == username
|
&& existing.username == username
|
||||||
&& existing.public_key.try_as_bytes()? == public_key_bytes
|
&& existing.public_key.as_bytes() == public_key.as_bytes()
|
||||||
&& existing.token == token
|
&& existing.token == token =>
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
None => false,
|
_ => Err(insert_error.into()),
|
||||||
};
|
|
||||||
if existing_matches {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(insert_error.into())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
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(®istration_token)
|
.bind(®istration_token)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
|
|
@ -546,20 +333,19 @@ 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.len() <= 15
|
&& username.chars().count() <= 15
|
||||||
&& username
|
&& !username.chars().any(char::is_control)
|
||||||
.bytes()
|
&& !username.contains(['/', '\\'])
|
||||||
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{MAX_PROTOCOL_ID, generate_protocol_id, valid_protocol_id};
|
use super::{MAX_PROTOCOL_ID, get_register_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(generate_protocol_id().0));
|
assert!(valid_protocol_id(get_register_id().await.unwrap().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));
|
||||||
|
|
|
||||||
25
src/error.rs
25
src/error.rs
|
|
@ -1,34 +1,11 @@
|
||||||
use std::path::PathBuf;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum IdentityError {
|
|
||||||
#[error("identity storage error at {path}: {source}")]
|
|
||||||
Storage {
|
|
||||||
path: PathBuf,
|
|
||||||
#[source]
|
|
||||||
source: mtp::files::FileError,
|
|
||||||
},
|
|
||||||
#[error("identity I/O error at {path}: {source}")]
|
|
||||||
Io {
|
|
||||||
path: PathBuf,
|
|
||||||
#[source]
|
|
||||||
source: std::io::Error,
|
|
||||||
},
|
|
||||||
#[error("public key bundle at {path} does not match the private keyring")]
|
|
||||||
PublicBundleMismatch { path: PathBuf },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum OmegaError {
|
pub enum OmegaError {
|
||||||
#[error("database pool is not initialized")]
|
#[error("database pool is not initialized")]
|
||||||
DatabaseNotInitialized,
|
DatabaseNotInitialized,
|
||||||
#[error("database error: {0}")]
|
#[error("database error: {0}")]
|
||||||
Database(sqlx::Error),
|
Database(sqlx::Error),
|
||||||
#[error("cryptographic error: {0}")]
|
|
||||||
Crypto(#[from] mtp::crypto::CryptoError),
|
|
||||||
#[error("identity error: {0}")]
|
|
||||||
Identity(#[from] IdentityError),
|
|
||||||
#[error("invalid input: {0}")]
|
#[error("invalid input: {0}")]
|
||||||
Validation(String),
|
Validation(String),
|
||||||
#[error("resource not found")]
|
#[error("resource not found")]
|
||||||
|
|
@ -68,8 +45,6 @@ impl OmegaError {
|
||||||
Self::NotFound => http::StatusCode::NOT_FOUND,
|
Self::NotFound => http::StatusCode::NOT_FOUND,
|
||||||
Self::DatabaseNotInitialized
|
Self::DatabaseNotInitialized
|
||||||
| Self::Database(_)
|
| Self::Database(_)
|
||||||
| Self::Crypto(_)
|
|
||||||
| Self::Identity(_)
|
|
||||||
| Self::Transport(_)
|
| Self::Transport(_)
|
||||||
| Self::NotConnected
|
| Self::NotConnected
|
||||||
| Self::NotAuthenticated
|
| Self::NotAuthenticated
|
||||||
|
|
|
||||||
297
src/identity.rs
297
src/identity.rs
|
|
@ -1,297 +0,0 @@
|
||||||
use crate::error::{IdentityError, Result};
|
|
||||||
use mtp::crypto::{Keyring, PublicKeyBundle};
|
|
||||||
use mtp::files::{
|
|
||||||
FileError, load_keyring_raw, load_public_key_bundle, save_keyring_raw, save_public_key_bundle,
|
|
||||||
};
|
|
||||||
use std::{
|
|
||||||
fs,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
sync::{
|
|
||||||
Arc,
|
|
||||||
atomic::{AtomicU64, Ordering},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const KEYRING_PATH: &str = "./omega.mk";
|
|
||||||
pub const PUBLIC_KEY_PATH: &str = "./omega.mpkb";
|
|
||||||
|
|
||||||
pub struct OmegaIdentity {
|
|
||||||
keyring: Arc<Keyring>,
|
|
||||||
}
|
|
||||||
|
|
||||||
static PUBLIC_BUNDLE_TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
|
|
||||||
|
|
||||||
impl OmegaIdentity {
|
|
||||||
pub fn load_or_create() -> Result<Self> {
|
|
||||||
Self::load_or_create_at(Path::new(KEYRING_PATH), Path::new(PUBLIC_KEY_PATH))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn load_or_create_at(
|
|
||||||
keyring_path: impl AsRef<Path>,
|
|
||||||
public_key_path: impl AsRef<Path>,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let keyring_path = keyring_path.as_ref();
|
|
||||||
let public_key_path = public_key_path.as_ref();
|
|
||||||
let keyring = match load_keyring_raw(keyring_path) {
|
|
||||||
Ok(keyring) => keyring,
|
|
||||||
Err(FileError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => {
|
|
||||||
return Self::create_at(keyring_path, public_key_path);
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
return Err(IdentityError::Storage {
|
|
||||||
path: keyring_path.to_path_buf(),
|
|
||||||
source: error,
|
|
||||||
}
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let identity = Self {
|
|
||||||
keyring: Arc::new(keyring),
|
|
||||||
};
|
|
||||||
match load_public_key_bundle(public_key_path) {
|
|
||||||
Ok(persisted_bundle) => {
|
|
||||||
identity.verify_public_bundle(public_key_path, &persisted_bundle)?
|
|
||||||
}
|
|
||||||
Err(FileError::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => {
|
|
||||||
Self::persist_public_bundle(&identity.public_key_bundle(), public_key_path)?;
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
return Err(IdentityError::Storage {
|
|
||||||
path: public_key_path.to_path_buf(),
|
|
||||||
source: error,
|
|
||||||
}
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(identity)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_at(keyring_path: &Path, public_key_path: &Path) -> Result<Self> {
|
|
||||||
let keyring = Keyring::generate();
|
|
||||||
save_keyring_raw(&keyring, keyring_path).map_err(|error| IdentityError::Storage {
|
|
||||||
path: keyring_path.to_path_buf(),
|
|
||||||
source: error,
|
|
||||||
})?;
|
|
||||||
Self::persist_public_bundle(&keyring.public_key_bundle(), public_key_path)?;
|
|
||||||
Ok(Self {
|
|
||||||
keyring: Arc::new(keyring),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn persist_public_bundle(bundle: &PublicKeyBundle, path: &Path) -> Result<()> {
|
|
||||||
let temporary_path = temporary_path(path)?;
|
|
||||||
let result = save_public_key_bundle(bundle, &temporary_path).map_err(|source| {
|
|
||||||
IdentityError::Storage {
|
|
||||||
path: path.to_path_buf(),
|
|
||||||
source,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if let Err(error) = result {
|
|
||||||
let _ = fs::remove_file(&temporary_path);
|
|
||||||
return Err(error.into());
|
|
||||||
}
|
|
||||||
if let Err(source) = fs::File::open(&temporary_path).and_then(|file| file.sync_all()) {
|
|
||||||
let _ = fs::remove_file(&temporary_path);
|
|
||||||
return Err(IdentityError::Io {
|
|
||||||
path: path.to_path_buf(),
|
|
||||||
source,
|
|
||||||
}
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
if let Err(source) = fs::rename(&temporary_path, path) {
|
|
||||||
let _ = fs::remove_file(&temporary_path);
|
|
||||||
return Err(IdentityError::Io {
|
|
||||||
path: path.to_path_buf(),
|
|
||||||
source,
|
|
||||||
}
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify_public_bundle(&self, path: &Path, persisted_bundle: &PublicKeyBundle) -> Result<()> {
|
|
||||||
let expected = self.keyring.public_key_bundle().try_as_bytes()?;
|
|
||||||
let actual = persisted_bundle.try_as_bytes()?;
|
|
||||||
if expected != actual {
|
|
||||||
return Err(IdentityError::PublicBundleMismatch {
|
|
||||||
path: path.to_path_buf(),
|
|
||||||
}
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn keyring(&self) -> &Keyring {
|
|
||||||
&self.keyring
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn public_key_bundle(&self) -> PublicKeyBundle {
|
|
||||||
self.keyring().public_key_bundle()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clone_keyring(&self) -> Result<Keyring> {
|
|
||||||
let bytes = self.keyring.try_to_bytes()?;
|
|
||||||
Ok(Keyring::from_bytes(&bytes)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn from_keyring(keyring: Keyring) -> Self {
|
|
||||||
Self {
|
|
||||||
keyring: Arc::new(keyring),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn temporary_path(path: &Path) -> Result<PathBuf> {
|
|
||||||
let parent = path.parent().unwrap_or_else(|| Path::new("."));
|
|
||||||
let file_name = path.file_name().ok_or_else(|| IdentityError::Io {
|
|
||||||
path: path.to_path_buf(),
|
|
||||||
source: std::io::Error::new(
|
|
||||||
std::io::ErrorKind::InvalidInput,
|
|
||||||
"identity path has no file name",
|
|
||||||
),
|
|
||||||
})?;
|
|
||||||
let mut temporary_name = file_name.to_os_string();
|
|
||||||
temporary_name.push(format!(
|
|
||||||
".tmp-{}-{}",
|
|
||||||
std::process::id(),
|
|
||||||
PUBLIC_BUNDLE_TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
|
|
||||||
));
|
|
||||||
Ok(parent.join(temporary_name))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use std::fs;
|
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
|
||||||
|
|
||||||
fn test_directory() -> std::path::PathBuf {
|
|
||||||
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
|
||||||
let id = COUNTER.fetch_add(1, Ordering::Relaxed);
|
|
||||||
let path =
|
|
||||||
std::env::temp_dir().join(format!("omega-identity-test-{}-{id}", std::process::id()));
|
|
||||||
fs::create_dir_all(&path).expect("create test directory");
|
|
||||||
path
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn generated_identity_survives_restart_with_separate_file_formats() {
|
|
||||||
let directory = test_directory();
|
|
||||||
let keyring_path = directory.join("omega.mk");
|
|
||||||
let public_key_path = directory.join("omega.mpkb");
|
|
||||||
let first = OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path)
|
|
||||||
.expect("create identity");
|
|
||||||
let first_bundle = first.public_key_bundle().try_as_bytes().expect("bundle");
|
|
||||||
|
|
||||||
let keyring_bytes = fs::read(&keyring_path).expect("read keyring");
|
|
||||||
let bundle_bytes = fs::read(&public_key_path).expect("read bundle");
|
|
||||||
assert_eq!(&keyring_bytes[..4], b"MTMK");
|
|
||||||
assert_eq!(&bundle_bytes[..4], b"MPKB");
|
|
||||||
|
|
||||||
let restarted = OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path)
|
|
||||||
.expect("reload identity");
|
|
||||||
assert_eq!(
|
|
||||||
restarted
|
|
||||||
.public_key_bundle()
|
|
||||||
.try_as_bytes()
|
|
||||||
.expect("bundle"),
|
|
||||||
first_bundle
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
load_public_key_bundle(&public_key_path)
|
|
||||||
.expect("load public bundle")
|
|
||||||
.try_as_bytes()
|
|
||||||
.expect("bundle"),
|
|
||||||
first_bundle
|
|
||||||
);
|
|
||||||
|
|
||||||
fs::remove_dir_all(directory).expect("remove test directory");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn invalid_existing_keyring_does_not_create_a_new_identity() {
|
|
||||||
let directory = test_directory();
|
|
||||||
let keyring_path = directory.join("omega.mk");
|
|
||||||
let public_key_path = directory.join("omega.mpkb");
|
|
||||||
fs::write(&keyring_path, b"not-a-keyring").expect("write invalid keyring");
|
|
||||||
|
|
||||||
let result = OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path);
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(!public_key_path.exists());
|
|
||||||
|
|
||||||
fs::remove_dir_all(directory).expect("remove test directory");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn missing_public_bundle_is_repaired_without_changing_keyring() {
|
|
||||||
let directory = test_directory();
|
|
||||||
let keyring_path = directory.join("omega.mk");
|
|
||||||
let public_key_path = directory.join("omega.mpkb");
|
|
||||||
OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path).expect("create identity");
|
|
||||||
let original_keyring = fs::read(&keyring_path).expect("read keyring");
|
|
||||||
fs::remove_file(&public_key_path).expect("remove bundle");
|
|
||||||
|
|
||||||
let repaired = OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path)
|
|
||||||
.expect("repair bundle");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
fs::read(&keyring_path).expect("read keyring"),
|
|
||||||
original_keyring
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
repaired.public_key_bundle().try_as_bytes().expect("bundle"),
|
|
||||||
load_public_key_bundle(&public_key_path)
|
|
||||||
.expect("load bundle")
|
|
||||||
.try_as_bytes()
|
|
||||||
.expect("bundle")
|
|
||||||
);
|
|
||||||
|
|
||||||
fs::remove_dir_all(directory).expect("remove test directory");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn mismatched_public_bundle_stops_startup() {
|
|
||||||
let directory = test_directory();
|
|
||||||
let keyring_path = directory.join("omega.mk");
|
|
||||||
let public_key_path = directory.join("omega.mpkb");
|
|
||||||
OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path).expect("create identity");
|
|
||||||
let other_keyring = Keyring::generate();
|
|
||||||
save_public_key_bundle(&other_keyring.public_key_bundle(), &public_key_path)
|
|
||||||
.expect("save mismatched bundle");
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path),
|
|
||||||
Err(crate::OmegaError::Identity(
|
|
||||||
IdentityError::PublicBundleMismatch { .. }
|
|
||||||
))
|
|
||||||
));
|
|
||||||
|
|
||||||
fs::remove_dir_all(directory).expect("remove test directory");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn existing_raw_keyring_is_reloaded_without_a_passphrase() {
|
|
||||||
let directory = test_directory();
|
|
||||||
let keyring_path = directory.join("omega.mk");
|
|
||||||
let public_key_path = directory.join("omega.mpkb");
|
|
||||||
|
|
||||||
let first = OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path)
|
|
||||||
.expect("create identity");
|
|
||||||
let original_keyring = fs::read(&keyring_path).expect("read keyring");
|
|
||||||
|
|
||||||
let reloaded = OmegaIdentity::load_or_create_at(&keyring_path, &public_key_path)
|
|
||||||
.expect("reload identity");
|
|
||||||
assert_eq!(
|
|
||||||
fs::read(&keyring_path).expect("read keyring"),
|
|
||||||
original_keyring
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
reloaded.public_key_bundle().try_as_bytes().expect("bundle"),
|
|
||||||
first.public_key_bundle().try_as_bytes().expect("bundle")
|
|
||||||
);
|
|
||||||
|
|
||||||
fs::remove_dir_all(directory).expect("remove test directory");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
60
src/main.rs
60
src/main.rs
|
|
@ -2,34 +2,58 @@ mod api;
|
||||||
mod config;
|
mod config;
|
||||||
mod db;
|
mod db;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
mod identity;
|
|
||||||
mod models;
|
mod models;
|
||||||
mod server;
|
mod server;
|
||||||
mod sql;
|
mod sql;
|
||||||
mod state;
|
|
||||||
mod transport;
|
mod transport;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
pub use error::{OmegaError, Result};
|
pub use error::{OmegaError, Result};
|
||||||
|
|
||||||
use crate::db::initialize;
|
use crate::db::initialize;
|
||||||
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;
|
||||||
use crate::{config::OmegaConfig, server::middleware};
|
|
||||||
use dotenv::from_path;
|
use dotenv::from_path;
|
||||||
|
use mtp::crypto::Keyring;
|
||||||
|
use mtp::files::{load_keyring_raw, save_keyring_raw, save_public_key_bundle};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use rustls::crypto::aws_lc_rs::default_provider;
|
use rustls::crypto::aws_lc_rs::default_provider;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::time::interval;
|
use tokio::time::interval;
|
||||||
|
|
||||||
|
const KEYRING_PATH: &str = "./omega.mk";
|
||||||
|
|
||||||
|
static KEYRING: Lazy<Keyring> = Lazy::new(|| {
|
||||||
|
load_keyring_raw(KEYRING_PATH).unwrap_or_else(|_| {
|
||||||
|
let kr = Keyring::generate();
|
||||||
|
if let Err(error) = save_keyring_raw(&kr, KEYRING_PATH) {
|
||||||
|
eprintln!("Failed to save generated keyring: {error}");
|
||||||
|
}
|
||||||
|
if let Err(error) = save_public_key_bundle(&kr.public_key_bundle(), KEYRING_PATH) {
|
||||||
|
eprintln!("Failed to save generated public key bundle: {error}");
|
||||||
|
}
|
||||||
|
eprintln!("Generated new keyring at {}", KEYRING_PATH);
|
||||||
|
kr
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
pub fn get_keyring() -> &'static Keyring {
|
||||||
|
&KEYRING
|
||||||
|
}
|
||||||
|
pub fn load_keyring() -> Keyring {
|
||||||
|
Keyring::from_bytes(&KEYRING.to_bytes()).unwrap_or_else(|error| {
|
||||||
|
eprintln!("Failed to clone keyring: {error}");
|
||||||
|
Keyring::generate()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
if default_provider().install_default().is_err() {
|
if let Err(_) = default_provider().install_default() {
|
||||||
println!("Error loading Provider");
|
println!("Error loading Provider");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -38,26 +62,6 @@ async fn main() {
|
||||||
log_in!("Incoming messages");
|
log_in!("Incoming messages");
|
||||||
log_out!("Outgoing messages");
|
log_out!("Outgoing messages");
|
||||||
|
|
||||||
let config = match OmegaConfig::from_env() {
|
|
||||||
Ok(config) => config,
|
|
||||||
Err(error) => {
|
|
||||||
log!("[FATAL] Omega configuration is invalid: {}", error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if middleware::initialize_config(config.rate_limits.clone()).is_err() {
|
|
||||||
log!("[FATAL] Omega rate-limit configuration was initialized more than once");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let identity = match identity::OmegaIdentity::load_or_create() {
|
|
||||||
Ok(identity) => identity,
|
|
||||||
Err(error) => {
|
|
||||||
log!("[FATAL] Omega identity initialization failed: {}", error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let state = OmegaState::new(identity, config);
|
|
||||||
|
|
||||||
log!("Started");
|
log!("Started");
|
||||||
log!(" .env");
|
log!(" .env");
|
||||||
if let Err(e) = initialize().await {
|
if let Err(e) = initialize().await {
|
||||||
|
|
@ -84,14 +88,13 @@ 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())
|
||||||
.unwrap_or(443);
|
.unwrap_or(443);
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
result = omikron_connection::start(port, state) => {
|
result = omikron_connection::start(port) => {
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
log_err!(0, PrintType::General, "Server error: {:?}", e);
|
log_err!(0, PrintType::General, "Server error: {:?}", e);
|
||||||
}
|
}
|
||||||
|
|
@ -102,5 +105,4 @@ async fn main() {
|
||||||
}
|
}
|
||||||
rate_limit_cleanup.abort();
|
rate_limit_cleanup.abort();
|
||||||
short_link_cleanup.abort();
|
short_link_cleanup.abort();
|
||||||
snapshot_outbox_worker.abort();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,7 @@ fn serialize_public_key<S>(
|
||||||
where
|
where
|
||||||
S: serde::Serializer,
|
S: serde::Serializer,
|
||||||
{
|
{
|
||||||
let encoded = key.try_to_base64().map_err(serde::ser::Error::custom)?;
|
serializer.serialize_str(&key.to_base64())
|
||||||
serializer.serialize_str(&encoded)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use ids::{IotaId, OmikronId, UserId};
|
pub use ids::{IotaId, OmikronId, UserId};
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,10 @@ use mtp::crypto::PublicKeyBundle;
|
||||||
#[derive(Clone, Debug, serde::Serialize)]
|
#[derive(Clone, Debug, serde::Serialize)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: UserId,
|
pub id: UserId,
|
||||||
pub iota_id: Option<IotaId>,
|
pub iota_id: IotaId,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub display: Option<String>,
|
pub display: Option<String>,
|
||||||
pub status: Option<String>,
|
pub status: Option<String>,
|
||||||
pub presence_preference: String,
|
|
||||||
pub about: Option<String>,
|
pub about: Option<String>,
|
||||||
pub avatar: Option<Vec<u8>>,
|
pub avatar: Option<Vec<u8>>,
|
||||||
pub sub_level: i32,
|
pub sub_level: i32,
|
||||||
|
|
@ -17,5 +16,4 @@ pub struct User {
|
||||||
pub public_key: PublicKeyBundle,
|
pub public_key: PublicKeyBundle,
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
pub token: String,
|
pub token: String,
|
||||||
pub created_at: i64,
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,24 +8,20 @@ use crate::db::{
|
||||||
user_repo::{get_by_user_id, get_by_username},
|
user_repo::{get_by_user_id, get_by_username},
|
||||||
};
|
};
|
||||||
use crate::error::{OmegaError, Result};
|
use crate::error::{OmegaError, Result};
|
||||||
use crate::identity::OmegaIdentity;
|
use crate::load_keyring;
|
||||||
use crate::models::UserId;
|
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::{
|
|
||||||
get_all_connections, get_connected_omikron, get_iota_primary_omikron_connection,
|
|
||||||
get_random_omikron,
|
|
||||||
};
|
};
|
||||||
|
use crate::sql::user_online_tracker::{get_all_connections, get_iota_primary_omikron_connection};
|
||||||
|
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::{HttpRequest, HttpResponse, RouteParams};
|
use mtp::webserver::{HttpRequest, HttpResponse, RouteParams};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
fn error_body(error: &OmegaError) -> String {
|
fn error_body(error: &OmegaError) -> String {
|
||||||
json(&StatusResponse {
|
json(&StatusResponse {
|
||||||
|
|
@ -37,13 +33,13 @@ fn error_body(error: &OmegaError) -> String {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn user_response(user: crate::models::User) -> Result<UserResponse> {
|
fn user_response(user: crate::models::User) -> UserResponse {
|
||||||
Ok(UserResponse {
|
UserResponse {
|
||||||
status: "success",
|
status: "success",
|
||||||
username: user.username,
|
username: user.username,
|
||||||
public_key: user.public_key.try_to_base64()?,
|
public_key: user.public_key.to_base64(),
|
||||||
user_id: user.id.0,
|
user_id: user.id.0,
|
||||||
iota_id: user.iota_id.map(|id| id.0),
|
iota_id: user.iota_id.0,
|
||||||
sub_level: user.sub_level,
|
sub_level: user.sub_level,
|
||||||
sub_end: user.sub_end,
|
sub_end: user.sub_end,
|
||||||
display: user.display,
|
display: user.display,
|
||||||
|
|
@ -52,10 +48,10 @@ fn user_response(user: crate::models::User) -> Result<UserResponse> {
|
||||||
avatar: user
|
avatar: user
|
||||||
.avatar
|
.avatar
|
||||||
.map(|value| base64::engine::general_purpose::STANDARD.encode(value)),
|
.map(|value| base64::engine::general_purpose::STANDARD.encode(value)),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusCode, String)> {
|
async fn route(path_parts: &[&str]) -> Result<(StatusCode, String)> {
|
||||||
match path_parts {
|
match path_parts {
|
||||||
["api", "get", "omikron"] => {
|
["api", "get", "omikron"] => {
|
||||||
let connection = get_random_omikron()
|
let connection = get_random_omikron()
|
||||||
|
|
@ -71,7 +67,7 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC
|
||||||
json(&OmikronResponse {
|
json(&OmikronResponse {
|
||||||
status: "success",
|
status: "success",
|
||||||
id,
|
id,
|
||||||
public_key: omikron.public_key.try_to_base64()?,
|
public_key: omikron.public_key.to_base64(),
|
||||||
ip_address: omikron.ip_address,
|
ip_address: omikron.ip_address,
|
||||||
port: omikron.port,
|
port: omikron.port,
|
||||||
}),
|
}),
|
||||||
|
|
@ -85,17 +81,7 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC
|
||||||
omikron_id
|
omikron_id
|
||||||
} else {
|
} else {
|
||||||
let user = get_by_user_id(UserId::from(id)).await?;
|
let user = get_by_user_id(UserId::from(id)).await?;
|
||||||
match user.iota_id {
|
get_iota_primary_omikron_connection(user.iota_id.0).ok_or(OmegaError::NotFound)?
|
||||||
Some(iota_id) => get_iota_primary_omikron_connection(iota_id.0),
|
|
||||||
None => {
|
|
||||||
get_random_omikron()
|
|
||||||
.await
|
|
||||||
.map_err(|_| OmegaError::NotFound)?
|
|
||||||
.get_omikron_id()
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ok_or(OmegaError::NotFound)?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Database rows describe registered Omikrons. The public discovery
|
// Database rows describe registered Omikrons. The public discovery
|
||||||
|
|
@ -107,7 +93,7 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC
|
||||||
json(&OmikronResponse {
|
json(&OmikronResponse {
|
||||||
status: "success",
|
status: "success",
|
||||||
id: omikron.id.0,
|
id: omikron.id.0,
|
||||||
public_key: omikron.public_key.try_to_base64()?,
|
public_key: omikron.public_key.to_base64(),
|
||||||
ip_address: omikron.ip_address,
|
ip_address: omikron.ip_address,
|
||||||
port: omikron.port,
|
port: omikron.port,
|
||||||
}),
|
}),
|
||||||
|
|
@ -122,7 +108,12 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC
|
||||||
.map(|(omikron_id, iotas)| {
|
.map(|(omikron_id, iotas)| {
|
||||||
let iotas = iotas
|
let iotas = iotas
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(iota_id, users)| (iota_id.to_string(), users.into_iter().collect()))
|
.map(|(iota_id, users)| {
|
||||||
|
(
|
||||||
|
iota_id.to_string(),
|
||||||
|
users.into_iter().map(i64::from).collect(),
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
(omikron_id.to_string(), iotas)
|
(omikron_id.to_string(), iotas)
|
||||||
})
|
})
|
||||||
|
|
@ -143,7 +134,7 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC
|
||||||
json(&IotaResponse {
|
json(&IotaResponse {
|
||||||
status: "success",
|
status: "success",
|
||||||
iota_id: iota.id.0,
|
iota_id: iota.id.0,
|
||||||
public_key: iota.public_key.try_to_base64()?,
|
public_key: iota.public_key.to_base64(),
|
||||||
}),
|
}),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
@ -155,9 +146,9 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC
|
||||||
json(&UsernameResponse {
|
json(&UsernameResponse {
|
||||||
status: "success",
|
status: "success",
|
||||||
username: user.username,
|
username: user.username,
|
||||||
public_key: user.public_key.try_to_base64()?,
|
public_key: user.public_key.to_base64(),
|
||||||
user_id: user.id.0,
|
user_id: user.id.0,
|
||||||
iota_id: user.iota_id.map(|id| id.0),
|
iota_id: user.iota_id.0,
|
||||||
sub_level: user.sub_level,
|
sub_level: user.sub_level,
|
||||||
sub_end: user.sub_end,
|
sub_end: user.sub_end,
|
||||||
}),
|
}),
|
||||||
|
|
@ -165,7 +156,7 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC
|
||||||
}
|
}
|
||||||
["api", "get", "public_key"] => {
|
["api", "get", "public_key"] => {
|
||||||
let public_key = base64::engine::general_purpose::STANDARD
|
let public_key = base64::engine::general_purpose::STANDARD
|
||||||
.encode(identity.public_key_bundle().try_as_bytes()?);
|
.encode(load_keyring().public_key_bundle().as_bytes());
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
json(&PublicKeyResponse {
|
json(&PublicKeyResponse {
|
||||||
|
|
@ -177,7 +168,7 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC
|
||||||
["api", "get", "user", id] => {
|
["api", "get", "user", id] => {
|
||||||
let id = parse_positive_id(id)?;
|
let id = parse_positive_id(id)?;
|
||||||
let user = get_by_user_id(UserId::from(id)).await?;
|
let user = get_by_user_id(UserId::from(id)).await?;
|
||||||
Ok((StatusCode::OK, json(&user_response(user)?)))
|
Ok((StatusCode::OK, json(&user_response(user))))
|
||||||
}
|
}
|
||||||
_ => Ok((
|
_ => Ok((
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
|
@ -186,42 +177,43 @@ async fn route(path_parts: &[&str], identity: &OmegaIdentity) -> Result<(StatusC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle(
|
pub async fn handle(request: HttpRequest, response: HttpResponse) -> HttpResponse {
|
||||||
request: HttpRequest,
|
|
||||||
response: HttpResponse,
|
|
||||||
identity: Arc<OmegaIdentity>,
|
|
||||||
) -> 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) {
|
||||||
return with_cors(response.status(StatusCode::TOO_MANY_REQUESTS).body(json(
|
return response
|
||||||
&StatusResponse {
|
.status(StatusCode::TOO_MANY_REQUESTS)
|
||||||
|
.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 with_cors(response.status(StatusCode::NO_CONTENT));
|
return response
|
||||||
|
.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) => with_cors(
|
Ok(bytes) => response
|
||||||
response
|
.status(StatusCode::OK)
|
||||||
.status(StatusCode::OK)
|
.header("access-control-allow-origin", &crate::config::cors_origin())
|
||||||
.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
|
||||||
Err(_) => with_cors(response.status(StatusCode::NOT_FOUND).body(json(
|
.status(StatusCode::NOT_FOUND)
|
||||||
&StatusResponse {
|
.header("access-control-allow-origin", &crate::config::cors_origin())
|
||||||
|
.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() {
|
||||||
|
|
@ -229,23 +221,25 @@ 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 with_cors(
|
return response
|
||||||
response
|
.status(StatusCode::TEMPORARY_REDIRECT)
|
||||||
.status(StatusCode::TEMPORARY_REDIRECT)
|
.header("location", &location);
|
||||||
.header("location", &location),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
let (status, body) = route(&path_parts, &identity)
|
let (status, body) = route(&path_parts)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|error| (error.status_code(), error_body(&error)));
|
.unwrap_or_else(|error| (error.status_code(), error_body(&error)));
|
||||||
with_cors(response.status(status).body(body))
|
response
|
||||||
|
.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(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
response: HttpResponse,
|
response: HttpResponse,
|
||||||
_params: RouteParams,
|
_params: RouteParams,
|
||||||
identity: Arc<OmegaIdentity>,
|
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
handle(request, response, identity).await
|
handle(request, response).await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
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
|
||||||
|
|
@ -35,10 +33,8 @@ 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.
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
with_cors(
|
response
|
||||||
response
|
.status(StatusCode::OK)
|
||||||
.status(StatusCode::OK)
|
.header("content-type", "text/plain; charset=utf-8")
|
||||||
.header("content-type", "text/plain; charset=utf-8")
|
.body(documentation)
|
||||||
.body(documentation),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,14 @@
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::{collections::VecDeque, sync::OnceLock, time::Instant};
|
use std::{collections::VecDeque, time::Instant};
|
||||||
use tokio::time::interval;
|
use tokio::time::interval;
|
||||||
|
|
||||||
static REQUESTS: Lazy<DashMap<(IpAddr, String), VecDeque<Instant>>> = Lazy::new(DashMap::new);
|
static REQUESTS: Lazy<DashMap<(IpAddr, String), VecDeque<Instant>>> = Lazy::new(DashMap::new);
|
||||||
static CONFIG: OnceLock<crate::config::RateLimitConfig> = OnceLock::new();
|
static CONFIG: Lazy<crate::config::RateLimitConfig> =
|
||||||
|
Lazy::new(crate::config::RateLimitConfig::from_env);
|
||||||
const MAX_TRACKED_CLIENT_BUCKETS: usize = 100_000;
|
const MAX_TRACKED_CLIENT_BUCKETS: usize = 100_000;
|
||||||
|
|
||||||
pub(crate) fn initialize_config(
|
|
||||||
config: crate::config::RateLimitConfig,
|
|
||||||
) -> Result<(), crate::config::RateLimitConfig> {
|
|
||||||
CONFIG.set(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config() -> &'static crate::config::RateLimitConfig {
|
|
||||||
static FALLBACK: Lazy<crate::config::RateLimitConfig> =
|
|
||||||
Lazy::new(crate::config::RateLimitConfig::from_env_or_default);
|
|
||||||
CONFIG.get().unwrap_or(&FALLBACK)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn allow(remote_addr: IpAddr, path: &str) -> bool {
|
pub fn allow(remote_addr: IpAddr, path: &str) -> bool {
|
||||||
let key = if path.contains("register") {
|
let key = if path.contains("register") {
|
||||||
"registration"
|
"registration"
|
||||||
|
|
@ -34,15 +23,15 @@ pub fn allow(remote_addr: IpAddr, path: &str) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let limit = if key == "registration" {
|
let limit = if key == "registration" {
|
||||||
config().registration_requests
|
CONFIG.registration_requests
|
||||||
} else {
|
} else {
|
||||||
config().general_requests
|
CONFIG.general_requests
|
||||||
};
|
};
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let mut entries = REQUESTS.entry(map_key).or_default();
|
let mut entries = REQUESTS.entry(map_key).or_default();
|
||||||
while entries
|
while entries
|
||||||
.front()
|
.front()
|
||||||
.is_some_and(|time| now.duration_since(*time) >= config().window)
|
.is_some_and(|time| now.duration_since(*time) >= CONFIG.window)
|
||||||
{
|
{
|
||||||
entries.pop_front();
|
entries.pop_front();
|
||||||
}
|
}
|
||||||
|
|
@ -55,7 +44,7 @@ pub fn allow(remote_addr: IpAddr, path: &str) -> bool {
|
||||||
|
|
||||||
pub fn spawn_cleanup_task() -> tokio::task::JoinHandle<()> {
|
pub fn spawn_cleanup_task() -> tokio::task::JoinHandle<()> {
|
||||||
tokio::spawn(async {
|
tokio::spawn(async {
|
||||||
let mut ticker = interval(config().window);
|
let mut ticker = interval(CONFIG.window);
|
||||||
loop {
|
loop {
|
||||||
ticker.tick().await;
|
ticker.tick().await;
|
||||||
cleanup_expired();
|
cleanup_expired();
|
||||||
|
|
@ -68,7 +57,7 @@ fn cleanup_expired() {
|
||||||
REQUESTS.retain(|_, entries| {
|
REQUESTS.retain(|_, entries| {
|
||||||
while entries
|
while entries
|
||||||
.front()
|
.front()
|
||||||
.is_some_and(|time| now.duration_since(*time) >= config().window)
|
.is_some_and(|time| now.duration_since(*time) >= CONFIG.window)
|
||||||
{
|
{
|
||||||
entries.pop_front();
|
entries.pop_front();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,6 @@
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod index;
|
pub mod index;
|
||||||
pub mod middleware;
|
pub mod middleware;
|
||||||
|
pub mod server;
|
||||||
pub mod short_link;
|
pub mod short_link;
|
||||||
pub mod validation;
|
pub mod validation;
|
||||||
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"], "*");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
16
src/server/server.rs
Normal file
16
src/server/server.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
use crate::server::{api, index::index_handler};
|
||||||
|
use mtp::webserver::WebServerConfig;
|
||||||
|
|
||||||
|
pub fn build_web_config() -> Result<WebServerConfig, mtp::webserver::RouterError> {
|
||||||
|
WebServerConfig::new()
|
||||||
|
.route("/api/download/iota_frontend", api::handle)?
|
||||||
|
.route("/api/get/omikron", api::handle)?
|
||||||
|
.route("/api/get/connections", api::handle)?
|
||||||
|
.route("/api/get/public_key", api::handle)?
|
||||||
|
.route_pattern("/api/get/omikron/{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/user/{id}", api::handle_pattern)?
|
||||||
|
.route_pattern("/direct/{short}", api::handle_pattern)?
|
||||||
|
.fallback(|_request, response| async move { index_handler(response) })
|
||||||
|
}
|
||||||
|
|
@ -34,7 +34,7 @@ pub async fn get_short_link(short: &str) -> Result<String, ()> {
|
||||||
short
|
short
|
||||||
};
|
};
|
||||||
let frag = short.replace(key, "");
|
let frag = short.replace(key, "");
|
||||||
let normalized = normalize_short(key);
|
let normalized = normalize_short(&key);
|
||||||
|
|
||||||
let target = short_link_repo::get(&normalized)
|
let target = short_link_repo::get(&normalized)
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
use crate::identity::OmegaIdentity;
|
|
||||||
use crate::server::{api, index::index_handler};
|
|
||||||
use http::{Method, StatusCode};
|
|
||||||
use mtp::webserver::{HttpRequest, HttpResponse, RouteParams, WebServerConfig};
|
|
||||||
use std::{future::Future, pin::Pin, sync::Arc};
|
|
||||||
|
|
||||||
type RouteFuture = Pin<Box<dyn Future<Output = HttpResponse> + Send>>;
|
|
||||||
|
|
||||||
fn api_handler(
|
|
||||||
identity: Arc<OmegaIdentity>,
|
|
||||||
) -> impl Fn(HttpRequest, HttpResponse) -> RouteFuture + Send + Sync + 'static {
|
|
||||||
move |request, response| Box::pin(api::handle(request, response, identity.clone()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn api_pattern_handler(
|
|
||||||
identity: Arc<OmegaIdentity>,
|
|
||||||
) -> impl Fn(HttpRequest, HttpResponse, RouteParams) -> RouteFuture + Send + Sync + 'static {
|
|
||||||
move |request, response, params| {
|
|
||||||
Box::pin(api::handle_pattern(
|
|
||||||
request,
|
|
||||||
response,
|
|
||||||
params,
|
|
||||||
identity.clone(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_web_config(
|
|
||||||
identity: Arc<OmegaIdentity>,
|
|
||||||
) -> Result<WebServerConfig, mtp::webserver::RouterError> {
|
|
||||||
WebServerConfig::new()
|
|
||||||
.route("/api/download/iota_frontend", api_handler(identity.clone()))?
|
|
||||||
.route("/api/get/omikron", api_handler(identity.clone()))?
|
|
||||||
.route("/api/get/connections", api_handler(identity.clone()))?
|
|
||||||
.route("/api/get/public_key", api_handler(identity.clone()))?
|
|
||||||
.route_pattern(
|
|
||||||
"/api/get/omikron/{id}",
|
|
||||||
api_pattern_handler(identity.clone()),
|
|
||||||
)?
|
|
||||||
.route_pattern("/api/get/iota/{id}", api_pattern_handler(identity.clone()))?
|
|
||||||
.route_pattern(
|
|
||||||
"/api/get/id/{username}",
|
|
||||||
api_pattern_handler(identity.clone()),
|
|
||||||
)?
|
|
||||||
.route_pattern("/api/get/user/{id}", api_pattern_handler(identity.clone()))?
|
|
||||||
.route_pattern("/direct/{short}", api_pattern_handler(identity))?
|
|
||||||
.fallback(|request, response| async move {
|
|
||||||
if request.method == Method::OPTIONS {
|
|
||||||
crate::server::with_cors(response.status(StatusCode::NO_CONTENT))
|
|
||||||
} else {
|
|
||||||
index_handler(response)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
use strum::IntoEnumIterator;
|
||||||
|
use strum_macros::EnumIter;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, EnumIter, Eq)]
|
||||||
#[allow(unused, non_camel_case_types)]
|
#[allow(unused, non_camel_case_types)]
|
||||||
pub enum UserStatus {
|
pub enum UserStatus {
|
||||||
user_offline,
|
user_offline,
|
||||||
|
|
@ -12,78 +15,17 @@ pub enum UserStatus {
|
||||||
iota_online,
|
iota_online,
|
||||||
iota_borked,
|
iota_borked,
|
||||||
}
|
}
|
||||||
|
#[allow(unused)]
|
||||||
impl UserStatus {
|
impl UserStatus {
|
||||||
pub fn from_client_preference(s: &str) -> Option<Self> {
|
pub fn to_string(&self) -> String {
|
||||||
match s {
|
format!("{:?}", self)
|
||||||
"user_online" => Some(Self::user_online),
|
|
||||||
"user_idle" => Some(Self::user_idle),
|
|
||||||
"user_dnd" => Some(Self::user_dnd),
|
|
||||||
"user_wc" => Some(Self::user_wc),
|
|
||||||
"user_invisible" => Some(Self::user_invisible),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
pub fn from_str(s: &str) -> Option<UserStatus> {
|
||||||
pub fn public_value(&self) -> Self {
|
for sel in UserStatus::iter() {
|
||||||
match self {
|
if &sel.to_string() == s {
|
||||||
Self::user_invisible => Self::user_offline,
|
return Some(sel);
|
||||||
value => value.clone(),
|
}
|
||||||
}
|
}
|
||||||
}
|
None
|
||||||
|
|
||||||
/// Parse a value received from a client or persisted as an account
|
|
||||||
/// preference. Derived connectivity and diagnostic states are never valid
|
|
||||||
/// preferences.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn from_str(s: &str) -> Option<Self> {
|
|
||||||
Self::from_client_preference(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for UserStatus {
|
|
||||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(formatter, "{self:?}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::UserStatus;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn accepts_only_client_preferences() {
|
|
||||||
for value in [
|
|
||||||
"user_online",
|
|
||||||
"user_idle",
|
|
||||||
"user_dnd",
|
|
||||||
"user_wc",
|
|
||||||
"user_invisible",
|
|
||||||
] {
|
|
||||||
assert!(
|
|
||||||
UserStatus::from_client_preference(value).is_some(),
|
|
||||||
"{value}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for value in [
|
|
||||||
"user_offline",
|
|
||||||
"iota_offline",
|
|
||||||
"iota_online",
|
|
||||||
"user_borked",
|
|
||||||
"iota_borked",
|
|
||||||
"unknown",
|
|
||||||
] {
|
|
||||||
assert_eq!(UserStatus::from_client_preference(value), None, "{value}");
|
|
||||||
assert_eq!(UserStatus::from_str(value), None, "{value}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn invisible_is_publicly_offline() {
|
|
||||||
assert_eq!(
|
|
||||||
UserStatus::user_invisible.public_value(),
|
|
||||||
UserStatus::user_offline
|
|
||||||
);
|
|
||||||
assert_eq!(UserStatus::user_dnd.public_value(), UserStatus::user_dnd);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
143
src/state.rs
143
src/state.rs
|
|
@ -1,143 +0,0 @@
|
||||||
use crate::{
|
|
||||||
config::OmegaConfig, identity::OmegaIdentity, sql::user_online_tracker::PresenceTracker,
|
|
||||||
};
|
|
||||||
use dashmap::DashMap;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
use tokio::sync::Semaphore;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
||||||
pub enum AccountChallengeOperation {
|
|
||||||
Attach,
|
|
||||||
Delete,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct AccountChallenge {
|
|
||||||
pub nonce: u64,
|
|
||||||
pub created_at: Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct OmegaState {
|
|
||||||
pub presence: Arc<PresenceTracker>,
|
|
||||||
pub identity: Arc<OmegaIdentity>,
|
|
||||||
pub(crate) config: Arc<OmegaConfig>,
|
|
||||||
pub(crate) global_handler_limit: Arc<Semaphore>,
|
|
||||||
pub(crate) omikron_handler_concurrency: usize,
|
|
||||||
challenges: DashMap<(AccountChallengeOperation, i64, i64), AccountChallenge>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OmegaState {
|
|
||||||
fn with_handler_limits(identity: OmegaIdentity, config: OmegaConfig) -> Self {
|
|
||||||
let omikron_handler_concurrency = config.dispatch.omikron_handler_concurrency;
|
|
||||||
let global_handler_concurrency = config.dispatch.global_handler_concurrency;
|
|
||||||
Self {
|
|
||||||
presence: Arc::new(PresenceTracker::default()),
|
|
||||||
identity: Arc::new(identity),
|
|
||||||
config: Arc::new(config),
|
|
||||||
global_handler_limit: Arc::new(Semaphore::new(global_handler_concurrency)),
|
|
||||||
omikron_handler_concurrency,
|
|
||||||
challenges: DashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(identity: OmegaIdentity, config: OmegaConfig) -> Arc<Self> {
|
|
||||||
Arc::new(Self::with_handler_limits(identity, config))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn test_state() -> Arc<Self> {
|
|
||||||
use mtp::crypto::Keyring;
|
|
||||||
|
|
||||||
let mut config = OmegaConfig::default();
|
|
||||||
config.dispatch.omikron_handler_concurrency = 4;
|
|
||||||
config.dispatch.global_handler_concurrency = 8;
|
|
||||||
Arc::new(Self::with_handler_limits(
|
|
||||||
OmegaIdentity::from_keyring(Keyring::generate()),
|
|
||||||
config,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn issue_challenge(
|
|
||||||
&self,
|
|
||||||
operation: AccountChallengeOperation,
|
|
||||||
user_id: i64,
|
|
||||||
requester_iota_id: i64,
|
|
||||||
) -> u64 {
|
|
||||||
let nonce = rand::random::<u64>();
|
|
||||||
self.challenges.insert(
|
|
||||||
(operation, user_id, requester_iota_id),
|
|
||||||
AccountChallenge {
|
|
||||||
nonce,
|
|
||||||
created_at: Instant::now(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
nonce
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn consume_challenge(
|
|
||||||
&self,
|
|
||||||
operation: AccountChallengeOperation,
|
|
||||||
user_id: i64,
|
|
||||||
requester_iota_id: i64,
|
|
||||||
nonce: u64,
|
|
||||||
) -> bool {
|
|
||||||
self.challenges
|
|
||||||
.remove(&(operation, user_id, requester_iota_id))
|
|
||||||
.is_some_and(|(_, value)| {
|
|
||||||
value.nonce == nonce && value.created_at.elapsed() <= Duration::from_secs(120)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::OmegaState;
|
|
||||||
use crate::sql::connection_status::UserStatus;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn state_instances_have_independent_presence_trackers() {
|
|
||||||
let first = OmegaState::test_state();
|
|
||||||
let second = OmegaState::test_state();
|
|
||||||
|
|
||||||
first.presence.connect_iota(11, 42);
|
|
||||||
|
|
||||||
assert!(first.presence.has_iota_route(11));
|
|
||||||
assert!(!second.presence.has_iota_route(11));
|
|
||||||
assert_eq!(first.presence.primary_iota_route(11), Some(42));
|
|
||||||
assert_eq!(second.presence.primary_iota_route(11), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn two_session_private_and_public_presence_flow_is_authoritative() {
|
|
||||||
let state = OmegaState::test_state();
|
|
||||||
state.presence.set_preference(7, UserStatus::user_online);
|
|
||||||
state.presence.set_preference(8, UserStatus::user_online);
|
|
||||||
state.presence.connect_iota(11, 42);
|
|
||||||
state.presence.track_session(7, 100, 42, 11);
|
|
||||||
state.presence.track_session(7, 101, 42, 11);
|
|
||||||
state.presence.replace_subscription(7, 100, 42, vec![8]);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
state.presence.resolve_public_state(8, 11),
|
|
||||||
UserStatus::user_offline
|
|
||||||
);
|
|
||||||
state.presence.set_preference(8, UserStatus::user_invisible);
|
|
||||||
assert_eq!(
|
|
||||||
state.presence.resolve_public_state(8, 11),
|
|
||||||
UserStatus::user_offline
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
state.presence.resolve_private_state(8),
|
|
||||||
UserStatus::user_invisible
|
|
||||||
);
|
|
||||||
|
|
||||||
state.presence.remove_session(7, 100, 42);
|
|
||||||
assert!(state.presence.owns_session(7, 101, 42));
|
|
||||||
state.presence.remove_session(7, 101, 42);
|
|
||||||
assert_eq!(
|
|
||||||
state.presence.resolve_public_state(7, 11),
|
|
||||||
UserStatus::user_offline
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
use std::collections::BTreeSet;
|
|
||||||
|
|
||||||
const OMIKRON_PREFIX: &str = "omikron;caps=";
|
|
||||||
const OMEGA_PREFIX: &str = "omega;caps=";
|
|
||||||
const SET_USER_STATE: &str = "set_user_state_v1";
|
|
||||||
const STATE_SUBSCRIBE: &str = "state_subscribe_v1";
|
|
||||||
const SESSION_SNAPSHOT: &str = "session_snapshot_v1";
|
|
||||||
const CLIENT_STATE_PUSH: &str = "client_state_push_v1";
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
||||||
pub struct PeerCapabilities {
|
|
||||||
pub set_user_state_v1: bool,
|
|
||||||
pub state_subscribe_v1: bool,
|
|
||||||
pub session_snapshot_v1: bool,
|
|
||||||
pub client_state_push_v1: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PeerCapabilities {
|
|
||||||
/// A missing descriptor is the legacy protocol: tuple route snapshots,
|
|
||||||
/// GetStates-only subscription refreshes, and ClientChanged pushes.
|
|
||||||
pub fn from_identification_description(description: Option<&str>) -> Result<Self, ()> {
|
|
||||||
parse_capabilities(description, OMIKRON_PREFIX)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
||||||
pub struct OmegaCapabilities {
|
|
||||||
pub set_user_state_v1: bool,
|
|
||||||
pub state_subscribe_v1: bool,
|
|
||||||
pub session_snapshot_v1: bool,
|
|
||||||
pub client_state_push_v1: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OmegaCapabilities {
|
|
||||||
pub fn current() -> Self {
|
|
||||||
Self {
|
|
||||||
set_user_state_v1: true,
|
|
||||||
state_subscribe_v1: true,
|
|
||||||
session_snapshot_v1: true,
|
|
||||||
client_state_push_v1: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn identification_description(&self) -> String {
|
|
||||||
let mut names = Vec::new();
|
|
||||||
if self.set_user_state_v1 {
|
|
||||||
names.push(SET_USER_STATE);
|
|
||||||
}
|
|
||||||
if self.state_subscribe_v1 {
|
|
||||||
names.push(STATE_SUBSCRIBE);
|
|
||||||
}
|
|
||||||
if self.session_snapshot_v1 {
|
|
||||||
names.push(SESSION_SNAPSHOT);
|
|
||||||
}
|
|
||||||
if self.client_state_push_v1 {
|
|
||||||
names.push(CLIENT_STATE_PUSH);
|
|
||||||
}
|
|
||||||
format!("{OMEGA_PREFIX}{}", names.join(","))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_capabilities(description: Option<&str>, prefix: &str) -> Result<PeerCapabilities, ()> {
|
|
||||||
let Some(description) = description else {
|
|
||||||
return Ok(PeerCapabilities::default());
|
|
||||||
};
|
|
||||||
if description == "omikron" {
|
|
||||||
return Ok(PeerCapabilities::default());
|
|
||||||
}
|
|
||||||
let Some(capabilities) = description.strip_prefix(prefix) else {
|
|
||||||
return Err(());
|
|
||||||
};
|
|
||||||
let mut seen = BTreeSet::new();
|
|
||||||
for capability in capabilities.split(',') {
|
|
||||||
if capability.is_empty() || !seen.insert(capability) {
|
|
||||||
return Err(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if seen.iter().any(|capability| {
|
|
||||||
!matches!(
|
|
||||||
*capability,
|
|
||||||
SET_USER_STATE | STATE_SUBSCRIBE | SESSION_SNAPSHOT | CLIENT_STATE_PUSH
|
|
||||||
)
|
|
||||||
}) {
|
|
||||||
return Err(());
|
|
||||||
}
|
|
||||||
Ok(PeerCapabilities {
|
|
||||||
set_user_state_v1: seen.contains(SET_USER_STATE),
|
|
||||||
state_subscribe_v1: seen.contains(STATE_SUBSCRIBE),
|
|
||||||
session_snapshot_v1: seen.contains(SESSION_SNAPSHOT),
|
|
||||||
client_state_push_v1: seen.contains(CLIENT_STATE_PUSH),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn advertised_capabilities_are_parsed() {
|
|
||||||
let capabilities = PeerCapabilities::from_identification_description(Some(
|
|
||||||
"omikron;caps=set_user_state_v1,state_subscribe_v1,session_snapshot_v1,client_state_push_v1",
|
|
||||||
)).unwrap();
|
|
||||||
assert!(capabilities.set_user_state_v1);
|
|
||||||
assert!(capabilities.state_subscribe_v1);
|
|
||||||
assert!(capabilities.session_snapshot_v1);
|
|
||||||
assert!(capabilities.client_state_push_v1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn invalid_capability_values_fail_identification() {
|
|
||||||
assert!(
|
|
||||||
PeerCapabilities::from_identification_description(Some("omikron;caps=unsupported"))
|
|
||||||
.is_err()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn legacy_peer_has_no_version_specific_features() {
|
|
||||||
let capabilities = PeerCapabilities::from_identification_description(None).unwrap();
|
|
||||||
assert!(!capabilities.set_user_state_v1);
|
|
||||||
assert!(!capabilities.state_subscribe_v1);
|
|
||||||
assert!(!capabilities.session_snapshot_v1);
|
|
||||||
assert!(!capabilities.client_state_push_v1);
|
|
||||||
assert_eq!(
|
|
||||||
PeerCapabilities::from_identification_description(Some("omikron")),
|
|
||||||
Ok(PeerCapabilities::default())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn reconnecting_with_the_same_identification_is_stable() {
|
|
||||||
let description = Some(
|
|
||||||
"omikron;caps=set_user_state_v1,state_subscribe_v1,session_snapshot_v1,client_state_push_v1",
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
PeerCapabilities::from_identification_description(description),
|
|
||||||
PeerCapabilities::from_identification_description(description)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn omega_capability_description_is_distinct_from_omikron_capabilities() {
|
|
||||||
let description = OmegaCapabilities::current().identification_description();
|
|
||||||
assert!(description.starts_with(OMEGA_PREFIX));
|
|
||||||
assert!(PeerCapabilities::from_identification_description(Some(&description)).is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,178 +1 @@
|
||||||
pub(crate) use super::omikron_connection::{OmikronConnection, OmikronResult};
|
pub(crate) use super::omikron_connection::OmikronConnection;
|
||||||
|
|
||||||
use mtp::codec::{CommunicationValue, DataValue};
|
|
||||||
|
|
||||||
pub(crate) trait RequiredMtpFields {
|
|
||||||
/// Require a nonzero request correlation ID. `None` and `Some(0)` are
|
|
||||||
/// distinct MTP wire states, but both are invalid for Omega requests.
|
|
||||||
fn require_id(&self) -> OmikronResult<u32>;
|
|
||||||
/// Require a nonzero authenticated peer identity.
|
|
||||||
fn require_sender(&self) -> OmikronResult<u64>;
|
|
||||||
/// Require a nonzero application routing target.
|
|
||||||
fn require_receiver(&self) -> OmikronResult<u64>;
|
|
||||||
fn require_sender_i64(&self) -> OmikronResult<i64>;
|
|
||||||
fn require_receiver_i64(&self) -> OmikronResult<i64>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RequiredMtpFields for CommunicationValue {
|
|
||||||
fn require_id(&self) -> OmikronResult<u32> {
|
|
||||||
self.id().filter(|id| *id != 0).ok_or_else(|| {
|
|
||||||
crate::OmegaError::Validation("MTP message is missing request id".into())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn require_sender(&self) -> OmikronResult<u64> {
|
|
||||||
self.sender()
|
|
||||||
.filter(|sender| *sender != 0)
|
|
||||||
.ok_or_else(|| crate::OmegaError::Validation("MTP message is missing sender".into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn require_receiver(&self) -> OmikronResult<u64> {
|
|
||||||
self.receiver()
|
|
||||||
.filter(|receiver| *receiver != 0)
|
|
||||||
.ok_or_else(|| crate::OmegaError::Validation("MTP message is missing receiver".into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn require_sender_i64(&self) -> OmikronResult<i64> {
|
|
||||||
positive_i64(self.require_sender()?, "sender")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn require_receiver_i64(&self) -> OmikronResult<i64> {
|
|
||||||
positive_i64(self.require_receiver()?, "receiver")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn positive_i64(value: impl TryInto<i128>, field: &str) -> OmikronResult<i64> {
|
|
||||||
let value = value
|
|
||||||
.try_into()
|
|
||||||
.map_err(|_| crate::OmegaError::Validation(format!("invalid {field}")))?;
|
|
||||||
let value = i64::try_from(value)
|
|
||||||
.map_err(|_| crate::OmegaError::Validation(format!("invalid {field}")))?;
|
|
||||||
if value <= 0 {
|
|
||||||
return Err(crate::OmegaError::Validation(format!("invalid {field}")));
|
|
||||||
}
|
|
||||||
Ok(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn validate_dispatch_fields(value: &CommunicationValue) -> OmikronResult<()> {
|
|
||||||
let Some(message_type) = value.get_comm_type_enum() else {
|
|
||||||
return Err(crate::OmegaError::Validation(
|
|
||||||
"MTP message has an unknown communication type".into(),
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
if !matches!(
|
|
||||||
message_type,
|
|
||||||
mtp::codec::CommunicationType::ClientChanged
|
|
||||||
| mtp::codec::CommunicationType::PushNotification
|
|
||||||
) {
|
|
||||||
value.require_id()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches!(
|
|
||||||
message_type,
|
|
||||||
mtp::codec::CommunicationType::GetUserData
|
|
||||||
| mtp::codec::CommunicationType::ChangeUserData
|
|
||||||
| mtp::codec::CommunicationType::DeleteUser
|
|
||||||
| mtp::codec::CommunicationType::AttachUserBegin
|
|
||||||
| mtp::codec::CommunicationType::AttachUserComplete
|
|
||||||
| mtp::codec::CommunicationType::DeleteUserCredentialBegin
|
|
||||||
| mtp::codec::CommunicationType::DeleteUserCredentialComplete
|
|
||||||
| mtp::codec::CommunicationType::EraseHostedUserDataAck
|
|
||||||
| mtp::codec::CommunicationType::ReleaseUserFromIota
|
|
||||||
| mtp::codec::CommunicationType::DeleteIota
|
|
||||||
| mtp::codec::CommunicationType::GetNotifications
|
|
||||||
| mtp::codec::CommunicationType::ReadNotification
|
|
||||||
| mtp::codec::CommunicationType::StateSubscribe
|
|
||||||
) {
|
|
||||||
value.require_sender()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) trait OptionalDataValueCompat {
|
|
||||||
fn as_number(&self) -> Option<i128>;
|
|
||||||
fn as_signed_number(&self) -> Option<i128>;
|
|
||||||
fn as_str(&self) -> Option<&str>;
|
|
||||||
fn as_bytes(&self) -> Option<Vec<u8>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OptionalDataValueCompat for Option<&DataValue> {
|
|
||||||
fn as_number(&self) -> Option<i128> {
|
|
||||||
self.and_then(|value| value.as_number())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_signed_number(&self) -> Option<i128> {
|
|
||||||
self.and_then(|value| value.as_signed_number())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_str(&self) -> Option<&str> {
|
|
||||||
self.and_then(|value| value.as_str())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_bytes(&self) -> Option<Vec<u8>> {
|
|
||||||
self.and_then(|value| value.as_bytes())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::{RequiredMtpFields, validate_dispatch_fields};
|
|
||||||
use mtp::codec::{CommunicationType, CommunicationValue};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn missing_request_id_is_rejected_without_a_zero_fallback() {
|
|
||||||
let value = CommunicationValue::new(CommunicationType::GetUserData).without_id();
|
|
||||||
assert!(value.require_id().is_err());
|
|
||||||
assert!(validate_dispatch_fields(&value).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn zero_routing_values_are_rejected_by_omega_contract() {
|
|
||||||
let value = CommunicationValue::new(CommunicationType::GetUserData)
|
|
||||||
.with_id(0)
|
|
||||||
.with_sender(0)
|
|
||||||
.with_receiver(0);
|
|
||||||
|
|
||||||
assert!(value.require_id().is_err());
|
|
||||||
assert!(value.require_sender().is_err());
|
|
||||||
assert!(value.require_receiver().is_err());
|
|
||||||
assert!(validate_dispatch_fields(&value).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn security_sensitive_messages_require_sender() {
|
|
||||||
for message_type in [
|
|
||||||
CommunicationType::GetUserData,
|
|
||||||
CommunicationType::ChangeUserData,
|
|
||||||
CommunicationType::DeleteUser,
|
|
||||||
CommunicationType::AttachUserBegin,
|
|
||||||
CommunicationType::AttachUserComplete,
|
|
||||||
CommunicationType::DeleteUserCredentialBegin,
|
|
||||||
CommunicationType::DeleteUserCredentialComplete,
|
|
||||||
CommunicationType::EraseHostedUserDataAck,
|
|
||||||
CommunicationType::ReleaseUserFromIota,
|
|
||||||
CommunicationType::DeleteIota,
|
|
||||||
CommunicationType::GetNotifications,
|
|
||||||
CommunicationType::ReadNotification,
|
|
||||||
CommunicationType::StateSubscribe,
|
|
||||||
] {
|
|
||||||
let value = CommunicationValue::new(message_type).with_id(1);
|
|
||||||
assert!(value.require_sender().is_err());
|
|
||||||
assert!(validate_dispatch_fields(&value).is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn optional_client_changed_id_remains_optional() {
|
|
||||||
let value = CommunicationValue::new(CommunicationType::ClientChanged).without_id();
|
|
||||||
assert!(validate_dispatch_fields(&value).is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn push_notification_uses_its_logical_sender_field() {
|
|
||||||
let value = CommunicationValue::new(CommunicationType::PushNotification);
|
|
||||||
assert!(validate_dispatch_fields(&value).is_ok());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,9 @@
|
||||||
use super::super::connection::{
|
use super::super::omikron_connection::{OmikronConnection, OmikronResult};
|
||||||
OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields,
|
|
||||||
};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{iota_repo, user_repo},
|
db::{iota_repo, user_repo},
|
||||||
models::{IotaId, UserId},
|
models::{IotaId, UserId},
|
||||||
state::AccountChallengeOperation,
|
|
||||||
};
|
|
||||||
use mtp::{
|
|
||||||
codec::{CommunicationType, CommunicationValue, DataType, DataValue},
|
|
||||||
crypto::{verify_ed25519, verify_ml_dsa},
|
|
||||||
};
|
};
|
||||||
|
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
async fn delete(
|
async fn delete(
|
||||||
|
|
@ -22,417 +16,28 @@ async fn delete(
|
||||||
Err(error) => CommunicationValue::new(CommunicationType::ErrorInternal)
|
Err(error) => CommunicationValue::new(CommunicationType::ErrorInternal)
|
||||||
.add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())),
|
.add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())),
|
||||||
};
|
};
|
||||||
connection
|
connection.send(&response.with_id(value.get_id())).await
|
||||||
.send(&response.with_id(value.require_id()?))
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn user(
|
pub async fn user(
|
||||||
connection: Arc<OmikronConnection>,
|
connection: Arc<OmikronConnection>,
|
||||||
value: CommunicationValue,
|
value: CommunicationValue,
|
||||||
) -> OmikronResult<()> {
|
) -> OmikronResult<()> {
|
||||||
value.require_id()?;
|
delete(
|
||||||
value.require_sender_i64()?;
|
connection,
|
||||||
let user_id = UserId::from(value.require_sender_i64()?);
|
value.clone(),
|
||||||
complete_delete(connection, value, user_id).await
|
user_repo::delete_user(UserId::from(value.get_sender() as i64)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
pub async fn iota(
|
pub async fn iota(
|
||||||
connection: Arc<OmikronConnection>,
|
connection: Arc<OmikronConnection>,
|
||||||
value: CommunicationValue,
|
value: CommunicationValue,
|
||||||
) -> OmikronResult<()> {
|
) -> OmikronResult<()> {
|
||||||
value.require_id()?;
|
|
||||||
value.require_sender_i64()?;
|
|
||||||
delete(
|
delete(
|
||||||
connection,
|
connection,
|
||||||
value.clone(),
|
value.clone(),
|
||||||
iota_repo::delete_iota(IotaId::from(value.require_sender_i64()?)),
|
iota_repo::delete_iota(IotaId::from(value.get_sender() as i64)),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn release_from_iota(
|
|
||||||
connection: Arc<OmikronConnection>,
|
|
||||||
value: CommunicationValue,
|
|
||||||
) -> OmikronResult<()> {
|
|
||||||
value.require_id()?;
|
|
||||||
value.require_sender_i64()?;
|
|
||||||
let Some(user_id) = value
|
|
||||||
.get_data(DataType::UserId)
|
|
||||||
.as_signed_number()
|
|
||||||
.and_then(|id| i64::try_from(id).ok())
|
|
||||||
.filter(|id| *id > 0)
|
|
||||||
else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
let requester = IotaId::from(value.require_sender_i64()?);
|
|
||||||
let Ok(user) = user_repo::get_by_user_id(UserId::from(user_id)).await else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorNotFound)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
if user.iota_id != Some(requester) {
|
|
||||||
return connection
|
|
||||||
.send_error_response(
|
|
||||||
value.require_id()?,
|
|
||||||
CommunicationType::ErrorNotAuthenticated,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
let previous_iota = user.iota_id;
|
|
||||||
match user_repo::change_iota_id(user.id, None).await {
|
|
||||||
Ok(()) => {
|
|
||||||
if let Some(iota) = previous_iota {
|
|
||||||
let _ = crate::transport::omikron_manager::publish_iota_user_snapshot(iota.0).await;
|
|
||||||
}
|
|
||||||
connection
|
|
||||||
.send(
|
|
||||||
&CommunicationValue::new(CommunicationType::Success)
|
|
||||||
.with_id(value.require_id()?),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
connection
|
|
||||||
.send(
|
|
||||||
&CommunicationValue::new(CommunicationType::ErrorInternal)
|
|
||||||
.with_id(value.require_id()?)
|
|
||||||
.add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn lifecycle_payload(domain: &[u8], user_id: i64, iota_id: i64, nonce: u64) -> Vec<u8> {
|
|
||||||
let mut payload = Vec::with_capacity(domain.len() + 32);
|
|
||||||
payload.extend_from_slice(domain);
|
|
||||||
payload.extend_from_slice(&user_id.to_be_bytes());
|
|
||||||
payload.extend_from_slice(&iota_id.to_be_bytes());
|
|
||||||
payload.extend_from_slice(&nonce.to_be_bytes());
|
|
||||||
payload
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn attach_begin(
|
|
||||||
connection: Arc<OmikronConnection>,
|
|
||||||
value: CommunicationValue,
|
|
||||||
) -> OmikronResult<()> {
|
|
||||||
value.require_id()?;
|
|
||||||
value.require_sender_i64()?;
|
|
||||||
let Some(user_id) = value
|
|
||||||
.get_data(DataType::UserId)
|
|
||||||
.as_signed_number()
|
|
||||||
.and_then(|v| i64::try_from(v).ok())
|
|
||||||
.filter(|v| *v > 0)
|
|
||||||
else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
if user_repo::get_by_user_id(UserId::from(user_id))
|
|
||||||
.await
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
return connection
|
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorNotFound)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
let requester = value.require_sender_i64()?;
|
|
||||||
let nonce =
|
|
||||||
connection
|
|
||||||
.state()
|
|
||||||
.issue_challenge(AccountChallengeOperation::Attach, user_id, requester);
|
|
||||||
connection
|
|
||||||
.send(
|
|
||||||
&CommunicationValue::new(CommunicationType::AttachUserChallenge)
|
|
||||||
.with_id(value.require_id()?)
|
|
||||||
.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into()))
|
|
||||||
.add_typed_default(DataType::ServerNonce, DataValue::SignedNumber(nonce.into())),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn attach_complete(
|
|
||||||
connection: Arc<OmikronConnection>,
|
|
||||||
value: CommunicationValue,
|
|
||||||
) -> OmikronResult<()> {
|
|
||||||
value.require_id()?;
|
|
||||||
value.require_sender_i64()?;
|
|
||||||
let Some(user_id) = value
|
|
||||||
.get_data(DataType::UserId)
|
|
||||||
.as_signed_number()
|
|
||||||
.and_then(|v| i64::try_from(v).ok())
|
|
||||||
.filter(|v| *v > 0)
|
|
||||||
else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
let requester = value.require_sender_i64()?;
|
|
||||||
let Some(nonce) = value
|
|
||||||
.get_data(DataType::ServerNonce)
|
|
||||||
.as_signed_number()
|
|
||||||
.and_then(|v| u64::try_from(v).ok())
|
|
||||||
else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(
|
|
||||||
value.require_id()?,
|
|
||||||
CommunicationType::ErrorInvalidChallenge,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
let signature = value.get_data(DataType::Signature).as_bytes();
|
|
||||||
let pq_signature = value.get_data(DataType::PqSignature).as_bytes();
|
|
||||||
let (Some(signature), Some(pq_signature)) = (signature, pq_signature) else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(
|
|
||||||
value.require_id()?,
|
|
||||||
CommunicationType::ErrorInvalidChallenge,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
if !connection.state().consume_challenge(
|
|
||||||
AccountChallengeOperation::Attach,
|
|
||||||
user_id,
|
|
||||||
requester,
|
|
||||||
nonce,
|
|
||||||
) {
|
|
||||||
return connection
|
|
||||||
.send_error_response(
|
|
||||||
value.require_id()?,
|
|
||||||
CommunicationType::ErrorInvalidChallenge,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
let Ok(user) = user_repo::get_by_user_id(UserId::from(user_id)).await else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorNotFound)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
let payload = lifecycle_payload(b"tensamin:user-attach:v1\0", user_id, requester, nonce);
|
|
||||||
if verify_ed25519(&user.public_key.sig_cl_public_key, &payload, &signature).is_err()
|
|
||||||
|| verify_ml_dsa(&user.public_key.sig_pq_public_key, &payload, &pq_signature).is_err()
|
|
||||||
{
|
|
||||||
return connection
|
|
||||||
.send_error_response(
|
|
||||||
value.require_id()?,
|
|
||||||
CommunicationType::ErrorNotAuthenticated,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
let previous_iota = user.iota_id;
|
|
||||||
match user_repo::change_iota_id(user.id, Some(IotaId::from(requester))).await {
|
|
||||||
Ok(()) => {
|
|
||||||
if let Some(iota) = previous_iota.filter(|id| id.0 != requester) {
|
|
||||||
let _ = crate::transport::omikron_manager::publish_iota_user_snapshot(iota.0).await;
|
|
||||||
}
|
|
||||||
let _ = crate::transport::omikron_manager::publish_iota_user_snapshot(requester).await;
|
|
||||||
connection
|
|
||||||
.send(
|
|
||||||
&CommunicationValue::new(CommunicationType::Success)
|
|
||||||
.with_id(value.require_id()?),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
connection
|
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorInternal)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn complete_delete(
|
|
||||||
connection: Arc<OmikronConnection>,
|
|
||||||
value: CommunicationValue,
|
|
||||||
user_id: UserId,
|
|
||||||
) -> OmikronResult<()> {
|
|
||||||
match user_repo::delete_user_with_pending_erasure(user_id).await {
|
|
||||||
Ok(iota_id) => {
|
|
||||||
let cleanup_pending = iota_id.is_some();
|
|
||||||
if let Some(iota_id) = iota_id {
|
|
||||||
let _ =
|
|
||||||
crate::transport::omikron_manager::publish_iota_user_snapshot(iota_id.0).await;
|
|
||||||
crate::transport::omikron_manager::deliver_pending_erasures(iota_id.0).await;
|
|
||||||
}
|
|
||||||
connection
|
|
||||||
.send(
|
|
||||||
&CommunicationValue::new(CommunicationType::Success)
|
|
||||||
.with_id(value.require_id()?)
|
|
||||||
.add_typed_default(
|
|
||||||
DataType::CleanupPending,
|
|
||||||
DataValue::Bool(cleanup_pending),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
Err(crate::error::OmegaError::NotFound) => {
|
|
||||||
connection
|
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorNotFound)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
connection
|
|
||||||
.send(
|
|
||||||
&CommunicationValue::new(CommunicationType::ErrorInternal)
|
|
||||||
.with_id(value.require_id()?)
|
|
||||||
.add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_credential_begin(
|
|
||||||
connection: Arc<OmikronConnection>,
|
|
||||||
value: CommunicationValue,
|
|
||||||
) -> OmikronResult<()> {
|
|
||||||
value.require_id()?;
|
|
||||||
value.require_sender_i64()?;
|
|
||||||
let Some(user_id) = value
|
|
||||||
.get_data(DataType::UserId)
|
|
||||||
.as_signed_number()
|
|
||||||
.and_then(|v| i64::try_from(v).ok())
|
|
||||||
.filter(|v| *v > 0)
|
|
||||||
else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
if user_repo::get_by_user_id(UserId::from(user_id))
|
|
||||||
.await
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
return connection
|
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorNotFound)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
let requester = value.require_sender_i64()?;
|
|
||||||
let nonce =
|
|
||||||
connection
|
|
||||||
.state()
|
|
||||||
.issue_challenge(AccountChallengeOperation::Delete, user_id, requester);
|
|
||||||
connection
|
|
||||||
.send(
|
|
||||||
&CommunicationValue::new(CommunicationType::DeleteUserCredentialChallenge)
|
|
||||||
.with_id(value.require_id()?)
|
|
||||||
.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into()))
|
|
||||||
.add_typed_default(DataType::ServerNonce, DataValue::SignedNumber(nonce.into())),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_credential_complete(
|
|
||||||
connection: Arc<OmikronConnection>,
|
|
||||||
value: CommunicationValue,
|
|
||||||
) -> OmikronResult<()> {
|
|
||||||
value.require_id()?;
|
|
||||||
value.require_sender_i64()?;
|
|
||||||
let Some(user_id) = value
|
|
||||||
.get_data(DataType::UserId)
|
|
||||||
.as_signed_number()
|
|
||||||
.and_then(|v| i64::try_from(v).ok())
|
|
||||||
.filter(|v| *v > 0)
|
|
||||||
else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
let requester = value.require_sender_i64()?;
|
|
||||||
let Some(nonce) = value
|
|
||||||
.get_data(DataType::ServerNonce)
|
|
||||||
.as_signed_number()
|
|
||||||
.and_then(|v| u64::try_from(v).ok())
|
|
||||||
else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(
|
|
||||||
value.require_id()?,
|
|
||||||
CommunicationType::ErrorInvalidChallenge,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
let (Some(signature), Some(pq_signature)) = (
|
|
||||||
value.get_data(DataType::Signature).as_bytes(),
|
|
||||||
value.get_data(DataType::PqSignature).as_bytes(),
|
|
||||||
) else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(
|
|
||||||
value.require_id()?,
|
|
||||||
CommunicationType::ErrorInvalidChallenge,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
if !connection.state().consume_challenge(
|
|
||||||
AccountChallengeOperation::Delete,
|
|
||||||
user_id,
|
|
||||||
requester,
|
|
||||||
nonce,
|
|
||||||
) {
|
|
||||||
return connection
|
|
||||||
.send_error_response(
|
|
||||||
value.require_id()?,
|
|
||||||
CommunicationType::ErrorInvalidChallenge,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
let Ok(user) = user_repo::get_by_user_id(UserId::from(user_id)).await else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorNotFound)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
let payload = lifecycle_payload(b"tensamin:user-delete:v1\0", user_id, requester, nonce);
|
|
||||||
if verify_ed25519(&user.public_key.sig_cl_public_key, &payload, &signature).is_err()
|
|
||||||
|| verify_ml_dsa(&user.public_key.sig_pq_public_key, &payload, &pq_signature).is_err()
|
|
||||||
{
|
|
||||||
return connection
|
|
||||||
.send_error_response(
|
|
||||||
value.require_id()?,
|
|
||||||
CommunicationType::ErrorNotAuthenticated,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
complete_delete(connection, value, user.id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn erase_hosted_user_data_ack(
|
|
||||||
connection: Arc<OmikronConnection>,
|
|
||||||
value: CommunicationValue,
|
|
||||||
) -> OmikronResult<()> {
|
|
||||||
value.require_id()?;
|
|
||||||
value.require_sender_i64()?;
|
|
||||||
let Some(user_id) = value
|
|
||||||
.get_data(DataType::UserId)
|
|
||||||
.as_signed_number()
|
|
||||||
.and_then(|v| i64::try_from(v).ok())
|
|
||||||
.filter(|v| *v > 0)
|
|
||||||
else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidUserId)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
let iota_id = IotaId::from(value.require_sender_i64()?);
|
|
||||||
match user_repo::acknowledge_pending_erasure(UserId::from(user_id), iota_id).await {
|
|
||||||
Ok(true) => {
|
|
||||||
connection
|
|
||||||
.send(
|
|
||||||
&CommunicationValue::new(CommunicationType::Success)
|
|
||||||
.with_id(value.require_id()?),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
Ok(false) => {
|
|
||||||
connection
|
|
||||||
.send_error_response(
|
|
||||||
value.require_id()?,
|
|
||||||
CommunicationType::ErrorNotAuthenticated,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
connection
|
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorInternal)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
use super::super::connection::{
|
use super::super::omikron_connection::{OmikronConnection, OmikronResult};
|
||||||
OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields,
|
|
||||||
};
|
|
||||||
use crate::server::short_link::add_short_link;
|
use crate::server::short_link::add_short_link;
|
||||||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -9,15 +7,15 @@ pub async fn shorten(
|
||||||
connection: Arc<OmikronConnection>,
|
connection: Arc<OmikronConnection>,
|
||||||
value: CommunicationValue,
|
value: CommunicationValue,
|
||||||
) -> OmikronResult<()> {
|
) -> OmikronResult<()> {
|
||||||
let link_data = value.get_data(DataType::Link);
|
let link = value
|
||||||
let link = link_data
|
.get_data(DataType::Link)
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or(crate::error::OmegaError::InvalidResponse)?;
|
.ok_or(crate::error::OmegaError::InvalidResponse)?;
|
||||||
let short = add_short_link(link)
|
let short = add_short_link(link)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| crate::error::OmegaError::Transport("short link error".to_string()))?;
|
.map_err(|_| crate::error::OmegaError::Transport("short link error".to_string()))?;
|
||||||
let response = CommunicationValue::new(CommunicationType::ShortenLink)
|
let response = CommunicationValue::new(CommunicationType::ShortenLink)
|
||||||
.with_id(value.require_id()?)
|
.with_id(value.get_id())
|
||||||
.add_typed_default(DataType::Link, DataValue::Str(short));
|
.add_typed_default(DataType::Link, DataValue::Str(short));
|
||||||
connection.send(&response).await
|
connection.send(&response).await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
use super::super::connection::{
|
use super::super::omikron_connection::{OmikronConnection, OmikronResult};
|
||||||
OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields,
|
|
||||||
};
|
|
||||||
use crate::{db::notification_repo, log, models::UserId};
|
use crate::{db::notification_repo, log, models::UserId};
|
||||||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
||||||
use mtp::type_map::TypeMap;
|
use mtp::type_map::TypeMap;
|
||||||
|
|
@ -10,36 +8,35 @@ pub async fn get(
|
||||||
connection: Arc<OmikronConnection>,
|
connection: Arc<OmikronConnection>,
|
||||||
value: CommunicationValue,
|
value: CommunicationValue,
|
||||||
) -> OmikronResult<()> {
|
) -> OmikronResult<()> {
|
||||||
let request_id = value.require_id()?;
|
let notifications =
|
||||||
let sender = value.require_sender_i64()?;
|
match notification_repo::get_notifications(UserId::from(value.get_sender() as i64)).await {
|
||||||
let notifications = match notification_repo::get_notifications(UserId::from(sender)).await {
|
Ok(items) => items
|
||||||
Ok(items) => items
|
.into_iter()
|
||||||
.into_iter()
|
.map(|item| {
|
||||||
.map(|item| {
|
let tm = TypeMap::latest();
|
||||||
let tm = TypeMap::latest();
|
let Some(sender) = DataType::SenderId.try_to_id(&tm) else {
|
||||||
let Some(sender) = DataType::SenderId.try_to_id(&tm) else {
|
return DataValue::Container(Vec::new());
|
||||||
return DataValue::Container(Vec::new());
|
};
|
||||||
};
|
let Some(amount) = DataType::Amount.try_to_id(&tm) else {
|
||||||
let Some(amount) = DataType::Amount.try_to_id(&tm) else {
|
return DataValue::Container(Vec::new());
|
||||||
return DataValue::Container(Vec::new());
|
};
|
||||||
};
|
DataValue::Container(vec![
|
||||||
DataValue::Container(vec![
|
(sender, DataValue::SignedNumber(item.sender_id.0.into())),
|
||||||
(sender, DataValue::SignedNumber(item.sender_id.0.into())),
|
(amount, DataValue::SignedNumber(item.amount.into())),
|
||||||
(amount, DataValue::SignedNumber(item.amount.into())),
|
])
|
||||||
])
|
})
|
||||||
})
|
.collect(),
|
||||||
.collect(),
|
Err(error) => {
|
||||||
Err(error) => {
|
log!(
|
||||||
log!(
|
crate::util::logger::PrintType::General,
|
||||||
crate::util::logger::PrintType::General,
|
"SQL get_notifications error: {}",
|
||||||
"SQL get_notifications error: {}",
|
error
|
||||||
error
|
);
|
||||||
);
|
Vec::new()
|
||||||
Vec::new()
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
let response = CommunicationValue::new(CommunicationType::GetNotifications)
|
let response = CommunicationValue::new(CommunicationType::GetNotifications)
|
||||||
.with_id(request_id)
|
.with_id(value.get_id())
|
||||||
.add_typed_default(DataType::Notifications, DataValue::Array(notifications));
|
.add_typed_default(DataType::Notifications, DataValue::Array(notifications));
|
||||||
connection.send(&response).await
|
connection.send(&response).await
|
||||||
}
|
}
|
||||||
|
|
@ -48,17 +45,19 @@ pub async fn read(
|
||||||
connection: Arc<OmikronConnection>,
|
connection: Arc<OmikronConnection>,
|
||||||
value: CommunicationValue,
|
value: CommunicationValue,
|
||||||
) -> OmikronResult<()> {
|
) -> OmikronResult<()> {
|
||||||
let request_id = value.require_id()?;
|
let receiver = match value.get_sender() {
|
||||||
let receiver = value.require_sender_i64()?;
|
sender if sender > 0 => sender as i64,
|
||||||
|
_ => match value.get_data(DataType::ReceiverId).as_number() {
|
||||||
|
Some(id) => id as i64,
|
||||||
|
None => return Ok(()),
|
||||||
|
},
|
||||||
|
};
|
||||||
let Some(other) = value
|
let Some(other) = value
|
||||||
.get_data(DataType::SenderId)
|
.get_data(DataType::SenderId)
|
||||||
.as_number()
|
.as_number()
|
||||||
.and_then(|id| i64::try_from(id).ok())
|
.map(|id| id as i64)
|
||||||
.filter(|id| *id > 0)
|
|
||||||
else {
|
else {
|
||||||
return connection
|
return Ok(());
|
||||||
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
};
|
};
|
||||||
if let Err(error) =
|
if let Err(error) =
|
||||||
notification_repo::read_notification(UserId::from(receiver), UserId::from(other)).await
|
notification_repo::read_notification(UserId::from(receiver), UserId::from(other)).await
|
||||||
|
|
@ -70,7 +69,7 @@ pub async fn read(
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let response =
|
let response =
|
||||||
CommunicationValue::new(CommunicationType::ReadNotification).with_id(request_id);
|
CommunicationValue::new(CommunicationType::ReadNotification).with_id(value.get_id());
|
||||||
let _ = connection.send(&response).await;
|
let _ = connection.send(&response).await;
|
||||||
let sync = CommunicationValue::new(CommunicationType::ReadNotification)
|
let sync = CommunicationValue::new(CommunicationType::ReadNotification)
|
||||||
.with_receiver(receiver as u64)
|
.with_receiver(receiver as u64)
|
||||||
|
|
@ -84,54 +83,18 @@ pub async fn push(
|
||||||
connection: Arc<OmikronConnection>,
|
connection: Arc<OmikronConnection>,
|
||||||
value: CommunicationValue,
|
value: CommunicationValue,
|
||||||
) -> OmikronResult<()> {
|
) -> OmikronResult<()> {
|
||||||
let request_id = value.id().filter(|id| *id != 0);
|
let receiver = match value.get_receiver() {
|
||||||
let receiver = value.require_receiver_i64().ok().or_else(|| {
|
receiver if receiver > 0 => receiver as i64,
|
||||||
value
|
_ => match value.get_data(DataType::ReceiverId).as_number() {
|
||||||
.get_data(DataType::ReceiverId)
|
Some(id) => id as i64,
|
||||||
.as_number()
|
None => return Ok(()),
|
||||||
.and_then(|id| i64::try_from(id).ok())
|
},
|
||||||
.filter(|id| *id > 0)
|
|
||||||
});
|
|
||||||
let Some(receiver) = receiver else {
|
|
||||||
if let Some(request_id) = request_id {
|
|
||||||
return connection
|
|
||||||
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
return Ok(());
|
|
||||||
};
|
};
|
||||||
let Some(sender) = value
|
let sender = value
|
||||||
.get_data(DataType::SenderId)
|
.get_data(DataType::SenderId)
|
||||||
.as_number()
|
.as_number()
|
||||||
.and_then(|sender| i64::try_from(sender).ok())
|
.map(|id| id as i64)
|
||||||
.filter(|sender| *sender > 0)
|
.unwrap_or(value.get_sender() as i64);
|
||||||
else {
|
|
||||||
if let Some(request_id) = request_id {
|
|
||||||
return connection
|
|
||||||
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let source_omikron = connection
|
|
||||||
.clone()
|
|
||||||
.get_omikron_id()
|
|
||||||
.await
|
|
||||||
.ok_or(crate::OmegaError::NotConnected)?;
|
|
||||||
if !connection
|
|
||||||
.state()
|
|
||||||
.presence
|
|
||||||
.routes_for_user(sender)
|
|
||||||
.iter()
|
|
||||||
.any(|route| route.omikron_id == source_omikron)
|
|
||||||
{
|
|
||||||
if let Some(request_id) = request_id {
|
|
||||||
return connection
|
|
||||||
.send_error_response(request_id, CommunicationType::ErrorNotAuthenticated)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if let Err(error) =
|
if let Err(error) =
|
||||||
notification_repo::add_notification(UserId::from(receiver), UserId::from(sender)).await
|
notification_repo::add_notification(UserId::from(receiver), UserId::from(sender)).await
|
||||||
{
|
{
|
||||||
|
|
@ -141,11 +104,9 @@ pub async fn push(
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
if let Some(request_id) = request_id {
|
let response =
|
||||||
let response =
|
CommunicationValue::new(CommunicationType::PushNotification).with_id(value.get_id());
|
||||||
CommunicationValue::new(CommunicationType::PushNotification).with_id(request_id);
|
let _ = connection.send(&response).await;
|
||||||
let _ = connection.send(&response).await;
|
|
||||||
}
|
|
||||||
let push = CommunicationValue::new(CommunicationType::PushNotification)
|
let push = CommunicationValue::new(CommunicationType::PushNotification)
|
||||||
.with_receiver(receiver as u64)
|
.with_receiver(receiver as u64)
|
||||||
.add_typed_default(DataType::SenderId, DataValue::SignedNumber(sender.into()));
|
.add_typed_default(DataType::SenderId, DataValue::SignedNumber(sender.into()));
|
||||||
|
|
|
||||||
|
|
@ -1,175 +1,69 @@
|
||||||
use super::super::connection::{
|
use super::super::omikron_connection::{OmikronConnection, OmikronResult};
|
||||||
OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields,
|
|
||||||
};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::user_repo, log_in, models::IotaId, sql::connection_status::UserStatus, state::OmegaState,
|
db::user_repo,
|
||||||
|
log_in,
|
||||||
|
models::IotaId,
|
||||||
|
sql::{connection_status::UserStatus, user_online_tracker},
|
||||||
};
|
};
|
||||||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
||||||
use std::{
|
use std::sync::Arc;
|
||||||
collections::{BTreeMap, HashMap, HashSet},
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn parse_subscription(value: &CommunicationValue) -> Result<(i64, i64, Vec<i64>), &'static str> {
|
pub async fn user_connected(
|
||||||
let Some(sender) = value.sender() else {
|
_connection: Arc<OmikronConnection>,
|
||||||
return Err("user_id");
|
|
||||||
};
|
|
||||||
let user_id = i64::try_from(sender)
|
|
||||||
.ok()
|
|
||||||
.filter(|id| *id > 0)
|
|
||||||
.ok_or("user_id")?;
|
|
||||||
let session_id = value
|
|
||||||
.get_data(DataType::SessionId)
|
|
||||||
.as_number()
|
|
||||||
.and_then(|id| i64::try_from(id).ok())
|
|
||||||
.filter(|id| *id > 0)
|
|
||||||
.ok_or("session_id")?;
|
|
||||||
let Some(DataValue::Array(values)) = value.get_data(DataType::UserIds) else {
|
|
||||||
return Err("user_ids");
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut user_ids = Vec::with_capacity(values.len());
|
|
||||||
for value in values {
|
|
||||||
let DataValue::SignedNumber(user_id) = value else {
|
|
||||||
return Err("user_ids");
|
|
||||||
};
|
|
||||||
let Ok(user_id) = i64::try_from(*user_id) else {
|
|
||||||
return Err("user_ids");
|
|
||||||
};
|
|
||||||
if user_id <= 0 {
|
|
||||||
return Err("user_ids");
|
|
||||||
}
|
|
||||||
if !user_ids.contains(&user_id) {
|
|
||||||
user_ids.push(user_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok((user_id, session_id, user_ids))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_preferences(state: &OmegaState, preferences: HashMap<i64, UserStatus>) {
|
|
||||||
state.presence.set_preferences(preferences);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn states_for_users(state: &OmegaState, users: &[crate::models::User]) -> HashMap<i64, UserStatus> {
|
|
||||||
users
|
|
||||||
.iter()
|
|
||||||
.map(|user| {
|
|
||||||
let status = user
|
|
||||||
.iota_id
|
|
||||||
.map(|iota_id| state.presence.resolve_public_state(user.id.0, iota_id.0))
|
|
||||||
.unwrap_or(UserStatus::user_offline);
|
|
||||||
(user.id.0, status)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn changed_states(
|
|
||||||
state: &OmegaState,
|
|
||||||
before: &HashMap<i64, UserStatus>,
|
|
||||||
users: &[crate::models::User],
|
|
||||||
) -> Vec<(i64, UserStatus)> {
|
|
||||||
let mut changes = users
|
|
||||||
.iter()
|
|
||||||
.filter_map(|user| {
|
|
||||||
let after = user
|
|
||||||
.iota_id
|
|
||||||
.map(|iota_id| state.presence.resolve_public_state(user.id.0, iota_id.0))
|
|
||||||
.unwrap_or(UserStatus::user_offline);
|
|
||||||
(before.get(&user.id.0) != Some(&after)).then_some((user.id.0, after))
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
changes.sort_by_key(|(user_id, _)| *user_id);
|
|
||||||
changes.dedup_by_key(|(user_id, _)| *user_id);
|
|
||||||
changes
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state_notification(
|
|
||||||
subscriber: &crate::sql::user_online_tracker::PresenceSubscriber,
|
|
||||||
user_id: i64,
|
|
||||||
user_state: &UserStatus,
|
|
||||||
) -> CommunicationValue {
|
|
||||||
CommunicationValue::new(CommunicationType::ClientChanged)
|
|
||||||
.with_receiver(subscriber.user_id as u64)
|
|
||||||
.add_typed_default(
|
|
||||||
DataType::SessionId,
|
|
||||||
DataValue::SignedNumber(subscriber.session_id.into()),
|
|
||||||
)
|
|
||||||
.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into()))
|
|
||||||
.add_typed_default(DataType::UserState, DataValue::Str(user_state.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn publish_state_changes(state: &OmegaState, changes: &[(i64, UserStatus)]) {
|
|
||||||
let mut grouped = BTreeMap::<i64, Vec<CommunicationValue>>::new();
|
|
||||||
for (user_id, user_state) in changes {
|
|
||||||
for subscriber in state.presence.subscribers(*user_id) {
|
|
||||||
grouped
|
|
||||||
.entry(subscriber.omikron_id)
|
|
||||||
.or_default()
|
|
||||||
.push(state_notification(&subscriber, *user_id, user_state));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (omikron_id, notifications) in grouped {
|
|
||||||
if let Err(error) =
|
|
||||||
crate::transport::omikron_manager::send_state_batch(omikron_id, notifications).await
|
|
||||||
{
|
|
||||||
log_in!(
|
|
||||||
crate::util::logger::PrintType::General,
|
|
||||||
"Failed to deliver presence state batch to Omikron {}: {}",
|
|
||||||
omikron_id,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn publish_changed_states(
|
|
||||||
state: &OmegaState,
|
|
||||||
before: &HashMap<i64, UserStatus>,
|
|
||||||
users: &[crate::models::User],
|
|
||||||
) {
|
|
||||||
publish_state_changes(state, &changed_states(state, before, users)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn state_subscribe(
|
|
||||||
state: Arc<OmegaState>,
|
|
||||||
connection: Arc<OmikronConnection>,
|
|
||||||
value: CommunicationValue,
|
value: CommunicationValue,
|
||||||
omikron_id: i64,
|
omikron_id: i64,
|
||||||
) -> OmikronResult<()> {
|
) -> OmikronResult<()> {
|
||||||
let (user_id, session_id, user_ids) = match parse_subscription(&value) {
|
log_in!(crate::util::logger::PrintType::Omega, "User connected");
|
||||||
Ok(subscription) => subscription,
|
if let Some(user_id) = value.get_data(DataType::UserId).as_number() {
|
||||||
Err("user_id") => {
|
let status = value
|
||||||
return connection
|
.get_data(DataType::UserState)
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorNoUserId)
|
.as_str()
|
||||||
.await;
|
.and_then(UserStatus::from_str)
|
||||||
|
.unwrap_or(UserStatus::user_online);
|
||||||
|
if let Ok(user_id) = i64::try_from(user_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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(detail) => {
|
|
||||||
return connection
|
|
||||||
.send_error_response_with_detail(
|
|
||||||
value.require_id()?,
|
|
||||||
CommunicationType::ErrorInvalidData,
|
|
||||||
detail,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if !state.presence.owns_session(user_id, session_id, omikron_id) {
|
|
||||||
return connection
|
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorNoIota)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
state
|
Ok(())
|
||||||
.presence
|
|
||||||
.replace_subscription(user_id, session_id, omikron_id, user_ids);
|
|
||||||
connection
|
|
||||||
.send(&CommunicationValue::new(CommunicationType::Success).with_id(value.require_id()?))
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Legacy state-change contract used by Omegas that predate SetUserState.
|
pub async fn user_disconnected(
|
||||||
/// The payload is ClientChanged with UserId and UserState only.
|
_: Arc<OmikronConnection>,
|
||||||
pub async fn client_changed_legacy(
|
value: CommunicationValue,
|
||||||
state: Arc<OmegaState>,
|
omikron_id: i64,
|
||||||
|
) -> OmikronResult<()> {
|
||||||
|
log_in!(crate::util::logger::PrintType::Omega, "User disconnected");
|
||||||
|
if let Some(user_id) = value.get_data(DataType::UserId).as_number() {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn client_changed(
|
||||||
_: Arc<OmikronConnection>,
|
_: Arc<OmikronConnection>,
|
||||||
value: CommunicationValue,
|
value: CommunicationValue,
|
||||||
_: i64,
|
_: i64,
|
||||||
|
|
@ -178,136 +72,29 @@ pub async fn client_changed_legacy(
|
||||||
.get_data(DataType::UserId)
|
.get_data(DataType::UserId)
|
||||||
.as_number()
|
.as_number()
|
||||||
.and_then(|id| i64::try_from(id).ok())
|
.and_then(|id| i64::try_from(id).ok())
|
||||||
.filter(|id| *id > 0)
|
|
||||||
else {
|
else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
let Some(status) = value
|
let Some(status) = value
|
||||||
.get_data(DataType::UserState)
|
.get_data(DataType::UserState)
|
||||||
.as_str()
|
.as_str()
|
||||||
.and_then(UserStatus::from_client_preference)
|
.and_then(UserStatus::from_str)
|
||||||
else {
|
else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
state.presence.set_preference(user_id, status);
|
// 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn user_connected(
|
|
||||||
state: Arc<OmegaState>,
|
|
||||||
connection: Arc<OmikronConnection>,
|
|
||||||
value: CommunicationValue,
|
|
||||||
omikron_id: i64,
|
|
||||||
) -> OmikronResult<()> {
|
|
||||||
log_in!(crate::util::logger::PrintType::Omega, "User connected");
|
|
||||||
let Some(user_id) = value
|
|
||||||
.get_data(DataType::UserId)
|
|
||||||
.as_number()
|
|
||||||
.and_then(|id| i64::try_from(id).ok())
|
|
||||||
.filter(|id| *id > 0)
|
|
||||||
else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
let Some(session_id) = value
|
|
||||||
.get_data(DataType::SessionId)
|
|
||||||
.as_number()
|
|
||||||
.and_then(|id| i64::try_from(id).ok())
|
|
||||||
.filter(|id| *id > 0)
|
|
||||||
else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
let Some(iota_id) = value
|
|
||||||
.get_data(DataType::IotaId)
|
|
||||||
.as_number()
|
|
||||||
.and_then(|id| i64::try_from(id).ok())
|
|
||||||
.filter(|id| *id > 0)
|
|
||||||
else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
let user = match user_repo::get_by_user_id(user_id.into()).await {
|
|
||||||
Ok(user) => user,
|
|
||||||
Err(crate::error::OmegaError::NotFound) => {
|
|
||||||
return connection
|
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorNotFound)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
Err(error) => return Err(error),
|
|
||||||
};
|
|
||||||
let preferences = match user_repo::get_presence_preferences(&[user_id]).await {
|
|
||||||
Ok(preferences) => preferences,
|
|
||||||
Err(error) => return Err(error),
|
|
||||||
};
|
|
||||||
if user.iota_id.map(|id| id.0) != Some(iota_id) || !state.presence.has_iota_route(iota_id) {
|
|
||||||
return connection
|
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorNoIota)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
apply_preferences(&state, preferences);
|
|
||||||
let users = [user];
|
|
||||||
let before = states_for_users(&state, &users);
|
|
||||||
state
|
|
||||||
.presence
|
|
||||||
.track_session(user_id, session_id, omikron_id, iota_id);
|
|
||||||
publish_changed_states(&state, &before, &users).await;
|
|
||||||
connection
|
|
||||||
.send(&CommunicationValue::new(CommunicationType::Success).with_id(value.require_id()?))
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn user_disconnected(
|
|
||||||
state: Arc<OmegaState>,
|
|
||||||
connection: Arc<OmikronConnection>,
|
|
||||||
value: CommunicationValue,
|
|
||||||
omikron_id: i64,
|
|
||||||
) -> OmikronResult<()> {
|
|
||||||
log_in!(crate::util::logger::PrintType::Omega, "User disconnected");
|
|
||||||
let Some(user_id) = value
|
|
||||||
.get_data(DataType::UserId)
|
|
||||||
.as_number()
|
|
||||||
.and_then(|id| i64::try_from(id).ok())
|
|
||||||
.filter(|id| *id > 0)
|
|
||||||
else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
let Some(session_id) = value
|
|
||||||
.get_data(DataType::SessionId)
|
|
||||||
.as_number()
|
|
||||||
.and_then(|id| i64::try_from(id).ok())
|
|
||||||
.filter(|id| *id > 0)
|
|
||||||
else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
if let Ok(user) = user_repo::get_by_user_id(user_id.into()).await {
|
|
||||||
let preferences = user_repo::get_presence_preferences(&[user_id]).await?;
|
|
||||||
apply_preferences(&state, preferences);
|
|
||||||
let users = [user];
|
|
||||||
let before = states_for_users(&state, &users);
|
|
||||||
state
|
|
||||||
.presence
|
|
||||||
.remove_session(user_id, session_id, omikron_id);
|
|
||||||
publish_changed_states(&state, &before, &users).await;
|
|
||||||
} else {
|
|
||||||
state
|
|
||||||
.presence
|
|
||||||
.remove_session(user_id, session_id, omikron_id);
|
|
||||||
}
|
|
||||||
connection
|
|
||||||
.send(&CommunicationValue::new(CommunicationType::Success).with_id(value.require_id()?))
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn iota_connected(
|
pub async fn iota_connected(
|
||||||
state: Arc<OmegaState>,
|
|
||||||
connection: Arc<OmikronConnection>,
|
connection: Arc<OmikronConnection>,
|
||||||
value: CommunicationValue,
|
value: CommunicationValue,
|
||||||
omikron_id: i64,
|
omikron_id: i64,
|
||||||
|
|
@ -316,36 +103,38 @@ pub async fn iota_connected(
|
||||||
let Some(iota_id) = value
|
let Some(iota_id) = value
|
||||||
.get_data(DataType::IotaId)
|
.get_data(DataType::IotaId)
|
||||||
.as_number()
|
.as_number()
|
||||||
.and_then(|id| i64::try_from(id).ok())
|
.map(|id| id as i64)
|
||||||
.filter(|id| *id > 0)
|
|
||||||
else {
|
else {
|
||||||
return connection
|
return Ok(());
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
};
|
};
|
||||||
let users = user_repo::get_users_by_iota_id(IotaId::from(iota_id)).await?;
|
user_online_tracker::track_iota_connection(iota_id, omikron_id, true);
|
||||||
let ids = users.iter().map(|user| user.id.0).collect::<Vec<_>>();
|
let mut user_ids = Vec::new();
|
||||||
apply_preferences(&state, user_repo::get_presence_preferences(&ids).await?);
|
match user_repo::get_users_by_iota_id(IotaId::from(iota_id)).await {
|
||||||
let before = states_for_users(&state, &users);
|
Ok(users) => {
|
||||||
state.presence.connect_iota(iota_id, omikron_id);
|
for user in users {
|
||||||
let user_ids = users
|
user_ids.push(DataValue::SignedNumber(user.id.0.into()));
|
||||||
.iter()
|
user_online_tracker::track_user_status(
|
||||||
.map(|user| DataValue::SignedNumber(user.id.0.into()))
|
user.id.0,
|
||||||
.collect();
|
UserStatus::user_offline,
|
||||||
|
omikron_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => log_in!(
|
||||||
|
crate::util::logger::PrintType::General,
|
||||||
|
"SQL error loading users for IOTA"
|
||||||
|
),
|
||||||
|
}
|
||||||
let response = CommunicationValue::new(CommunicationType::IotaUserData)
|
let response = CommunicationValue::new(CommunicationType::IotaUserData)
|
||||||
|
.with_id(value.get_id())
|
||||||
.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));
|
||||||
connection.clone().send(&response).await?;
|
let _ = connection.send(&response).await;
|
||||||
crate::transport::omikron_manager::deliver_pending_erasures(iota_id).await;
|
Ok(())
|
||||||
publish_changed_states(&state, &before, &users).await;
|
|
||||||
connection
|
|
||||||
.send(&CommunicationValue::new(CommunicationType::Success).with_id(value.require_id()?))
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn iota_disconnected(
|
pub async fn iota_disconnected(
|
||||||
state: Arc<OmegaState>,
|
_: Arc<OmikronConnection>,
|
||||||
connection: Arc<OmikronConnection>,
|
|
||||||
value: CommunicationValue,
|
value: CommunicationValue,
|
||||||
omikron_id: i64,
|
omikron_id: i64,
|
||||||
) -> OmikronResult<()> {
|
) -> OmikronResult<()> {
|
||||||
|
|
@ -353,329 +142,42 @@ pub async fn iota_disconnected(
|
||||||
let Some(iota_id) = value
|
let Some(iota_id) = value
|
||||||
.get_data(DataType::IotaId)
|
.get_data(DataType::IotaId)
|
||||||
.as_number()
|
.as_number()
|
||||||
.and_then(|id| i64::try_from(id).ok())
|
.map(|id| id as i64)
|
||||||
.filter(|id| *id > 0)
|
|
||||||
else {
|
else {
|
||||||
return connection
|
return Ok(());
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
};
|
};
|
||||||
let users = user_repo::get_users_by_iota_id(IotaId::from(iota_id)).await?;
|
if user_online_tracker::untrack_iota_connection(iota_id, omikron_id) {
|
||||||
let ids = users.iter().map(|user| user.id.0).collect::<Vec<_>>();
|
if let Ok(users) = user_repo::get_users_by_iota_id(IotaId::from(iota_id)).await {
|
||||||
apply_preferences(&state, user_repo::get_presence_preferences(&ids).await?);
|
user_online_tracker::untrack_many_users(
|
||||||
let before = states_for_users(&state, &users);
|
&users.iter().map(|user| user.id.0).collect::<Vec<_>>(),
|
||||||
state.presence.untrack_iota_connection(iota_id, omikron_id);
|
);
|
||||||
publish_changed_states(&state, &before, &users).await;
|
}
|
||||||
connection
|
}
|
||||||
.send(&CommunicationValue::new(CommunicationType::Success).with_id(value.require_id()?))
|
Ok(())
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn sync_status(
|
pub async fn sync_status(
|
||||||
state: Arc<OmegaState>,
|
_: Arc<OmikronConnection>,
|
||||||
connection: Arc<OmikronConnection>,
|
|
||||||
value: CommunicationValue,
|
value: CommunicationValue,
|
||||||
omikron_id: i64,
|
omikron_id: i64,
|
||||||
) -> OmikronResult<()> {
|
) -> OmikronResult<()> {
|
||||||
let request_id = value.require_id()?;
|
if let DataValue::Array(ids) = value.get_data(DataType::UserIds) {
|
||||||
let Some(DataValue::Array(iota_values)) = value.get_data(DataType::IotaIds) else {
|
for id in ids {
|
||||||
return connection
|
if let DataValue::SignedNumber(id) = id {
|
||||||
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
|
user_online_tracker::track_user_status(
|
||||||
.await;
|
*id as i64,
|
||||||
};
|
UserStatus::user_offline,
|
||||||
let Some(DataValue::Array(session_values)) = value.get_data(DataType::UserStates) else {
|
omikron_id,
|
||||||
return connection
|
);
|
||||||
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut iota_ids = Vec::with_capacity(iota_values.len());
|
|
||||||
for item in iota_values {
|
|
||||||
let DataValue::SignedNumber(id) = item else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
let Ok(id) = i64::try_from(*id) else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
if id <= 0 {
|
|
||||||
return connection
|
|
||||||
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
if !iota_ids.contains(&id) {
|
|
||||||
iota_ids.push(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !connection.peer_capabilities().session_snapshot_v1 {
|
|
||||||
let Some(DataValue::Array(user_values)) = value.get_data(DataType::UserIds) else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
let mut user_ids = Vec::with_capacity(user_values.len());
|
|
||||||
for item in user_values {
|
|
||||||
let DataValue::SignedNumber(user_id) = item else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
let Ok(user_id) = i64::try_from(*user_id) else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
if user_id <= 0 {
|
|
||||||
return connection
|
|
||||||
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
if !user_ids.contains(&user_id) {
|
|
||||||
user_ids.push(user_id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let previous_iota_ids = state.presence.iota_ids_owned_by(omikron_id);
|
|
||||||
let affected_iota_ids = previous_iota_ids
|
|
||||||
.iter()
|
|
||||||
.chain(iota_ids.iter())
|
|
||||||
.copied()
|
|
||||||
.collect::<HashSet<_>>();
|
|
||||||
let users = user_repo::get_users_by_ids(&user_ids).await?;
|
|
||||||
let returned_user_ids = users.iter().map(|user| user.id.0).collect::<Vec<_>>();
|
|
||||||
apply_preferences(
|
|
||||||
&state,
|
|
||||||
user_repo::get_presence_preferences(&returned_user_ids).await?,
|
|
||||||
);
|
|
||||||
let before = states_for_users(&state, &users);
|
|
||||||
state
|
|
||||||
.presence
|
|
||||||
.replace_omikron_snapshot(omikron_id, &iota_ids, &[]);
|
|
||||||
let affected_users = user_repo::get_users_by_ids_and_iota_ids(
|
|
||||||
&returned_user_ids,
|
|
||||||
&affected_iota_ids.iter().copied().collect::<Vec<_>>(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
publish_changed_states(&state, &before, &affected_users).await;
|
|
||||||
return connection
|
|
||||||
.send(&CommunicationValue::new(CommunicationType::Success).with_id(request_id))
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
|
if let DataValue::Array(ids) = value.get_data(DataType::IotaIds) {
|
||||||
let tm = mtp::type_map::TypeMap::latest();
|
for id in ids {
|
||||||
let mut sessions = Vec::with_capacity(session_values.len());
|
if let DataValue::SignedNumber(id) = id {
|
||||||
for item in session_values {
|
user_online_tracker::track_iota_connection(*id as i64, omikron_id, true);
|
||||||
let (user_id, session_id, iota_id) = if connection.peer_capabilities().session_snapshot_v1 {
|
}
|
||||||
let DataValue::Container(entries) = item else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
let find = |kind| {
|
|
||||||
entries.iter().find_map(|(key, value)| {
|
|
||||||
(Some(*key) == DataType::try_to_id(kind, &tm)).then_some(value)
|
|
||||||
})
|
|
||||||
};
|
|
||||||
let (
|
|
||||||
Some(DataValue::SignedNumber(user_id)),
|
|
||||||
Some(DataValue::SignedNumber(session_id)),
|
|
||||||
Some(DataValue::SignedNumber(iota_id)),
|
|
||||||
) = (
|
|
||||||
find(DataType::UserId),
|
|
||||||
find(DataType::SessionId),
|
|
||||||
find(DataType::IotaId),
|
|
||||||
)
|
|
||||||
else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
(*user_id, *session_id, *iota_id)
|
|
||||||
} else {
|
|
||||||
let DataValue::Array(values) = item else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
let [
|
|
||||||
DataValue::SignedNumber(user_id),
|
|
||||||
DataValue::SignedNumber(session_id),
|
|
||||||
DataValue::SignedNumber(iota_id),
|
|
||||||
] = values.as_slice()
|
|
||||||
else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
(*user_id, *session_id, *iota_id)
|
|
||||||
};
|
|
||||||
let (Ok(user_id), Ok(session_id), Ok(iota_id)) = (
|
|
||||||
i64::try_from(user_id),
|
|
||||||
i64::try_from(session_id),
|
|
||||||
i64::try_from(iota_id),
|
|
||||||
) else {
|
|
||||||
return connection
|
|
||||||
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
if user_id <= 0 || session_id <= 0 || iota_id <= 0 || !iota_ids.contains(&iota_id) {
|
|
||||||
return connection
|
|
||||||
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
if sessions.iter().any(|(existing_user, existing_session, _)| {
|
|
||||||
*existing_user == user_id && *existing_session == session_id
|
|
||||||
}) {
|
|
||||||
return connection
|
|
||||||
.send_error_response(request_id, CommunicationType::ErrorInvalidData)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
sessions.push((user_id, session_id, iota_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
let previous_iota_ids = state.presence.iota_ids_owned_by(omikron_id);
|
|
||||||
let previous_session_user_ids = state
|
|
||||||
.presence
|
|
||||||
.sessions_owned_by(omikron_id)
|
|
||||||
.into_iter()
|
|
||||||
.map(|(user_id, _, _)| user_id)
|
|
||||||
.collect::<HashSet<_>>();
|
|
||||||
let new_session_user_ids = sessions
|
|
||||||
.iter()
|
|
||||||
.map(|(user_id, _, _)| *user_id)
|
|
||||||
.collect::<HashSet<_>>();
|
|
||||||
let affected_iota_ids = previous_iota_ids
|
|
||||||
.iter()
|
|
||||||
.chain(iota_ids.iter())
|
|
||||||
.copied()
|
|
||||||
.collect::<HashSet<_>>();
|
|
||||||
let users = user_repo::get_users_by_ids_and_iota_ids(
|
|
||||||
&previous_session_user_ids
|
|
||||||
.iter()
|
|
||||||
.chain(new_session_user_ids.iter())
|
|
||||||
.copied()
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
&affected_iota_ids.iter().copied().collect::<Vec<_>>(),
|
|
||||||
)
|
|
||||||
.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<_>>();
|
|
||||||
apply_preferences(
|
|
||||||
&state,
|
|
||||||
user_repo::get_presence_preferences(&user_ids).await?,
|
|
||||||
);
|
|
||||||
let before = states_for_users(&state, &users);
|
|
||||||
state
|
|
||||||
.presence
|
|
||||||
.replace_omikron_snapshot(omikron_id, &iota_ids, &sessions);
|
|
||||||
publish_changed_states(&state, &before, &users).await;
|
|
||||||
connection
|
|
||||||
.send(&CommunicationValue::new(CommunicationType::Success).with_id(request_id))
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn omikron_disconnected(state: Arc<OmegaState>, omikron_id: i64) {
|
|
||||||
let iota_ids = state.presence.iota_ids_owned_by(omikron_id);
|
|
||||||
let session_user_ids = state
|
|
||||||
.presence
|
|
||||||
.sessions_owned_by(omikron_id)
|
|
||||||
.into_iter()
|
|
||||||
.map(|(user_id, _, _)| user_id)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let users = match user_repo::get_users_by_ids_and_iota_ids(&session_user_ids, &iota_ids).await {
|
|
||||||
Ok(users) => users,
|
|
||||||
Err(error) => {
|
|
||||||
log_in!(
|
|
||||||
crate::util::logger::PrintType::General,
|
|
||||||
"Failed to load users before Omikron {} cleanup: {}",
|
|
||||||
omikron_id,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let user_ids = users.iter().map(|user| user.id.0).collect::<Vec<_>>();
|
|
||||||
if let Err(error) = user_repo::get_presence_preferences(&user_ids)
|
|
||||||
.await
|
|
||||||
.map(|preferences| apply_preferences(&state, preferences))
|
|
||||||
{
|
|
||||||
log_in!(
|
|
||||||
crate::util::logger::PrintType::General,
|
|
||||||
"Failed to load preferences before Omikron {} cleanup: {}",
|
|
||||||
omikron_id,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let before = states_for_users(&state, &users);
|
|
||||||
let removed = state.presence.remove_omikron(omikron_id);
|
|
||||||
debug_assert_eq!(removed.iota_ids, {
|
|
||||||
let mut ids = iota_ids.clone();
|
|
||||||
ids.sort_unstable();
|
|
||||||
ids.dedup();
|
|
||||||
ids
|
|
||||||
});
|
|
||||||
publish_changed_states(&state, &before, &users).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::parse_subscription;
|
|
||||||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
|
||||||
|
|
||||||
fn request(user_ids: DataValue) -> CommunicationValue {
|
|
||||||
CommunicationValue::new(CommunicationType::StateSubscribe)
|
|
||||||
.with_sender(7)
|
|
||||||
.add_typed_default(DataType::SessionId, DataValue::SignedNumber(11))
|
|
||||||
.add_typed_default(DataType::UserIds, user_ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn subscription_parser_deduplicates_valid_targets() {
|
|
||||||
let parsed = parse_subscription(&request(DataValue::Array(vec![
|
|
||||||
DataValue::SignedNumber(20),
|
|
||||||
DataValue::SignedNumber(21),
|
|
||||||
DataValue::SignedNumber(20),
|
|
||||||
])))
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(parsed, (7, 11, vec![20, 21]));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn subscription_parser_rejects_missing_or_malformed_fields() {
|
|
||||||
let missing_users = CommunicationValue::new(CommunicationType::StateSubscribe)
|
|
||||||
.with_sender(7)
|
|
||||||
.add_typed_default(DataType::SessionId, DataValue::SignedNumber(11));
|
|
||||||
assert_eq!(parse_subscription(&missing_users), Err("user_ids"));
|
|
||||||
|
|
||||||
let malformed_users = request(DataValue::Array(vec![DataValue::Str("bad".into())]));
|
|
||||||
assert_eq!(parse_subscription(&malformed_users), Err("user_ids"));
|
|
||||||
|
|
||||||
let invalid_session = CommunicationValue::new(CommunicationType::StateSubscribe)
|
|
||||||
.with_sender(7)
|
|
||||||
.add_typed_default(DataType::SessionId, DataValue::SignedNumber(0))
|
|
||||||
.add_typed_default(
|
|
||||||
DataType::UserIds,
|
|
||||||
DataValue::Array(vec![DataValue::SignedNumber(20)]),
|
|
||||||
);
|
|
||||||
assert_eq!(parse_subscription(&invalid_session), Err("session_id"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty_subscription_is_valid_and_authoritative() {
|
|
||||||
let parsed = parse_subscription(&request(DataValue::Array(Vec::new()))).unwrap();
|
|
||||||
assert_eq!(parsed, (7, 11, Vec::new()));
|
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
use super::super::connection::{
|
use super::super::omikron_connection::{OmikronConnection, OmikronResult};
|
||||||
OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields,
|
|
||||||
};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{iota_repo, user_repo},
|
db::{iota_repo, user_repo},
|
||||||
models::{IotaId, UserId},
|
models::{IotaId, UserId},
|
||||||
|
|
@ -22,13 +20,13 @@ pub async fn get_register(
|
||||||
.filter(|id| user_repo::valid_protocol_id(*id));
|
.filter(|id| user_repo::valid_protocol_id(*id));
|
||||||
let Some(iota_id) = iota_id else {
|
let Some(iota_id) = iota_id else {
|
||||||
return connection
|
return connection
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
|
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidData)
|
||||||
.await;
|
.await;
|
||||||
};
|
};
|
||||||
let (register_id, registration_token) =
|
let (register_id, registration_token) =
|
||||||
user_repo::allocate_registration(IotaId::from(iota_id), value.require_id()?).await?;
|
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.require_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()),
|
||||||
|
|
@ -47,7 +45,7 @@ pub async fn complete_iota(
|
||||||
.and_then(|key| PublicKeyBundle::from_base64(key).ok());
|
.and_then(|key| PublicKeyBundle::from_base64(key).ok());
|
||||||
let Some(public_key) = public_key else {
|
let Some(public_key) = public_key else {
|
||||||
return connection
|
return connection
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
|
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidData)
|
||||||
.await;
|
.await;
|
||||||
};
|
};
|
||||||
match iota_repo::create_new_iota(public_key).await {
|
match iota_repo::create_new_iota(public_key).await {
|
||||||
|
|
@ -55,7 +53,7 @@ pub async fn complete_iota(
|
||||||
connection
|
connection
|
||||||
.send(
|
.send(
|
||||||
&CommunicationValue::new(CommunicationType::CompleteRegisterIota)
|
&CommunicationValue::new(CommunicationType::CompleteRegisterIota)
|
||||||
.with_id(value.require_id()?)
|
.with_id(value.get_id())
|
||||||
.add_typed_default(DataType::IotaId, DataValue::SignedNumber(id.0.into())),
|
.add_typed_default(DataType::IotaId, DataValue::SignedNumber(id.0.into())),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|
@ -64,7 +62,7 @@ pub async fn complete_iota(
|
||||||
connection
|
connection
|
||||||
.send(
|
.send(
|
||||||
&CommunicationValue::new(CommunicationType::ErrorInternal)
|
&CommunicationValue::new(CommunicationType::ErrorInternal)
|
||||||
.with_id(value.require_id()?)
|
.with_id(value.get_id())
|
||||||
.add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())),
|
.add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|
@ -108,7 +106,7 @@ pub async fn complete_user(
|
||||||
})
|
})
|
||||||
else {
|
else {
|
||||||
return connection
|
return connection
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
|
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidData)
|
||||||
.await;
|
.await;
|
||||||
};
|
};
|
||||||
// Omikron supplies the authenticated Iota ID in the payload. The lease
|
// Omikron supplies the authenticated Iota ID in the payload. The lease
|
||||||
|
|
@ -120,7 +118,7 @@ pub async fn complete_user(
|
||||||
.filter(|id| user_repo::valid_protocol_id(*id));
|
.filter(|id| user_repo::valid_protocol_id(*id));
|
||||||
let Some(iota_id) = iota_id else {
|
let Some(iota_id) = iota_id else {
|
||||||
return connection
|
return connection
|
||||||
.send_error_response(value.require_id()?, CommunicationType::ErrorInvalidData)
|
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidData)
|
||||||
.await;
|
.await;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -135,29 +133,15 @@ 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).with_id(value.get_id()))
|
||||||
&CommunicationValue::new(CommunicationType::Success)
|
|
||||||
.with_id(value.require_id()?),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
connection
|
connection
|
||||||
.send(
|
.send(
|
||||||
&CommunicationValue::new(CommunicationType::ErrorInternal)
|
&CommunicationValue::new(CommunicationType::ErrorInternal)
|
||||||
.with_id(value.require_id()?)
|
.with_id(value.get_id())
|
||||||
.add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())),
|
.add_typed_default(DataType::ErrorType, DataValue::Str(error.to_string())),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -1,149 +1,46 @@
|
||||||
use super::super::connection::{
|
use super::super::omikron_connection::{OmikronConnection, OmikronResult};
|
||||||
OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields,
|
use crate::sql::{connection_status::UserStatus, user_online_tracker};
|
||||||
};
|
|
||||||
use crate::db::user_repo;
|
|
||||||
use mtp::{
|
use mtp::{
|
||||||
codec::{CommunicationType, CommunicationValue, DataType, DataValue},
|
codec::{CommunicationType, CommunicationValue, DataType, DataValue},
|
||||||
type_map::TypeMap,
|
type_map::TypeMap,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::sync::Arc;
|
||||||
collections::{HashMap, HashSet},
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
async fn send_error(
|
|
||||||
connection: Arc<OmikronConnection>,
|
|
||||||
request_id: u32,
|
|
||||||
error_type: CommunicationType,
|
|
||||||
session_id: Option<i128>,
|
|
||||||
) -> OmikronResult<()> {
|
|
||||||
let mut response = CommunicationValue::new(error_type).with_id(request_id);
|
|
||||||
if let Some(session_id) = session_id {
|
|
||||||
response =
|
|
||||||
response.add_typed_default(DataType::SessionId, DataValue::SignedNumber(session_id));
|
|
||||||
}
|
|
||||||
connection.send(&response).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get(
|
pub async fn get(
|
||||||
connection: Arc<OmikronConnection>,
|
connection: Arc<OmikronConnection>,
|
||||||
value: CommunicationValue,
|
value: CommunicationValue,
|
||||||
) -> OmikronResult<()> {
|
) -> OmikronResult<()> {
|
||||||
let state = connection.state();
|
let DataValue::Array(ids) = value.get_data(DataType::UserIds) else {
|
||||||
let legacy_peer = !connection.peer_capabilities().client_state_push_v1;
|
return Ok(());
|
||||||
let Some(DataValue::Array(ids)) = value.get_data(DataType::UserIds) else {
|
|
||||||
return send_error(
|
|
||||||
connection,
|
|
||||||
value.require_id()?,
|
|
||||||
CommunicationType::ErrorInvalidData,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
};
|
};
|
||||||
let session_id = value
|
|
||||||
.get_data(DataType::SessionId)
|
|
||||||
.as_number()
|
|
||||||
.filter(|id| *id > 0);
|
|
||||||
if session_id.is_none() && !legacy_peer {
|
|
||||||
return send_error(
|
|
||||||
connection,
|
|
||||||
value.require_id()?,
|
|
||||||
CommunicationType::ErrorInvalidData,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
let tm = TypeMap::latest();
|
let tm = TypeMap::latest();
|
||||||
let mut requested_user_ids = Vec::new();
|
let states = ids
|
||||||
let mut requested_set = HashSet::new();
|
.iter()
|
||||||
for id in ids {
|
.filter_map(|id| {
|
||||||
let DataValue::SignedNumber(id) = id else {
|
let DataValue::SignedNumber(id) = id else {
|
||||||
return send_error(
|
return None;
|
||||||
connection,
|
};
|
||||||
value.require_id()?,
|
let status = user_online_tracker::get_user_status(*id as i64)
|
||||||
CommunicationType::ErrorInvalidData,
|
.map(|status| {
|
||||||
session_id,
|
if status.connection_type == UserStatus::user_invisible {
|
||||||
)
|
UserStatus::user_offline.to_string()
|
||||||
.await;
|
} else {
|
||||||
};
|
status.connection_type.to_string()
|
||||||
let Ok(user_id) = i64::try_from(*id) else {
|
}
|
||||||
return send_error(
|
})
|
||||||
connection,
|
.unwrap_or_else(|| UserStatus::iota_offline.to_string());
|
||||||
value.require_id()?,
|
let mut map = Vec::new();
|
||||||
CommunicationType::ErrorInvalidData,
|
if let Some(kind) = DataType::UserId.try_to_id(&tm) {
|
||||||
session_id,
|
map.push((kind, DataValue::SignedNumber((*id as i64).into())));
|
||||||
)
|
}
|
||||||
.await;
|
if let Some(kind) = DataType::UserState.try_to_id(&tm) {
|
||||||
};
|
map.push((kind, DataValue::Str(status)));
|
||||||
if user_id <= 0 {
|
}
|
||||||
return send_error(
|
Some(DataValue::Container(map))
|
||||||
connection,
|
})
|
||||||
value.require_id()?,
|
.collect();
|
||||||
CommunicationType::ErrorInvalidData,
|
|
||||||
session_id,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
if !requested_set.insert(user_id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
requested_user_ids.push(user_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
let users = match user_repo::get_users_by_ids(&requested_user_ids).await {
|
|
||||||
Ok(users) => users,
|
|
||||||
Err(_) => {
|
|
||||||
return send_error(
|
|
||||||
connection,
|
|
||||||
value.require_id()?,
|
|
||||||
CommunicationType::ErrorInternal,
|
|
||||||
session_id,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let users_by_id: HashMap<_, _> = users.into_iter().map(|user| (user.id.0, user)).collect();
|
|
||||||
let mut states = Vec::new();
|
|
||||||
let mut missing_user_ids = Vec::new();
|
|
||||||
for user_id in requested_user_ids {
|
|
||||||
let Some(user) = users_by_id.get(&user_id) else {
|
|
||||||
missing_user_ids.push(user_id);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let status = user
|
|
||||||
.iota_id
|
|
||||||
.map(|iota_id| state.presence.resolve_public_state(user_id, iota_id.0))
|
|
||||||
.unwrap_or(crate::sql::connection_status::UserStatus::user_offline)
|
|
||||||
.to_string();
|
|
||||||
let mut map = Vec::new();
|
|
||||||
if let Some(kind) = DataType::UserId.try_to_id(&tm) {
|
|
||||||
map.push((kind, DataValue::SignedNumber(user_id.into())));
|
|
||||||
}
|
|
||||||
if let Some(kind) = DataType::UserState.try_to_id(&tm) {
|
|
||||||
map.push((kind, DataValue::Str(status)));
|
|
||||||
}
|
|
||||||
states.push(DataValue::Container(map));
|
|
||||||
}
|
|
||||||
let response = CommunicationValue::new(CommunicationType::GetStates)
|
let response = CommunicationValue::new(CommunicationType::GetStates)
|
||||||
.with_id(value.require_id()?)
|
.with_id(value.get_id())
|
||||||
.add_typed_default(DataType::UserStates, DataValue::Array(states));
|
.add_typed_default(DataType::UserStates, DataValue::Array(states));
|
||||||
let response = if let Some(session_id) = session_id {
|
|
||||||
response.add_typed_default(DataType::SessionId, DataValue::SignedNumber(session_id))
|
|
||||||
} else {
|
|
||||||
response
|
|
||||||
};
|
|
||||||
let response = if legacy_peer {
|
|
||||||
response
|
|
||||||
} else {
|
|
||||||
response.add_typed_default(
|
|
||||||
DataType::MissingUserIds,
|
|
||||||
DataValue::Array(
|
|
||||||
missing_user_ids
|
|
||||||
.into_iter()
|
|
||||||
.map(|id| DataValue::SignedNumber(id.into()))
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
connection.send(&response).await
|
connection.send(&response).await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
use super::super::connection::{
|
use super::super::omikron_connection::{OmikronConnection, OmikronResult};
|
||||||
OmikronConnection, OmikronResult, OptionalDataValueCompat, RequiredMtpFields,
|
|
||||||
};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{iota_repo, user_repo},
|
db::{iota_repo, user_repo},
|
||||||
models::{IotaId, UserId},
|
models::{IotaId, UserId},
|
||||||
|
sql::{connection_status::UserStatus, user_online_tracker},
|
||||||
};
|
};
|
||||||
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||||
use mtp::{
|
use mtp::{
|
||||||
|
|
@ -12,12 +11,9 @@ use mtp::{
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
fn connections(connection: &OmikronConnection, iota_id: i64) -> DataValue {
|
fn connections(iota_id: i64) -> DataValue {
|
||||||
DataValue::Array(
|
DataValue::Array(
|
||||||
connection
|
user_online_tracker::get_iota_omikron_connections(iota_id)
|
||||||
.state()
|
|
||||||
.presence
|
|
||||||
.iota_connections(iota_id)
|
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|id| DataValue::SignedNumber(id.into()))
|
.map(|id| DataValue::SignedNumber(id.into()))
|
||||||
|
|
@ -29,16 +25,10 @@ pub async fn get_user(
|
||||||
connection: Arc<OmikronConnection>,
|
connection: Arc<OmikronConnection>,
|
||||||
value: CommunicationValue,
|
value: CommunicationValue,
|
||||||
) -> OmikronResult<()> {
|
) -> OmikronResult<()> {
|
||||||
let request_id = value.require_id()?;
|
let user = if let Some(id) = value.get_data(DataType::UserId).as_number() {
|
||||||
let sender = value.require_sender_i64()?;
|
user_repo::get_by_user_id(UserId::from(id as i64))
|
||||||
let state = connection.state();
|
.await
|
||||||
let user = if let Some(id) = value
|
.ok()
|
||||||
.get_data(DataType::UserId)
|
|
||||||
.as_number()
|
|
||||||
.and_then(|id| i64::try_from(id).ok())
|
|
||||||
.filter(|id| *id > 0)
|
|
||||||
{
|
|
||||||
user_repo::get_by_user_id(UserId::from(id)).await.ok()
|
|
||||||
} else if let Some(name) = value.get_data(DataType::Username).as_str() {
|
} else if let Some(name) = value.get_data(DataType::Username).as_str() {
|
||||||
user_repo::get_by_username(name).await.ok()
|
user_repo::get_by_username(name).await.ok()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -46,28 +36,25 @@ pub async fn get_user(
|
||||||
};
|
};
|
||||||
let Some(user) = user else {
|
let Some(user) = user else {
|
||||||
return connection
|
return connection
|
||||||
.send_error_response(request_id, CommunicationType::ErrorNotFound)
|
.send_error_response(value.get_id(), CommunicationType::ErrorNotFound)
|
||||||
.await;
|
.await;
|
||||||
};
|
};
|
||||||
let id = user.id.0;
|
let id = user.id.0;
|
||||||
let iota_id = user.iota_id.map(|id| id.0);
|
let iota_id = user.iota_id.0;
|
||||||
let username = user.username.clone();
|
let username = user.username.clone();
|
||||||
let display = user
|
let display = user
|
||||||
.display
|
.display
|
||||||
.filter(|name| !name.is_empty())
|
.filter(|name| !name.is_empty())
|
||||||
.unwrap_or_else(|| username.clone());
|
.unwrap_or_else(|| username.clone());
|
||||||
let mut response = CommunicationValue::new(CommunicationType::GetUserData)
|
let mut response = CommunicationValue::new(CommunicationType::GetUserData)
|
||||||
.with_id(request_id)
|
.with_id(value.get_id())
|
||||||
.add_typed_default(DataType::Username, DataValue::Str(username))
|
.add_typed_default(DataType::Username, DataValue::Str(username))
|
||||||
.add_typed_default(
|
.add_typed_default(
|
||||||
DataType::PublicKey,
|
DataType::PublicKey,
|
||||||
DataValue::Str(user.public_key.try_to_base64()?),
|
DataValue::Str(user.public_key.to_base64()),
|
||||||
)
|
)
|
||||||
.add_typed_default(DataType::UserId, DataValue::SignedNumber(id.into()))
|
.add_typed_default(DataType::UserId, DataValue::SignedNumber(id.into()))
|
||||||
.add_typed_default(
|
.add_typed_default(DataType::IotaId, DataValue::SignedNumber(iota_id.into()))
|
||||||
DataType::CreatedAt,
|
|
||||||
DataValue::SignedNumber(user.created_at.into()),
|
|
||||||
)
|
|
||||||
.add_typed_default(DataType::Display, DataValue::Str(display))
|
.add_typed_default(DataType::Display, DataValue::Str(display))
|
||||||
.add_typed_default(
|
.add_typed_default(
|
||||||
DataType::SubLevel,
|
DataType::SubLevel,
|
||||||
|
|
@ -87,44 +74,28 @@ pub async fn get_user(
|
||||||
response =
|
response =
|
||||||
response.add_typed_default(DataType::Avatar, DataValue::Str(STANDARD.encode(avatar)));
|
response.add_typed_default(DataType::Avatar, DataValue::Str(STANDARD.encode(avatar)));
|
||||||
}
|
}
|
||||||
let route = state.presence.user_route(id);
|
let online = user_online_tracker::get_user_status(id);
|
||||||
let private_request = sender == id;
|
|
||||||
let resolved_status = if private_request {
|
|
||||||
if !state
|
|
||||||
.presence
|
|
||||||
.load_preference(id, &user.presence_preference)
|
|
||||||
{
|
|
||||||
crate::log_in!(
|
|
||||||
crate::util::logger::PrintType::General,
|
|
||||||
"Invalid persisted presence preference for user {}, using user_online",
|
|
||||||
id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
state.presence.resolve_private_state(id)
|
|
||||||
} else {
|
|
||||||
iota_id
|
|
||||||
.map(|iota_id| state.presence.resolve_public_state(id, iota_id))
|
|
||||||
.unwrap_or(crate::sql::connection_status::UserStatus::user_offline)
|
|
||||||
};
|
|
||||||
response = response
|
response = response
|
||||||
.add_typed_default(
|
.add_typed_default(
|
||||||
DataType::OnlineStatus,
|
DataType::OnlineStatus,
|
||||||
DataValue::Str(resolved_status.to_string()),
|
DataValue::Str(
|
||||||
|
online
|
||||||
|
.as_ref()
|
||||||
|
.map(|status| {
|
||||||
|
if status.connection_type == UserStatus::user_invisible {
|
||||||
|
UserStatus::user_offline.to_string()
|
||||||
|
} else {
|
||||||
|
status.connection_type.to_string()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| UserStatus::iota_offline.to_string()),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.add_typed_default(
|
.add_typed_default(DataType::OmikronConnections, connections(iota_id));
|
||||||
DataType::OmikronConnections,
|
if let Some(status) = online {
|
||||||
iota_id
|
|
||||||
.map(|iota_id| connections(&connection, iota_id))
|
|
||||||
.unwrap_or_else(|| DataValue::Array(Vec::new())),
|
|
||||||
);
|
|
||||||
if let Some(iota_id) = iota_id {
|
|
||||||
response =
|
|
||||||
response.add_typed_default(DataType::IotaId, DataValue::SignedNumber(iota_id.into()));
|
|
||||||
}
|
|
||||||
if let Some(route) = route {
|
|
||||||
response = response.add_typed_default(
|
response = response.add_typed_default(
|
||||||
DataType::OmikronId,
|
DataType::OmikronId,
|
||||||
DataValue::SignedNumber(route.omikron_id.into()),
|
DataValue::SignedNumber(status.omikron_id.into()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
connection.send(&response).await
|
connection.send(&response).await
|
||||||
|
|
@ -134,44 +105,33 @@ pub async fn get_iota(
|
||||||
connection: Arc<OmikronConnection>,
|
connection: Arc<OmikronConnection>,
|
||||||
value: CommunicationValue,
|
value: CommunicationValue,
|
||||||
) -> OmikronResult<()> {
|
) -> OmikronResult<()> {
|
||||||
let request_id = value.require_id()?;
|
let found = if let Some(id) = value.get_data(DataType::IotaId).as_number() {
|
||||||
let found = if let Some(id) = value
|
iota_repo::get_iota_by_id(IotaId::from(id as i64))
|
||||||
.get_data(DataType::IotaId)
|
|
||||||
.as_number()
|
|
||||||
.and_then(|id| i64::try_from(id).ok())
|
|
||||||
.filter(|id| *id > 0)
|
|
||||||
{
|
|
||||||
iota_repo::get_iota_by_id(IotaId::from(id))
|
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
.map(|iota| (iota.id.0, iota.public_key, None, None))
|
.map(|iota| (iota.id.0, iota.public_key, None, None))
|
||||||
} else if let Some(id) = value.get_data(DataType::UserId).as_number() {
|
} else if let Some(id) = value.get_data(DataType::UserId).as_number() {
|
||||||
if let Some(id) = i64::try_from(id).ok().filter(|id| *id > 0)
|
if let Ok(user) = user_repo::get_by_user_id(UserId::from(id as i64)).await {
|
||||||
&& let Ok(user) = user_repo::get_by_user_id(UserId::from(id)).await
|
iota_repo::get_iota_by_id(user.iota_id)
|
||||||
{
|
.await
|
||||||
match user.iota_id {
|
.ok()
|
||||||
Some(iota_id) => iota_repo::get_iota_by_id(iota_id)
|
.map(|iota| (iota.id.0, iota.public_key, Some(user.id.0), None))
|
||||||
.await
|
|
||||||
.ok()
|
|
||||||
.map(|iota| (iota.id.0, iota.public_key, Some(user.id.0), None)),
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
} else if let Some(name) = value.get_data(DataType::Username).as_str() {
|
} else if let Some(name) = value.get_data(DataType::Username).as_str() {
|
||||||
if let Ok(user) = user_repo::get_by_username(name).await {
|
if let Ok(user) = user_repo::get_by_username(name).await {
|
||||||
match user.iota_id {
|
iota_repo::get_iota_by_id(user.iota_id)
|
||||||
Some(iota_id) => iota_repo::get_iota_by_id(iota_id).await.ok().map(|iota| {
|
.await
|
||||||
|
.ok()
|
||||||
|
.map(|iota| {
|
||||||
(
|
(
|
||||||
iota.id.0,
|
iota.id.0,
|
||||||
iota.public_key,
|
iota.public_key,
|
||||||
Some(user.id.0),
|
Some(user.id.0),
|
||||||
Some(name.to_owned()),
|
Some(name.to_owned()),
|
||||||
)
|
)
|
||||||
}),
|
})
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -180,14 +140,14 @@ pub async fn get_iota(
|
||||||
};
|
};
|
||||||
let Some((id, key, user_id, username)) = found else {
|
let Some((id, key, user_id, username)) = found else {
|
||||||
return connection
|
return connection
|
||||||
.send_error_response(request_id, CommunicationType::ErrorNotFound)
|
.send_error_response(value.get_id(), CommunicationType::ErrorNotFound)
|
||||||
.await;
|
.await;
|
||||||
};
|
};
|
||||||
let mut response = CommunicationValue::new(CommunicationType::GetIotaData)
|
let mut response = CommunicationValue::new(CommunicationType::GetIotaData)
|
||||||
.with_id(request_id)
|
.with_id(value.get_id())
|
||||||
.add_typed_default(DataType::PublicKey, DataValue::Str(key.try_to_base64()?))
|
.add_typed_default(DataType::PublicKey, DataValue::Str(key.to_base64()))
|
||||||
.add_typed_default(DataType::IotaId, DataValue::SignedNumber(id.into()))
|
.add_typed_default(DataType::IotaId, DataValue::SignedNumber(id.into()))
|
||||||
.add_typed_default(DataType::OmikronConnections, connections(&connection, id));
|
.add_typed_default(DataType::OmikronConnections, connections(id));
|
||||||
if let Some(user_id) = user_id {
|
if let Some(user_id) = user_id {
|
||||||
response =
|
response =
|
||||||
response.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into()));
|
response.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into()));
|
||||||
|
|
@ -202,8 +162,7 @@ async fn update_user(
|
||||||
connection: Arc<OmikronConnection>,
|
connection: Arc<OmikronConnection>,
|
||||||
value: CommunicationValue,
|
value: CommunicationValue,
|
||||||
) -> OmikronResult<()> {
|
) -> OmikronResult<()> {
|
||||||
let request_id = value.require_id()?;
|
let id = UserId::from(value.get_sender() as i64);
|
||||||
let id = UserId::from(value.require_sender_i64()?);
|
|
||||||
let mut error = None;
|
let mut error = None;
|
||||||
if let Some(name) = value.get_data(DataType::Username).as_str() {
|
if let Some(name) = value.get_data(DataType::Username).as_str() {
|
||||||
error = user_repo::change_username(id, name.to_owned())
|
error = user_repo::change_username(id, name.to_owned())
|
||||||
|
|
@ -211,55 +170,56 @@ async fn update_user(
|
||||||
.err()
|
.err()
|
||||||
.map(|e| e.to_string());
|
.map(|e| e.to_string());
|
||||||
}
|
}
|
||||||
if error.is_none()
|
if error.is_none() {
|
||||||
&& let Some(name) = value.get_data(DataType::Display).as_str()
|
if let Some(name) = value.get_data(DataType::Display).as_str() {
|
||||||
{
|
error = user_repo::change_display_name(id, name.to_owned())
|
||||||
error = user_repo::change_display_name(id, name.to_owned())
|
.await
|
||||||
.await
|
.err()
|
||||||
.err()
|
.map(|e| e.to_string());
|
||||||
.map(|e| e.to_string());
|
}
|
||||||
}
|
}
|
||||||
if error.is_none()
|
if error.is_none() {
|
||||||
&& let Some(avatar) = value.get_data(DataType::Avatar).as_str()
|
if let Some(avatar) = value.get_data(DataType::Avatar).as_str() {
|
||||||
{
|
error = user_repo::change_avatar(id, avatar.to_owned())
|
||||||
error = user_repo::change_avatar(id, avatar.to_owned())
|
.await
|
||||||
.await
|
.err()
|
||||||
.err()
|
.map(|e| e.to_string());
|
||||||
.map(|e| e.to_string());
|
}
|
||||||
}
|
}
|
||||||
if error.is_none()
|
if error.is_none() {
|
||||||
&& let Some(about) = value.get_data(DataType::About).as_str()
|
if let Some(about) = value.get_data(DataType::About).as_str() {
|
||||||
{
|
error = user_repo::change_about(id, about.to_owned())
|
||||||
error = user_repo::change_about(id, about.to_owned())
|
.await
|
||||||
.await
|
.err()
|
||||||
.err()
|
.map(|e| e.to_string());
|
||||||
.map(|e| e.to_string());
|
}
|
||||||
}
|
}
|
||||||
if error.is_none()
|
if error.is_none() {
|
||||||
&& let Some(status) = value.get_data(DataType::Status).as_str()
|
if let Some(status) = value.get_data(DataType::Status).as_str() {
|
||||||
{
|
error = user_repo::change_status(id, status.to_owned())
|
||||||
error = user_repo::change_status(id, status.to_owned())
|
.await
|
||||||
.await
|
.err()
|
||||||
.err()
|
.map(|e| e.to_string());
|
||||||
.map(|e| e.to_string());
|
}
|
||||||
}
|
}
|
||||||
if error.is_none()
|
if error.is_none() {
|
||||||
&& let Some(key) = value
|
if let Some(key) = value
|
||||||
.get_data(DataType::PublicKey)
|
.get_data(DataType::PublicKey)
|
||||||
.as_str()
|
.as_str()
|
||||||
.and_then(|key| PublicKeyBundle::from_base64(key).ok())
|
.and_then(|key| PublicKeyBundle::from_base64(key).ok())
|
||||||
{
|
{
|
||||||
error = user_repo::change_keys(id, key)
|
error = user_repo::change_keys(id, key)
|
||||||
.await
|
.await
|
||||||
.err()
|
.err()
|
||||||
.map(|e| e.to_string());
|
.map(|e| e.to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let response = match error {
|
let response = match error {
|
||||||
None => CommunicationValue::new(CommunicationType::Success),
|
None => CommunicationValue::new(CommunicationType::Success),
|
||||||
Some(error) => CommunicationValue::new(CommunicationType::ErrorInternal)
|
Some(error) => CommunicationValue::new(CommunicationType::ErrorInternal)
|
||||||
.add_typed_default(DataType::ErrorType, DataValue::Str(error)),
|
.add_typed_default(DataType::ErrorType, DataValue::Str(error)),
|
||||||
};
|
};
|
||||||
connection.send(&response.with_id(request_id)).await
|
connection.send(&response.with_id(value.get_id())).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn change_user(
|
pub async fn change_user(
|
||||||
|
|
@ -268,3 +228,44 @@ 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 Some(reset) = value.get_data(DataType::ResetToken).as_str() else {
|
||||||
|
return connection
|
||||||
|
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidData)
|
||||||
|
.await;
|
||||||
|
};
|
||||||
|
let Some(new_token) = value.get_data(DataType::NewToken).as_str() else {
|
||||||
|
return connection
|
||||||
|
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidData)
|
||||||
|
.await;
|
||||||
|
};
|
||||||
|
let user_id = UserId::from(value.get_sender() as i64);
|
||||||
|
let user = match user_repo::get_by_user_id(user_id).await {
|
||||||
|
Ok(user) => user,
|
||||||
|
Err(_) => {
|
||||||
|
return connection
|
||||||
|
.send_error_response(value.get_id(), CommunicationType::ErrorNotFound)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if user.token != reset {
|
||||||
|
return connection
|
||||||
|
.send_error_response(value.get_id(), CommunicationType::ErrorInvalidChallenge)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
let result =
|
||||||
|
match user_repo::change_iota_id(user_id, IotaId::from(value.get_sender() as 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(value.get_id())).await
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
pub mod capabilities;
|
|
||||||
pub mod connection;
|
pub mod connection;
|
||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
pub mod omikron_connection;
|
pub mod omikron_connection;
|
||||||
pub mod omikron_manager;
|
pub mod omikron_manager;
|
||||||
pub mod relay_router;
|
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,32 @@
|
||||||
use super::capabilities::{OmegaCapabilities, PeerCapabilities};
|
|
||||||
use crate::models::OmikronId;
|
use crate::models::OmikronId;
|
||||||
use crate::{
|
use crate::{
|
||||||
log, log_cv_in, log_cv_out, log_err, log_in, server,
|
load_keyring, log, log_cv_in, log_cv_out, log_err, log_in, server,
|
||||||
state::OmegaState,
|
|
||||||
transport::connection::{RequiredMtpFields, validate_dispatch_fields},
|
|
||||||
transport::omikron_manager,
|
transport::omikron_manager,
|
||||||
util::{file_util::load_file_vec, logger::PrintType},
|
util::{file_util::load_file_vec, logger::PrintType},
|
||||||
};
|
};
|
||||||
use dashmap::{DashMap, mapref::entry::Entry};
|
use dashmap::DashMap;
|
||||||
use mtp::{
|
use mtp::{
|
||||||
codec::{CommunicationType, CommunicationValue},
|
codec::{CommunicationType, CommunicationValue},
|
||||||
crypto::PublicKeyBundle,
|
crypto::PublicKeyBundle,
|
||||||
host::{AuthState, AuthenticationPolicy, HostConfig, Policy, SendMode},
|
host::{AuthenticationPolicy, HostConfig, Policy, SendMode},
|
||||||
webserver::{MTPWebServer, WebMtpReceiver, WebMtpSender},
|
webserver::{MTPWebServer, WebMtpReceiver, WebMtpSender},
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
net::IpAddr,
|
net::{IpAddr, Ipv4Addr},
|
||||||
sync::{
|
sync::{
|
||||||
Arc, Mutex as StdMutex,
|
Arc,
|
||||||
atomic::{AtomicU32, AtomicUsize, Ordering},
|
atomic::{AtomicUsize, Ordering},
|
||||||
},
|
},
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use tokio::{
|
use tokio::{sync::Mutex, time::interval};
|
||||||
sync::{Mutex, Semaphore, mpsc, oneshot},
|
|
||||||
time::interval,
|
|
||||||
};
|
|
||||||
use tokio_util::{sync::CancellationToken, task::TaskTracker};
|
|
||||||
|
|
||||||
const CLEANUP_INTERVAL: Duration = Duration::from_secs(30);
|
const CLEANUP_INTERVAL: Duration = Duration::from_secs(30);
|
||||||
const MAX_WAITING_AGE: Duration = Duration::from_secs(60);
|
const MAX_WAITING_AGE: Duration = Duration::from_secs(60);
|
||||||
const DEFAULT_BIND_ADDRESS: &str = "0.0.0.0";
|
|
||||||
static ACTIVE_CONNECTIONS: AtomicUsize = AtomicUsize::new(0);
|
static ACTIVE_CONNECTIONS: AtomicUsize = AtomicUsize::new(0);
|
||||||
static NEXT_CORRELATION_ID: AtomicU32 = AtomicU32::new(1);
|
|
||||||
static ACTIVE_CONNECTIONS_BY_IP: once_cell::sync::Lazy<DashMap<IpAddr, usize>> =
|
static ACTIVE_CONNECTIONS_BY_IP: once_cell::sync::Lazy<DashMap<IpAddr, usize>> =
|
||||||
once_cell::sync::Lazy::new(DashMap::new);
|
once_cell::sync::Lazy::new(DashMap::new);
|
||||||
|
|
||||||
fn parse_bind_address(value: Option<&str>) -> Result<IpAddr, std::net::AddrParseError> {
|
|
||||||
value
|
|
||||||
.unwrap_or(DEFAULT_BIND_ADDRESS)
|
|
||||||
.trim()
|
|
||||||
.parse::<IpAddr>()
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ConnectionLimitGuard(Option<IpAddr>);
|
struct ConnectionLimitGuard(Option<IpAddr>);
|
||||||
impl Drop for ConnectionLimitGuard {
|
impl Drop for ConnectionLimitGuard {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
|
|
@ -58,181 +42,32 @@ impl Drop for ConnectionLimitGuard {
|
||||||
|
|
||||||
pub type OmikronResult<T> = crate::error::Result<T>;
|
pub type OmikronResult<T> = crate::error::Result<T>;
|
||||||
pub struct WaitingTask {
|
pub struct WaitingTask {
|
||||||
pub sender: oneshot::Sender<CommunicationValue>,
|
pub task: Box<dyn Fn(Arc<OmikronConnection>, CommunicationValue) -> bool + Send + Sync>,
|
||||||
pub inserted_at: Instant,
|
pub inserted_at: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
enum DispatchClass {
|
|
||||||
Concurrent,
|
|
||||||
Ordered,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct OmikronConnection {
|
pub struct OmikronConnection {
|
||||||
id: u64,
|
id: u64,
|
||||||
state: Arc<OmegaState>,
|
|
||||||
sender: Mutex<Option<WebMtpSender>>,
|
sender: Mutex<Option<WebMtpSender>>,
|
||||||
waiting_tasks: DashMap<u32, WaitingTask>,
|
waiting_tasks: DashMap<u32, WaitingTask>,
|
||||||
cleanup_handle: std::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
|
cleanup_handle: std::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
|
||||||
handler_tasks: TaskTracker,
|
|
||||||
handler_cancel: CancellationToken,
|
|
||||||
handler_limit: Arc<Semaphore>,
|
|
||||||
ordered_sender: mpsc::Sender<CommunicationValue>,
|
|
||||||
ordered_receiver: StdMutex<Option<mpsc::Receiver<CommunicationValue>>>,
|
|
||||||
peer_capabilities: PeerCapabilities,
|
|
||||||
}
|
}
|
||||||
impl Drop for OmikronConnection {
|
impl Drop for OmikronConnection {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Some(handle) = self.cleanup_handle.lock().unwrap().take() {
|
if let Some(handle) = self.cleanup_handle.lock().unwrap().take() {
|
||||||
handle.abort();
|
handle.abort();
|
||||||
}
|
}
|
||||||
self.handler_cancel.cancel();
|
|
||||||
self.handler_tasks.close();
|
|
||||||
self.waiting_tasks.clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OmikronConnection {
|
impl OmikronConnection {
|
||||||
pub fn new(
|
pub fn new(sender: WebMtpSender, id: u64) -> Arc<Self> {
|
||||||
sender: WebMtpSender,
|
Arc::new(Self {
|
||||||
id: u64,
|
|
||||||
description: Option<&str>,
|
|
||||||
state: Arc<OmegaState>,
|
|
||||||
authenticated: bool,
|
|
||||||
) -> Option<Arc<Self>> {
|
|
||||||
if !authenticated || id == 0 || i64::try_from(id).is_err() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let peer_capabilities =
|
|
||||||
PeerCapabilities::from_identification_description(description).ok()?;
|
|
||||||
let handler_concurrency = state.omikron_handler_concurrency;
|
|
||||||
let (ordered_sender, ordered_receiver) = mpsc::channel(handler_concurrency);
|
|
||||||
Some(Arc::new(Self {
|
|
||||||
id,
|
id,
|
||||||
state,
|
|
||||||
sender: Mutex::new(Some(sender)),
|
sender: Mutex::new(Some(sender)),
|
||||||
waiting_tasks: DashMap::new(),
|
waiting_tasks: DashMap::new(),
|
||||||
cleanup_handle: std::sync::Mutex::new(None),
|
cleanup_handle: std::sync::Mutex::new(None),
|
||||||
handler_tasks: TaskTracker::new(),
|
})
|
||||||
handler_cancel: CancellationToken::new(),
|
|
||||||
handler_limit: Arc::new(Semaphore::new(handler_concurrency)),
|
|
||||||
ordered_sender,
|
|
||||||
ordered_receiver: StdMutex::new(Some(ordered_receiver)),
|
|
||||||
peer_capabilities,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn peer_capabilities(&self) -> &PeerCapabilities {
|
|
||||||
&self.peer_capabilities
|
|
||||||
}
|
|
||||||
|
|
||||||
fn track_handler<F>(&self, task: F)
|
|
||||||
where
|
|
||||||
F: std::future::Future<Output = ()> + Send + 'static,
|
|
||||||
{
|
|
||||||
let handle = self.handler_tasks.spawn(task);
|
|
||||||
let id = self.id as i64;
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(error) = handle.await {
|
|
||||||
log_err!(
|
|
||||||
id,
|
|
||||||
PrintType::Omega,
|
|
||||||
"Omikron handler task failed: {}",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_ordered_worker(self: &Arc<Self>) {
|
|
||||||
let Some(mut receiver) = self.ordered_receiver.lock().unwrap().take() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let connection = self.clone();
|
|
||||||
let cancellation = self.handler_cancel.clone();
|
|
||||||
self.track_handler(async move {
|
|
||||||
loop {
|
|
||||||
let value = tokio::select! {
|
|
||||||
_ = cancellation.cancelled() => break,
|
|
||||||
value = receiver.recv() => match value {
|
|
||||||
Some(value) => value,
|
|
||||||
None => break,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let global_permit = tokio::select! {
|
|
||||||
_ = cancellation.cancelled() => break,
|
|
||||||
permit = connection.state.global_handler_limit.clone().acquire_owned() => {
|
|
||||||
match permit {
|
|
||||||
Ok(permit) => permit,
|
|
||||||
Err(_) => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let permit = tokio::select! {
|
|
||||||
_ = cancellation.cancelled() => break,
|
|
||||||
permit = connection.handler_limit.clone().acquire_owned() => {
|
|
||||||
match permit {
|
|
||||||
Ok(permit) => permit,
|
|
||||||
Err(_) => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let result = tokio::select! {
|
|
||||||
_ = cancellation.cancelled() => break,
|
|
||||||
result = connection.clone().dispatch(value) => result,
|
|
||||||
};
|
|
||||||
drop(permit);
|
|
||||||
drop(global_permit);
|
|
||||||
if let Err(error) = result {
|
|
||||||
log_err!(
|
|
||||||
connection.id as i64,
|
|
||||||
PrintType::Omega,
|
|
||||||
"Error processing ordered Omikron message: {}",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn stop_handlers(&self) {
|
|
||||||
self.handler_cancel.cancel();
|
|
||||||
self.handler_tasks.close();
|
|
||||||
let _ = tokio::time::timeout(Duration::from_secs(1), self.handler_tasks.wait()).await;
|
|
||||||
self.waiting_tasks.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn correlation_response(value: &CommunicationValue) -> bool {
|
|
||||||
matches!(
|
|
||||||
value.get_comm_type_enum(),
|
|
||||||
Some(CommunicationType::Success)
|
|
||||||
| Some(CommunicationType::ErrorProtocol)
|
|
||||||
| Some(CommunicationType::ErrorAnonymous)
|
|
||||||
| Some(CommunicationType::ErrorInternal)
|
|
||||||
| Some(CommunicationType::ErrorInvalidData)
|
|
||||||
| Some(CommunicationType::ErrorInvalidUserId)
|
|
||||||
| Some(CommunicationType::ErrorInvalidOmikronId)
|
|
||||||
| Some(CommunicationType::ErrorNotFound)
|
|
||||||
| Some(CommunicationType::ErrorNotAuthenticated)
|
|
||||||
| Some(CommunicationType::ErrorNoIota)
|
|
||||||
| Some(CommunicationType::ErrorInvalidChallenge)
|
|
||||||
| Some(CommunicationType::ErrorInvalidSecret)
|
|
||||||
| Some(CommunicationType::ErrorInvalidPrivateKey)
|
|
||||||
| Some(CommunicationType::ErrorInvalidPublicKey)
|
|
||||||
| Some(CommunicationType::ErrorNoUserId)
|
|
||||||
| Some(CommunicationType::ErrorNoCallId)
|
|
||||||
| Some(CommunicationType::ErrorInvalidCallId)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_correlation_id(&self) -> Option<u32> {
|
|
||||||
for _ in 0..u32::MAX {
|
|
||||||
let id = NEXT_CORRELATION_ID.fetch_add(1, Ordering::Relaxed);
|
|
||||||
if id != 0 && !self.waiting_tasks.contains_key(&id) {
|
|
||||||
return Some(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle(self: Arc<Self>, receiver: &mut WebMtpReceiver) {
|
pub async fn handle(self: Arc<Self>, receiver: &mut WebMtpReceiver) {
|
||||||
|
|
@ -241,33 +76,12 @@ impl OmikronConnection {
|
||||||
PrintType::Omega,
|
PrintType::Omega,
|
||||||
"Omikron connection started"
|
"Omikron connection started"
|
||||||
);
|
);
|
||||||
let capabilities = CommunicationValue::new(CommunicationType::IdentificationResponse)
|
let cleanup_conn = self.clone();
|
||||||
.add_typed_default(
|
|
||||||
mtp::codec::DataType::Description,
|
|
||||||
mtp::codec::DataValue::Str(
|
|
||||||
OmegaCapabilities::current().identification_description(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if let Err(error) = self.clone().send(&capabilities).await {
|
|
||||||
log_err!(
|
|
||||||
self.id as i64,
|
|
||||||
PrintType::Omega,
|
|
||||||
"Failed to send Omega capabilities: {}",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
self.clone().cleanup().await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.start_ordered_worker();
|
|
||||||
let cleanup_conn = Arc::downgrade(&self);
|
|
||||||
*self.cleanup_handle.lock().unwrap() = Some(tokio::spawn(async move {
|
*self.cleanup_handle.lock().unwrap() = Some(tokio::spawn(async move {
|
||||||
let mut ticker = interval(CLEANUP_INTERVAL);
|
let mut ticker = interval(CLEANUP_INTERVAL);
|
||||||
loop {
|
loop {
|
||||||
ticker.tick().await;
|
ticker.tick().await;
|
||||||
let Some(connection) = cleanup_conn.upgrade() else {
|
cleanup_conn
|
||||||
break;
|
|
||||||
};
|
|
||||||
connection
|
|
||||||
.waiting_tasks
|
.waiting_tasks
|
||||||
.retain(|_, task| task.inserted_at.elapsed() < MAX_WAITING_AGE);
|
.retain(|_, task| task.inserted_at.elapsed() < MAX_WAITING_AGE);
|
||||||
}
|
}
|
||||||
|
|
@ -309,136 +123,36 @@ impl OmikronConnection {
|
||||||
|
|
||||||
async fn process_message(self: Arc<Self>, value: CommunicationValue) -> OmikronResult<()> {
|
async fn process_message(self: Arc<Self>, value: CommunicationValue) -> OmikronResult<()> {
|
||||||
log_cv_in!(PrintType::Omikron, &value);
|
log_cv_in!(PrintType::Omikron, &value);
|
||||||
if Self::correlation_response(&value)
|
if let Some((_, task)) = self.waiting_tasks.remove(&value.get_id()) {
|
||||||
&& let Some(message_id) = value.id()
|
let _ = (task.task)(self.clone(), value);
|
||||||
&& let Some((_, task)) = self.waiting_tasks.remove(&message_id)
|
|
||||||
{
|
|
||||||
let _ = task.sender.send(value);
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
self.dispatch(value).await
|
||||||
if !value.is_type(CommunicationType::Relay) {
|
|
||||||
validate_dispatch_fields(&value)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let dispatch_class = Self::dispatch_class(&value);
|
|
||||||
if dispatch_class == DispatchClass::Ordered {
|
|
||||||
tokio::select! {
|
|
||||||
_ = self.handler_cancel.cancelled() => {
|
|
||||||
return Err(crate::error::OmegaError::NotConnected);
|
|
||||||
}
|
|
||||||
result = self.ordered_sender.send(value) => {
|
|
||||||
result.map_err(|_| crate::error::OmegaError::NotConnected)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let global_permit = tokio::select! {
|
|
||||||
_ = self.handler_cancel.cancelled() => {
|
|
||||||
return Err(crate::error::OmegaError::NotConnected);
|
|
||||||
}
|
|
||||||
permit = self.state.global_handler_limit.clone().acquire_owned() => {
|
|
||||||
permit.map_err(|_| crate::error::OmegaError::NotConnected)?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let permit = tokio::select! {
|
|
||||||
_ = self.handler_cancel.cancelled() => {
|
|
||||||
drop(global_permit);
|
|
||||||
return Err(crate::error::OmegaError::NotConnected);
|
|
||||||
}
|
|
||||||
permit = self.handler_limit.clone().acquire_owned() => {
|
|
||||||
permit.map_err(|_| crate::error::OmegaError::NotConnected)?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let connection = self.clone();
|
|
||||||
let cancellation = self.handler_cancel.clone();
|
|
||||||
self.track_handler(async move {
|
|
||||||
tokio::select! {
|
|
||||||
_ = cancellation.cancelled() => {}
|
|
||||||
result = connection.clone().dispatch(value) => {
|
|
||||||
if let Err(error) = result {
|
|
||||||
log_err!(
|
|
||||||
connection.id as i64,
|
|
||||||
PrintType::Omega,
|
|
||||||
"Error processing Omikron message: {}",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
drop(permit);
|
|
||||||
drop(global_permit);
|
|
||||||
});
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dispatch_class(value: &CommunicationValue) -> DispatchClass {
|
|
||||||
match value.get_comm_type_enum() {
|
|
||||||
Some(CommunicationType::UserConnected)
|
|
||||||
| Some(CommunicationType::UserDisconnected)
|
|
||||||
| Some(CommunicationType::IotaConnected)
|
|
||||||
| Some(CommunicationType::IotaDisconnected)
|
|
||||||
| Some(CommunicationType::SyncClientIotaStatus)
|
|
||||||
| Some(CommunicationType::StateSubscribe)
|
|
||||||
| Some(CommunicationType::ClientChanged) => DispatchClass::Ordered,
|
|
||||||
_ => DispatchClass::Concurrent,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn dispatch(self: Arc<Self>, value: CommunicationValue) -> OmikronResult<()> {
|
async fn dispatch(self: Arc<Self>, value: CommunicationValue) -> OmikronResult<()> {
|
||||||
let id = self.id as i64;
|
let id = self.id as i64;
|
||||||
let state = self.state.clone();
|
|
||||||
if value.is_type(CommunicationType::Relay) {
|
|
||||||
let value = crate::transport::relay_router::ensure_relay_frame_id(value);
|
|
||||||
let request_id = value.id();
|
|
||||||
let result = crate::transport::relay_router::route_from_omikron(id, value).await;
|
|
||||||
let response = match &result {
|
|
||||||
Ok(response) => request_id.map(|request_id| response.clone().with_id(request_id)),
|
|
||||||
Err(error) => {
|
|
||||||
log_err!(id, PrintType::Omega, "Relay routing failed: {}", error);
|
|
||||||
request_id.map(|request_id| {
|
|
||||||
CommunicationValue::new(
|
|
||||||
crate::transport::relay_router::error_response_type(error),
|
|
||||||
)
|
|
||||||
.with_id(request_id)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if let Some(response) = response
|
|
||||||
&& let Err(send_error) = self.clone().send(&response).await
|
|
||||||
{
|
|
||||||
log_err!(
|
|
||||||
id,
|
|
||||||
PrintType::Omega,
|
|
||||||
"Relay routing response failed: {}",
|
|
||||||
send_error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|error| crate::error::OmegaError::Transport(error.to_string()));
|
|
||||||
}
|
|
||||||
match value.get_comm_type_enum() {
|
match value.get_comm_type_enum() {
|
||||||
Some(CommunicationType::ShortenLink) => {
|
Some(CommunicationType::ShortenLink) => {
|
||||||
crate::transport::handlers::links::shorten(self, value).await
|
crate::transport::handlers::links::shorten(self, value).await
|
||||||
}
|
}
|
||||||
Some(CommunicationType::UserConnected) => {
|
Some(CommunicationType::UserConnected) => {
|
||||||
crate::transport::handlers::presence::user_connected(state, self, value, id).await
|
crate::transport::handlers::presence::user_connected(self, value, id).await
|
||||||
}
|
}
|
||||||
Some(CommunicationType::UserDisconnected) => {
|
Some(CommunicationType::UserDisconnected) => {
|
||||||
crate::transport::handlers::presence::user_disconnected(state, self, value, id)
|
crate::transport::handlers::presence::user_disconnected(self, value, id).await
|
||||||
.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(state, self, value, id).await
|
crate::transport::handlers::presence::iota_connected(self, value, id).await
|
||||||
}
|
}
|
||||||
Some(CommunicationType::IotaDisconnected) => {
|
Some(CommunicationType::IotaDisconnected) => {
|
||||||
crate::transport::handlers::presence::iota_disconnected(state, self, value, id)
|
crate::transport::handlers::presence::iota_disconnected(self, value, id).await
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
Some(CommunicationType::SyncClientIotaStatus) => {
|
Some(CommunicationType::SyncClientIotaStatus) => {
|
||||||
crate::transport::handlers::presence::sync_status(state, self, value, id).await
|
crate::transport::handlers::presence::sync_status(self, value, id).await
|
||||||
}
|
}
|
||||||
Some(CommunicationType::GetUserData) => {
|
Some(CommunicationType::GetUserData) => {
|
||||||
crate::transport::handlers::user_data::get_user(self, value).await
|
crate::transport::handlers::user_data::get_user(self, value).await
|
||||||
|
|
@ -449,6 +163,9 @@ 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
|
||||||
}
|
}
|
||||||
|
|
@ -461,24 +178,6 @@ impl OmikronConnection {
|
||||||
Some(CommunicationType::DeleteUser) => {
|
Some(CommunicationType::DeleteUser) => {
|
||||||
crate::transport::handlers::account::user(self, value).await
|
crate::transport::handlers::account::user(self, value).await
|
||||||
}
|
}
|
||||||
Some(CommunicationType::AttachUserBegin) => {
|
|
||||||
crate::transport::handlers::account::attach_begin(self, value).await
|
|
||||||
}
|
|
||||||
Some(CommunicationType::AttachUserComplete) => {
|
|
||||||
crate::transport::handlers::account::attach_complete(self, value).await
|
|
||||||
}
|
|
||||||
Some(CommunicationType::DeleteUserCredentialBegin) => {
|
|
||||||
crate::transport::handlers::account::delete_credential_begin(self, value).await
|
|
||||||
}
|
|
||||||
Some(CommunicationType::DeleteUserCredentialComplete) => {
|
|
||||||
crate::transport::handlers::account::delete_credential_complete(self, value).await
|
|
||||||
}
|
|
||||||
Some(CommunicationType::EraseHostedUserDataAck) => {
|
|
||||||
crate::transport::handlers::account::erase_hosted_user_data_ack(self, value).await
|
|
||||||
}
|
|
||||||
Some(CommunicationType::ReleaseUserFromIota) => {
|
|
||||||
crate::transport::handlers::account::release_from_iota(self, value).await
|
|
||||||
}
|
|
||||||
Some(CommunicationType::DeleteIota) => {
|
Some(CommunicationType::DeleteIota) => {
|
||||||
crate::transport::handlers::account::iota(self, value).await
|
crate::transport::handlers::account::iota(self, value).await
|
||||||
}
|
}
|
||||||
|
|
@ -494,13 +193,6 @@ impl OmikronConnection {
|
||||||
Some(CommunicationType::GetStates) => {
|
Some(CommunicationType::GetStates) => {
|
||||||
crate::transport::handlers::states::get(self, value).await
|
crate::transport::handlers::states::get(self, value).await
|
||||||
}
|
}
|
||||||
Some(CommunicationType::StateSubscribe) => {
|
|
||||||
crate::transport::handlers::presence::state_subscribe(state, self, value, id).await
|
|
||||||
}
|
|
||||||
Some(CommunicationType::ClientChanged) => {
|
|
||||||
crate::transport::handlers::presence::client_changed_legacy(state, self, value, id)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
_ => {
|
_ => {
|
||||||
log_err!(
|
log_err!(
|
||||||
0,
|
0,
|
||||||
|
|
@ -524,71 +216,6 @@ impl OmikronConnection {
|
||||||
.await
|
.await
|
||||||
.map_err(|error| crate::error::OmegaError::SendError(error.to_string()))
|
.map_err(|error| crate::error::OmegaError::SendError(error.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn send_messages(
|
|
||||||
self: Arc<Self>,
|
|
||||||
values: &[CommunicationValue],
|
|
||||||
) -> OmikronResult<()> {
|
|
||||||
let guard = self.sender.lock().await;
|
|
||||||
let sender = guard
|
|
||||||
.as_ref()
|
|
||||||
.ok_or(crate::error::OmegaError::NotConnected)?;
|
|
||||||
for value in values {
|
|
||||||
log_cv_out!(PrintType::Omikron, value);
|
|
||||||
sender
|
|
||||||
.send(value)
|
|
||||||
.await
|
|
||||||
.map_err(|error| crate::error::OmegaError::SendError(error.to_string()))?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn await_response(
|
|
||||||
self: Arc<Self>,
|
|
||||||
value: &CommunicationValue,
|
|
||||||
timeout: Duration,
|
|
||||||
) -> OmikronResult<CommunicationValue> {
|
|
||||||
value.require_id()?;
|
|
||||||
let (tx, rx) = oneshot::channel();
|
|
||||||
let message_id = self.next_correlation_id().ok_or_else(|| {
|
|
||||||
crate::OmegaError::Transport("no relay correlation id available".into())
|
|
||||||
})?;
|
|
||||||
match self.waiting_tasks.entry(message_id) {
|
|
||||||
Entry::Vacant(entry) => {
|
|
||||||
entry.insert(WaitingTask {
|
|
||||||
sender: tx,
|
|
||||||
inserted_at: Instant::now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Entry::Occupied(_) => {
|
|
||||||
return Err(crate::OmegaError::Transport(
|
|
||||||
"relay correlation id collision".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let outbound = value.clone().with_id(message_id);
|
|
||||||
|
|
||||||
if let Err(error) = self.clone().send(&outbound).await {
|
|
||||||
self.waiting_tasks.remove(&message_id);
|
|
||||||
return Err(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
match tokio::time::timeout(timeout, rx).await {
|
|
||||||
Ok(Ok(response)) => Ok(response),
|
|
||||||
Ok(Err(_)) => {
|
|
||||||
self.waiting_tasks.remove(&message_id);
|
|
||||||
Err(crate::error::OmegaError::Transport(
|
|
||||||
"Relay response channel closed".into(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
self.waiting_tasks.remove(&message_id);
|
|
||||||
Err(crate::error::OmegaError::Transport(
|
|
||||||
"Relay response timed out".into(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub(crate) async fn send_error_response(
|
pub(crate) async fn send_error_response(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
message_id: u32,
|
message_id: u32,
|
||||||
|
|
@ -597,22 +224,6 @@ impl OmikronConnection {
|
||||||
self.send(&CommunicationValue::new(error_type).with_id(message_id))
|
self.send(&CommunicationValue::new(error_type).with_id(message_id))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
pub(crate) async fn send_error_response_with_detail(
|
|
||||||
self: Arc<Self>,
|
|
||||||
message_id: u32,
|
|
||||||
error_type: CommunicationType,
|
|
||||||
detail: &'static str,
|
|
||||||
) -> OmikronResult<()> {
|
|
||||||
self.send(
|
|
||||||
&CommunicationValue::new(error_type)
|
|
||||||
.with_id(message_id)
|
|
||||||
.add_typed_default(
|
|
||||||
mtp::codec::DataType::ErrorType,
|
|
||||||
mtp::codec::DataValue::Str(detail.to_string()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
pub async fn close(self: Arc<Self>) {
|
pub async fn close(self: Arc<Self>) {
|
||||||
log_in!(
|
log_in!(
|
||||||
self.id as i64,
|
self.id as i64,
|
||||||
|
|
@ -624,15 +235,10 @@ impl OmikronConnection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async fn cleanup(self: Arc<Self>) {
|
async fn cleanup(self: Arc<Self>) {
|
||||||
self.stop_handlers().await;
|
|
||||||
if self.id != 0 {
|
if self.id != 0 {
|
||||||
log_in!(self.id as i64, PrintType::Omega, "Omikron disconnected");
|
log_in!(self.id as i64, PrintType::Omega, "Omikron disconnected");
|
||||||
if omikron_manager::remove_omikron(self.id as i64, &self).await {
|
if omikron_manager::remove_omikron(self.id as i64, &self).await {
|
||||||
crate::transport::handlers::presence::omikron_disconnected(
|
crate::sql::user_online_tracker::untrack_omikron(self.id as i64).await;
|
||||||
self.state.clone(),
|
|
||||||
self.id as i64,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(handle) = self.cleanup_handle.lock().unwrap().take() {
|
if let Some(handle) = self.cleanup_handle.lock().unwrap().take() {
|
||||||
|
|
@ -642,21 +248,13 @@ impl OmikronConnection {
|
||||||
pub async fn get_omikron_id(self: Arc<Self>) -> Option<i64> {
|
pub async fn get_omikron_id(self: Arc<Self>) -> Option<i64> {
|
||||||
Some(self.id as i64)
|
Some(self.id as i64)
|
||||||
}
|
}
|
||||||
pub fn state(&self) -> Arc<OmegaState> {
|
|
||||||
self.state.clone()
|
|
||||||
}
|
|
||||||
pub async fn send_message(self: Arc<Self>, value: &CommunicationValue) -> OmikronResult<()> {
|
pub async fn send_message(self: Arc<Self>, value: &CommunicationValue) -> OmikronResult<()> {
|
||||||
self.send(value).await
|
self.send(value).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_by_omikron_id(
|
pub async fn get_by_omikron_id(omikron_id: u64, _: Option<String>) -> Option<PublicKeyBundle> {
|
||||||
omikron_id: u64,
|
crate::db::omikron_repo::get_omikron_by_id(OmikronId::from(omikron_id as i64))
|
||||||
description: Option<String>,
|
|
||||||
) -> Option<PublicKeyBundle> {
|
|
||||||
PeerCapabilities::from_identification_description(description.as_deref()).ok()?;
|
|
||||||
let omikron_id = i64::try_from(omikron_id).ok().filter(|id| *id > 0)?;
|
|
||||||
crate::db::omikron_repo::get_omikron_by_id(OmikronId::from(omikron_id))
|
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
.map(|omikron| omikron.public_key)
|
.map(|omikron| omikron.public_key)
|
||||||
|
|
@ -665,13 +263,13 @@ pub async fn complete_register(_: PublicKeyBundle, _: Option<String>) -> u64 {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start(port: u16, state: Arc<OmegaState>) -> 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::web::build_web_config(state.identity.clone())?
|
let web_config = server::server::build_web_config()?
|
||||||
.serve_tcp_https(true)
|
.serve_tcp_https(true)
|
||||||
.max_tcp_connections(256);
|
.max_tcp_connections(256);
|
||||||
let ip = parse_bind_address(std::env::var("BIND_ADDRESS").ok().as_deref())?;
|
let ip = IpAddr::from(Ipv4Addr::new(0, 0, 0, 0));
|
||||||
let host_config = HostConfig::new(ip, port, cert_pem, key_pem)
|
let host_config = HostConfig::new(ip, port, cert_pem, key_pem)
|
||||||
.with_policy(Policy {
|
.with_policy(Policy {
|
||||||
send_mode: SendMode::SingleStreamPerMessage,
|
send_mode: SendMode::SingleStreamPerMessage,
|
||||||
|
|
@ -693,7 +291,7 @@ pub async fn start(port: u16, state: Arc<OmegaState>) -> Result<(), Box<dyn std:
|
||||||
max_frames_per_stream: None,
|
max_frames_per_stream: None,
|
||||||
})
|
})
|
||||||
.with_authentication(
|
.with_authentication(
|
||||||
state.identity.clone_keyring()?,
|
load_keyring(),
|
||||||
Box::new(|id, description| Box::pin(get_by_omikron_id(id, description))),
|
Box::new(|id, description| Box::pin(get_by_omikron_id(id, description))),
|
||||||
Box::new(|key, description| Box::pin(complete_register(key, description))),
|
Box::new(|key, description| Box::pin(complete_register(key, description))),
|
||||||
)
|
)
|
||||||
|
|
@ -714,7 +312,7 @@ pub async fn start(port: u16, state: Arc<OmegaState>) -> Result<(), Box<dyn std:
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let config = &state.config.rate_limits;
|
let config = crate::config::RateLimitConfig::from_env();
|
||||||
let peer_ip = conn.remote_addr.map(|address| address.ip());
|
let peer_ip = conn.remote_addr.map(|address| address.ip());
|
||||||
let active = ACTIVE_CONNECTIONS.fetch_add(1, Ordering::AcqRel) + 1;
|
let active = ACTIVE_CONNECTIONS.fetch_add(1, Ordering::AcqRel) + 1;
|
||||||
let peer_active = peer_ip.map(|ip| {
|
let peer_active = peer_ip.map(|ip| {
|
||||||
|
|
@ -722,11 +320,10 @@ pub async fn start(port: u16, state: Arc<OmegaState>) -> Result<(), Box<dyn std:
|
||||||
*count += 1;
|
*count += 1;
|
||||||
*count
|
*count
|
||||||
});
|
});
|
||||||
let connection_limit_guard = ConnectionLimitGuard(peer_ip);
|
|
||||||
if active > config.transport_connections
|
if active > config.transport_connections
|
||||||
|| peer_active.is_some_and(|count| count > config.transport_connections_per_ip)
|
|| peer_active.is_some_and(|count| count > config.transport_connections_per_ip)
|
||||||
{
|
{
|
||||||
drop(connection_limit_guard);
|
drop(ConnectionLimitGuard(peer_ip));
|
||||||
log_err!(
|
log_err!(
|
||||||
0,
|
0,
|
||||||
PrintType::Omega,
|
PrintType::Omega,
|
||||||
|
|
@ -734,90 +331,12 @@ pub async fn start(port: u16, state: Arc<OmegaState>) -> Result<(), Box<dyn std:
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let authenticated = matches!(&conn.auth_state, AuthState::Authenticated);
|
let connection = OmikronConnection::new(conn.sender, conn.client_id);
|
||||||
let Some(connection) = OmikronConnection::new(
|
|
||||||
conn.sender,
|
|
||||||
conn.client_id,
|
|
||||||
conn.description.as_deref(),
|
|
||||||
state.clone(),
|
|
||||||
authenticated,
|
|
||||||
) else {
|
|
||||||
drop(connection_limit_guard);
|
|
||||||
log_err!(
|
|
||||||
0,
|
|
||||||
PrintType::Omega,
|
|
||||||
"Rejected Omikron connection without authenticated transport state or valid capabilities"
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let _guard = connection_limit_guard;
|
let _guard = ConnectionLimitGuard(peer_ip);
|
||||||
omikron_manager::add_omikron(connection.clone()).await;
|
omikron_manager::add_omikron(connection.clone()).await;
|
||||||
connection.handle(&mut conn.receiver).await;
|
connection.handle(&mut conn.receiver).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::{DispatchClass, OmikronConnection, parse_bind_address};
|
|
||||||
use mtp::codec::{CommunicationType, CommunicationValue};
|
|
||||||
use std::net::{IpAddr, Ipv4Addr};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn relay_dispatch_is_not_on_the_ordered_state_lane() {
|
|
||||||
let value = CommunicationValue::new(CommunicationType::Relay);
|
|
||||||
assert_eq!(
|
|
||||||
OmikronConnection::dispatch_class(&value),
|
|
||||||
DispatchClass::Concurrent
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parses_configured_bind_address() {
|
|
||||||
assert_eq!(
|
|
||||||
parse_bind_address(Some("10.200.2.0")),
|
|
||||||
Ok(IpAddr::V4(Ipv4Addr::new(10, 200, 2, 0)))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn presence_lifecycle_dispatch_is_ordered() {
|
|
||||||
let value = CommunicationValue::new(CommunicationType::UserConnected);
|
|
||||||
assert_eq!(
|
|
||||||
OmikronConnection::dispatch_class(&value),
|
|
||||||
DispatchClass::Ordered
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn client_presence_changes_are_ordered() {
|
|
||||||
let value = CommunicationValue::new(CommunicationType::ClientChanged);
|
|
||||||
assert_eq!(
|
|
||||||
OmikronConnection::dispatch_class(&value),
|
|
||||||
DispatchClass::Ordered
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn only_protocol_responses_match_waiting_tasks() {
|
|
||||||
assert!(OmikronConnection::correlation_response(
|
|
||||||
&CommunicationValue::new(CommunicationType::Success).with_id(1)
|
|
||||||
));
|
|
||||||
assert!(OmikronConnection::correlation_response(
|
|
||||||
&CommunicationValue::new(CommunicationType::ErrorInvalidData).with_id(1)
|
|
||||||
));
|
|
||||||
assert!(!OmikronConnection::correlation_response(
|
|
||||||
&CommunicationValue::new(CommunicationType::GetUserData).with_id(1)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn defaults_bind_address_to_all_interfaces() {
|
|
||||||
assert_eq!(
|
|
||||||
parse_bind_address(None),
|
|
||||||
Ok(IpAddr::V4(Ipv4Addr::UNSPECIFIED))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,12 @@
|
||||||
use crate::db::user_repo;
|
|
||||||
use crate::state::OmegaState;
|
|
||||||
use crate::transport::connection::OmikronConnection;
|
use crate::transport::connection::OmikronConnection;
|
||||||
use crate::transport::omikron_connection::OmikronResult;
|
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
use mtp::codec::CommunicationValue;
|
||||||
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());
|
||||||
|
|
||||||
pub async fn add_omikron(conn: Arc<OmikronConnection>) {
|
pub async fn add_omikron(conn: Arc<OmikronConnection>) {
|
||||||
let id = match conn.clone().get_omikron_id().await {
|
let id = match conn.clone().get_omikron_id().await {
|
||||||
|
|
@ -26,7 +20,6 @@ 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 {
|
||||||
|
|
@ -41,175 +34,22 @@ pub fn get_connected_omikron(omikron_id: i64) -> Option<Arc<OmikronConnection>>
|
||||||
.map(|connection| connection.clone())
|
.map(|connection| connection.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_state() -> Option<Arc<OmegaState>> {
|
|
||||||
OMIKRON_CONNECTIONS
|
|
||||||
.iter()
|
|
||||||
.next()
|
|
||||||
.map(|connection| connection.value().state())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_iota_primary_omikron_connection(iota_id: i64) -> Option<i64> {
|
|
||||||
get_state().and_then(|state| state.presence.primary_iota_route(iota_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_all_connections()
|
|
||||||
-> Result<std::collections::HashMap<i64, std::collections::HashMap<i64, Vec<i64>>>, ()> {
|
|
||||||
match get_state() {
|
|
||||||
Some(state) => {
|
|
||||||
let mut result = state.presence.connection_routes();
|
|
||||||
let iota_ids = state
|
|
||||||
.presence
|
|
||||||
.all_iota_routes()
|
|
||||||
.keys()
|
|
||||||
.copied()
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let users = user_repo::get_users_by_iota_ids(&iota_ids)
|
|
||||||
.await
|
|
||||||
.map_err(|_| ())?;
|
|
||||||
for user in users {
|
|
||||||
if let Some(iota_id) = user.iota_id {
|
|
||||||
for omikron_id in state
|
|
||||||
.presence
|
|
||||||
.iota_connections(iota_id.0)
|
|
||||||
.unwrap_or_default()
|
|
||||||
{
|
|
||||||
if let Some(users) = result
|
|
||||||
.get_mut(&omikron_id)
|
|
||||||
.and_then(|iotas| iotas.get_mut(&iota_id.0))
|
|
||||||
{
|
|
||||||
users.push(user.id.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for iotas in result.values_mut() {
|
|
||||||
for users in iotas.values_mut() {
|
|
||||||
users.sort_unstable();
|
|
||||||
users.dedup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
None => Ok(std::collections::HashMap::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_state_batch(
|
|
||||||
omikron_id: i64,
|
|
||||||
notifications: Vec<CommunicationValue>,
|
|
||||||
) -> OmikronResult<()> {
|
|
||||||
let connection =
|
|
||||||
get_connected_omikron(omikron_id).ok_or(crate::error::OmegaError::NotConnected)?;
|
|
||||||
connection.send_messages(¬ifications).await
|
|
||||||
}
|
|
||||||
|
|
||||||
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()) {
|
||||||
&& let Some(connection) = get_connected_omikron(key)
|
if let Some(connection) = get_connected_omikron(key) {
|
||||||
{
|
return Ok(connection);
|
||||||
return Ok(connection);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(())
|
Err(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_to_user(user_id: i64, cv: &CommunicationValue) {
|
pub async fn send_to_user(user_id: i64, cv: &CommunicationValue) {
|
||||||
if let Some(state) = get_state() {
|
if let Some(user_conn) = crate::sql::user_online_tracker::get_user_status(user_id) {
|
||||||
for user_route in state.presence.routes_for_user(user_id) {
|
if let Some(omikron_conn) = OMIKRON_CONNECTIONS.get(&user_conn.omikron_id) {
|
||||||
if let Some(omikron_conn) = OMIKRON_CONNECTIONS.get(&user_route.omikron_id) {
|
let _ = omikron_conn.value().clone().send_message(cv).await;
|
||||||
let _ = omikron_conn.value().clone().send_message(cv).await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Publish each Iota membership snapshot to every live relay route. Each
|
|
||||||
* Omikron keeps a local authorization index, so sending only a primary route
|
|
||||||
* leaves the remaining relays stale after registration or migration.
|
|
||||||
*/
|
|
||||||
pub async fn publish_iota_user_snapshot(iota_id: i64) -> OmikronResult<()> {
|
|
||||||
let state = get_state().ok_or(crate::error::OmegaError::NotConnected)?;
|
|
||||||
let omikron_ids = state
|
|
||||||
.presence
|
|
||||||
.iota_connections(iota_id)
|
|
||||||
.ok_or(crate::error::OmegaError::NotConnected)?;
|
|
||||||
let users = user_repo::get_users_by_iota_id(crate::models::IotaId::from(iota_id)).await?;
|
|
||||||
let user_ids = users
|
|
||||||
.into_iter()
|
|
||||||
.map(|user| DataValue::SignedNumber(user.id.0.into()))
|
|
||||||
.collect();
|
|
||||||
let snapshot = CommunicationValue::new(CommunicationType::IotaUserData)
|
|
||||||
.add_typed_default(DataType::IotaId, DataValue::SignedNumber(iota_id.into()))
|
|
||||||
.add_typed_default(DataType::UserIds, DataValue::Array(user_ids));
|
|
||||||
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) {
|
|
||||||
let Ok(users) =
|
|
||||||
user_repo::pending_erasures_for_iota(crate::models::IotaId::from(iota_id)).await
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(omikron_id) = get_iota_primary_omikron_connection(iota_id) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(connection) = get_connected_omikron(omikron_id) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
for user_id in users {
|
|
||||||
let request = CommunicationValue::new(CommunicationType::EraseHostedUserData)
|
|
||||||
.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.0.into()))
|
|
||||||
.add_typed_default(DataType::IotaId, DataValue::SignedNumber(iota_id.into()));
|
|
||||||
let _ = connection.clone().send(&request).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,230 +0,0 @@
|
||||||
use super::omikron_manager;
|
|
||||||
use crate::{log_err, util::logger::PrintType};
|
|
||||||
use mtp::codec::{CommunicationType, CommunicationValue, RelayError, forward_relay_frame};
|
|
||||||
use std::{
|
|
||||||
convert::TryFrom,
|
|
||||||
sync::atomic::{AtomicU32, Ordering},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
const TARGET_KIND_MASK: u64 = 0xC000_0000_0000_0000;
|
|
||||||
const TARGET_ID_MASK: u64 = (1_u64 << 48) - 1;
|
|
||||||
const USER_TARGET_KIND: u64 = 0x4000_0000_0000_0000;
|
|
||||||
const IOTA_TARGET_KIND: u64 = 0x8000_0000_0000_0000;
|
|
||||||
static NEXT_RELAY_FRAME_ID: AtomicU32 = AtomicU32::new(1);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum RouteTarget {
|
|
||||||
User(u64),
|
|
||||||
Iota(u64),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RouteTarget {
|
|
||||||
pub fn from_wire_id(value: u64) -> Option<Self> {
|
|
||||||
let id = value & TARGET_ID_MASK;
|
|
||||||
if id == 0 || value & !(TARGET_KIND_MASK | TARGET_ID_MASK) != 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
match value & TARGET_KIND_MASK {
|
|
||||||
USER_TARGET_KIND => Some(Self::User(id)),
|
|
||||||
IOTA_TARGET_KIND => Some(Self::Iota(id)),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum RelayRouteError {
|
|
||||||
#[error("relay has no destination Iota")]
|
|
||||||
MissingDestinationIota,
|
|
||||||
#[error("relay has an outer sender")]
|
|
||||||
OuterSenderPresent,
|
|
||||||
#[error("relay has invalid route target {0}")]
|
|
||||||
InvalidDestinationTarget(u64),
|
|
||||||
#[error("relay destination Iota is outside Omega's ID range")]
|
|
||||||
DestinationIotaOutOfRange,
|
|
||||||
#[error("destination Iota is offline")]
|
|
||||||
IotaOffline,
|
|
||||||
#[error("destination Omikron is offline")]
|
|
||||||
OmikronOffline,
|
|
||||||
#[error("relay route resolves back to source Omikron")]
|
|
||||||
RouteLoop,
|
|
||||||
#[error(transparent)]
|
|
||||||
Relay(#[from] RelayError),
|
|
||||||
#[error("sending relay to destination Omikron failed: {0}")]
|
|
||||||
Send(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ensure_relay_frame_id(frame: CommunicationValue) -> CommunicationValue {
|
|
||||||
if frame.id().is_some_and(|id| id != 0) {
|
|
||||||
return frame;
|
|
||||||
}
|
|
||||||
let id = NEXT_RELAY_FRAME_ID.fetch_add(1, Ordering::Relaxed).max(1);
|
|
||||||
frame.with_id(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn error_response_type(error: &RelayRouteError) -> CommunicationType {
|
|
||||||
match error {
|
|
||||||
RelayRouteError::IotaOffline | RelayRouteError::OmikronOffline => {
|
|
||||||
CommunicationType::ErrorNoIota
|
|
||||||
}
|
|
||||||
RelayRouteError::Send(_) => CommunicationType::ErrorInternal,
|
|
||||||
RelayRouteError::MissingDestinationIota
|
|
||||||
| RelayRouteError::OuterSenderPresent
|
|
||||||
| RelayRouteError::InvalidDestinationTarget(_)
|
|
||||||
| RelayRouteError::DestinationIotaOutOfRange
|
|
||||||
| RelayRouteError::RouteLoop
|
|
||||||
| RelayRouteError::Relay(_) => CommunicationType::ErrorInvalidData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn route_from_omikron(
|
|
||||||
source_omikron_id: i64,
|
|
||||||
frame: CommunicationValue,
|
|
||||||
) -> Result<CommunicationValue, RelayRouteError> {
|
|
||||||
let frame = ensure_relay_frame_id(frame);
|
|
||||||
if !frame.is_type(CommunicationType::Relay) {
|
|
||||||
return Err(RelayRouteError::Relay(RelayError::NotRelay));
|
|
||||||
}
|
|
||||||
if frame.sender().is_some() {
|
|
||||||
return Err(RelayRouteError::OuterSenderPresent);
|
|
||||||
}
|
|
||||||
let Some(destination_wire_id) = frame.receiver() else {
|
|
||||||
log_err!(
|
|
||||||
source_omikron_id,
|
|
||||||
PrintType::Omega,
|
|
||||||
"Relay routing failed: missing destination Iota"
|
|
||||||
);
|
|
||||||
return Err(RelayRouteError::MissingDestinationIota);
|
|
||||||
};
|
|
||||||
let Some(RouteTarget::Iota(destination_iota)) = RouteTarget::from_wire_id(destination_wire_id)
|
|
||||||
else {
|
|
||||||
return Err(RelayRouteError::InvalidDestinationTarget(
|
|
||||||
destination_wire_id,
|
|
||||||
));
|
|
||||||
};
|
|
||||||
let destination_iota_i64 =
|
|
||||||
i64::try_from(destination_iota).map_err(|_| RelayRouteError::DestinationIotaOutOfRange)?;
|
|
||||||
let frame = forward_relay_frame(&frame, destination_wire_id)?;
|
|
||||||
|
|
||||||
let Some(destination_omikron) =
|
|
||||||
omikron_manager::get_iota_primary_omikron_connection(destination_iota_i64)
|
|
||||||
else {
|
|
||||||
log_err!(
|
|
||||||
source_omikron_id,
|
|
||||||
PrintType::Omega,
|
|
||||||
"Relay destination Iota {} is offline",
|
|
||||||
destination_iota
|
|
||||||
);
|
|
||||||
return Err(RelayRouteError::IotaOffline);
|
|
||||||
};
|
|
||||||
if destination_omikron == source_omikron_id {
|
|
||||||
log_err!(
|
|
||||||
source_omikron_id,
|
|
||||||
PrintType::Omega,
|
|
||||||
"Relay route loop for destination Iota {} and Omikron {}",
|
|
||||||
destination_iota,
|
|
||||||
destination_omikron
|
|
||||||
);
|
|
||||||
return Err(RelayRouteError::RouteLoop);
|
|
||||||
}
|
|
||||||
let Some(connection) = omikron_manager::get_connected_omikron(destination_omikron) else {
|
|
||||||
log_err!(
|
|
||||||
source_omikron_id,
|
|
||||||
PrintType::Omega,
|
|
||||||
"Relay destination Iota {} resolves to disconnected Omikron {}",
|
|
||||||
destination_iota,
|
|
||||||
destination_omikron
|
|
||||||
);
|
|
||||||
return Err(RelayRouteError::OmikronOffline);
|
|
||||||
};
|
|
||||||
let response = connection
|
|
||||||
.await_response(&frame, Duration::from_secs(20))
|
|
||||||
.await
|
|
||||||
.map_err(|error| {
|
|
||||||
log_err!(
|
|
||||||
source_omikron_id,
|
|
||||||
PrintType::Omega,
|
|
||||||
"Relay send to destination Iota {} via Omikron {} failed: {}",
|
|
||||||
destination_iota,
|
|
||||||
destination_omikron,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
RelayRouteError::Send(error.to_string())
|
|
||||||
})?;
|
|
||||||
if response.is_type(CommunicationType::Success) {
|
|
||||||
Ok(response)
|
|
||||||
} else {
|
|
||||||
Err(RelayRouteError::Send(format!(
|
|
||||||
"destination Omikron rejected the Relay with {}",
|
|
||||||
response.get_type()
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use mtp::codec::DataValue;
|
|
||||||
|
|
||||||
fn wire(target: RouteTarget) -> u64 {
|
|
||||||
let (kind, id) = match target {
|
|
||||||
RouteTarget::User(id) => (USER_TARGET_KIND, id),
|
|
||||||
RouteTarget::Iota(id) => (IOTA_TARGET_KIND, id),
|
|
||||||
};
|
|
||||||
kind | id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn relay_frame() -> CommunicationValue {
|
|
||||||
CommunicationValue::new(CommunicationType::Relay)
|
|
||||||
.without_sender()
|
|
||||||
.with_receiver(wire(RouteTarget::Iota(42)))
|
|
||||||
.with_payload(DataValue::Bytes(vec![1, 2, 3, 4]))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn forwarding_preserves_relay_payload_and_next_hop() {
|
|
||||||
let frame = relay_frame().with_receiver(wire(RouteTarget::Iota(7)));
|
|
||||||
let result = forward_relay_frame(&frame, wire(RouteTarget::Iota(42)));
|
|
||||||
assert!(result.is_ok());
|
|
||||||
let Ok(forwarded) = result else { return };
|
|
||||||
assert_eq!(forwarded.receiver(), Some(wire(RouteTarget::Iota(42))));
|
|
||||||
assert_eq!(forwarded.sender(), None);
|
|
||||||
assert_eq!(forwarded.payload(), frame.payload());
|
|
||||||
assert_eq!(forwarded.id(), frame.id());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn outer_sender_is_rejected_before_route_lookup() {
|
|
||||||
let frame = relay_frame().with_sender(9);
|
|
||||||
let result = route_from_omikron(1, frame).await;
|
|
||||||
assert!(matches!(result, Err(RelayRouteError::OuterSenderPresent)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn missing_destination_is_rejected_before_route_lookup() {
|
|
||||||
let frame = relay_frame().without_receiver();
|
|
||||||
let result = route_from_omikron(1, frame).await;
|
|
||||||
assert!(matches!(
|
|
||||||
result,
|
|
||||||
Err(RelayRouteError::MissingDestinationIota)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn user_route_target_is_rejected_by_opaque_omega_router() {
|
|
||||||
let frame = relay_frame().with_receiver(wire(RouteTarget::User(42)));
|
|
||||||
let result = route_from_omikron(1, frame).await;
|
|
||||||
assert!(matches!(
|
|
||||||
result,
|
|
||||||
Err(RelayRouteError::InvalidDestinationTarget(_))
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn unavailable_destination_is_reported_as_iota_offline() {
|
|
||||||
let result = route_from_omikron(1, relay_frame()).await;
|
|
||||||
assert!(matches!(result, Err(RelayRouteError::IotaOffline)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +1,293 @@
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use std::path::PathBuf;
|
use std::fs::{self, File};
|
||||||
|
use std::io::{self, BufReader, Read};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use zip::ZipArchive;
|
||||||
|
|
||||||
|
use crate::log;
|
||||||
|
|
||||||
static WORKING_DIR: Lazy<PathBuf> =
|
static WORKING_DIR: Lazy<PathBuf> =
|
||||||
Lazy::new(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
|
Lazy::new(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
|
||||||
|
|
||||||
pub fn load_file_vec(path: &str, name: &str) -> Result<Vec<u8>, std::io::Error> {
|
pub fn delete_file(path: &str, name: &str) -> bool {
|
||||||
let dir = PathBuf::from(get_directory()).join(path);
|
let dir = Path::new(&get_directory()).join(path);
|
||||||
|
let file = dir.join(name);
|
||||||
|
if !file.exists() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
fs::remove_file(file).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn delete_directory(path: &str) -> bool {
|
||||||
|
let dir = Path::new(&get_directory()).join(path);
|
||||||
|
delete_dir_recursive(&dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn delete_dir_recursive(directory: &Path) -> bool {
|
||||||
|
if !directory.exists() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if let Err(e) = fs::remove_dir_all(directory) {
|
||||||
|
log!(
|
||||||
|
"[IMPORTANT] Couldn't delete directory {}: {}",
|
||||||
|
directory.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn delete_user_directory(user_id: Uuid) {
|
||||||
|
let user_dir = Path::new(&get_directory())
|
||||||
|
.join("users")
|
||||||
|
.join(user_id.to_string());
|
||||||
|
let _ = delete_dir_recursive(&user_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_file_buf(path: &str, name: &str) -> io::Result<BufReader<File>> {
|
||||||
|
let dir = Path::new(&get_directory()).join(path);
|
||||||
let file_path = dir.join(name);
|
let file_path = dir.join(name);
|
||||||
|
|
||||||
|
// Ensure the directory exists, create if necessary
|
||||||
|
if !dir.exists() {
|
||||||
|
if let Err(_) = fs::create_dir_all(&dir) {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::NotFound,
|
||||||
|
"Directory creation failed",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the file if it doesn't exist
|
||||||
|
if !file_path.exists() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::NotFound,
|
||||||
|
"File creation failed",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the file and return a BufReader for efficient reading
|
||||||
|
let file = File::open(&file_path)?;
|
||||||
|
Ok(BufReader::new(file))
|
||||||
|
}
|
||||||
|
pub fn has_file(path: &str, name: &str) -> bool {
|
||||||
|
let dir = Path::new(&get_directory()).join(path);
|
||||||
|
let file_path = dir.join(name);
|
||||||
|
|
||||||
|
if !dir.exists() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !file_path.exists() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
pub fn has_dir(path: &str) -> bool {
|
||||||
|
let dir = Path::new(&get_directory()).join(path);
|
||||||
|
|
||||||
|
if !dir.exists() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_file(path: &str, name: &str) -> String {
|
||||||
|
let dir = Path::new(&get_directory()).join(path);
|
||||||
|
let file_path = dir.join(name);
|
||||||
|
|
||||||
|
if !dir.exists() {
|
||||||
|
if let Err(e) = fs::create_dir_all(&dir) {
|
||||||
|
log!("[IMPORTANT] Couldn't create directories: {}", e);
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !file_path.exists() {
|
||||||
|
if let Err(e) = File::create(&file_path) {
|
||||||
|
log!("[IMPORTANT] Couldn't create file: {}", e);
|
||||||
|
}
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut content = String::new();
|
||||||
|
if let Ok(mut f) = File::open(&file_path) {
|
||||||
|
let _ = f.read_to_string(&mut content);
|
||||||
|
}
|
||||||
|
content
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_file_vec(path: &str, name: &str) -> Result<Vec<u8>, std::io::Error> {
|
||||||
|
let dir = Path::new(&get_directory()).join(path);
|
||||||
|
let file_path = dir.join(name);
|
||||||
|
|
||||||
std::fs::read(file_path)
|
std::fs::read(file_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn save_file(path: &str, name: &str, value: &str) {
|
||||||
|
let dir = Path::new(&get_directory()).join(path);
|
||||||
|
let file_path = dir.join(name);
|
||||||
|
|
||||||
|
if !dir.exists() {
|
||||||
|
if let Err(e) = fs::create_dir_all(&dir) {
|
||||||
|
log!("[IMPORTANT] Couldn't create directories: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = fs::write(&file_path, value) {
|
||||||
|
log!(
|
||||||
|
"[IMPORTANT] Couldn't write file {}: {}",
|
||||||
|
file_path.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_children(path: &str) -> Vec<String> {
|
||||||
|
let dir = Path::new(&get_directory()).join(path);
|
||||||
|
let mut children = Vec::new();
|
||||||
|
if let Ok(entries) = fs::read_dir(&dir) {
|
||||||
|
for entry in entries {
|
||||||
|
if let Ok(entry) = entry {
|
||||||
|
children.push(entry.file_name().to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
children
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_directory() -> String {
|
pub fn get_directory() -> String {
|
||||||
WORKING_DIR.to_string_lossy().to_string()
|
WORKING_DIR.to_string_lossy().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn working_dir() -> &'static Path {
|
||||||
|
&WORKING_DIR
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to download the zip file content to a file on disk
|
||||||
|
#[allow(dead_code)]
|
||||||
|
async fn download_zip(url: &str, as_name: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let response = reqwest::get(url).await?;
|
||||||
|
|
||||||
|
// Check for successful response status
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(format!("Failed to download file: Status {}", response.status()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut zip_file = tokio::fs::File::create(as_name).await?;
|
||||||
|
let body = response.bytes().await?;
|
||||||
|
zip_file.write_all(&body).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code, deprecated)]
|
||||||
|
fn extract_zip_contents_to_folder(
|
||||||
|
zip_path: &Path,
|
||||||
|
target_dir: &Path,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let file = File::open(zip_path)?;
|
||||||
|
let mut archive = ZipArchive::new(file)?;
|
||||||
|
|
||||||
|
let staging_dir = target_dir.with_extension("staging");
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&staging_dir);
|
||||||
|
fs::create_dir_all(&staging_dir)?;
|
||||||
|
|
||||||
|
let mut first_item_name: Option<PathBuf> = None;
|
||||||
|
|
||||||
|
for i in 0..archive.len() {
|
||||||
|
let mut file = archive.by_index(i)?;
|
||||||
|
let entry_path = staging_dir.join(file.sanitized_name());
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
if file.name().ends_with('/') || file.sanitized_name().components().count() == 1 {
|
||||||
|
first_item_name = Some(file.sanitized_name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.name().ends_with('/') {
|
||||||
|
fs::create_dir_all(&entry_path)?;
|
||||||
|
} else {
|
||||||
|
if let Some(parent) = entry_path.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let mut out_file = File::create(entry_path)?;
|
||||||
|
io::copy(&mut file, &mut out_file)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(root_path) = first_item_name {
|
||||||
|
let root_dir = staging_dir.join(&root_path);
|
||||||
|
|
||||||
|
if root_dir.is_dir() {
|
||||||
|
let root_contents_count = fs::read_dir(&staging_dir)?.count();
|
||||||
|
|
||||||
|
if root_contents_count == 1
|
||||||
|
|| (root_contents_count > 1 && fs::metadata(&root_dir).is_ok())
|
||||||
|
{
|
||||||
|
let _ = fs::remove_dir_all(target_dir);
|
||||||
|
fs::create_dir_all(target_dir)?;
|
||||||
|
|
||||||
|
for entry in fs::read_dir(root_dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let src = entry.path();
|
||||||
|
let dest = target_dir.join(entry.file_name());
|
||||||
|
|
||||||
|
if let Err(_) = fs::rename(&src, &dest) {
|
||||||
|
if src.is_file() {
|
||||||
|
fs::copy(&src, &dest)?;
|
||||||
|
} else {
|
||||||
|
if entry.path().is_dir() {
|
||||||
|
fs::rename(&src, &dest)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&staging_dir);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log!("Extracting directly (no single root folder detected).");
|
||||||
|
let _ = fs::remove_dir_all(target_dir);
|
||||||
|
fs::rename(&staging_dir, target_dir)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn download_and_extract_zip(url: &str, as_name: &str) {
|
||||||
|
let base_dir = PathBuf::from(get_directory());
|
||||||
|
let zip_filename = format!("{}.zip", Uuid::new_v4());
|
||||||
|
let zip_path = base_dir.join(&zip_filename);
|
||||||
|
let target_dir = base_dir.join(as_name);
|
||||||
|
|
||||||
|
if let Err(e) = download_zip(url, &zip_path).await {
|
||||||
|
log!("Error downloading file: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let zip_path_clone = zip_path.clone();
|
||||||
|
let target_dir_clone = target_dir.clone();
|
||||||
|
let extract_result = extract_zip_contents_to_folder(&zip_path_clone, &target_dir_clone);
|
||||||
|
if let Err(e) = extract_result {
|
||||||
|
log!("Panic during ZIP extraction: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = tokio::fs::remove_file(&zip_path).await {
|
||||||
|
log!("Error cleaning up ZIP file {}: {}", zip_path.display(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,7 @@ pub fn log_cv_internal(
|
||||||
let formatted = format_cv(cv);
|
let formatted = format_cv(cv);
|
||||||
|
|
||||||
log_internal(
|
log_internal(
|
||||||
cv.sender().and_then(|sender| i64::try_from(sender).ok()),
|
Some(cv.get_sender() as i64),
|
||||||
print_type.unwrap_or(PrintType::General),
|
print_type.unwrap_or(PrintType::General),
|
||||||
prefix,
|
prefix,
|
||||||
false,
|
false,
|
||||||
|
|
@ -233,14 +233,14 @@ pub fn log_cv_internal(
|
||||||
pub fn format_cv(cv: &CommunicationValue) -> String {
|
pub fn format_cv(cv: &CommunicationValue) -> String {
|
||||||
let mut parts = Vec::new();
|
let mut parts = Vec::new();
|
||||||
|
|
||||||
let sender = cv.sender().filter(|sender| *sender != 0);
|
let sender = cv.get_sender();
|
||||||
let receiver = cv.receiver().filter(|receiver| *receiver != 0);
|
let receiver = cv.get_receiver();
|
||||||
|
|
||||||
if let (Some(sender), Some(receiver)) = (sender, receiver) {
|
if sender > 0 && receiver > 0 {
|
||||||
parts.push(format!("{} > {}", sender, receiver));
|
parts.push(format!("{} > {}", sender, receiver));
|
||||||
} else if let Some(sender) = sender {
|
} else if sender > 0 {
|
||||||
parts.push(format!("{}", sender));
|
parts.push(format!("{}", sender));
|
||||||
} else if let Some(receiver) = receiver {
|
} else if receiver > 0 {
|
||||||
parts.push(format!("> {}", receiver));
|
parts.push(format!("> {}", receiver));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,20 +248,16 @@ pub fn format_cv(cv: &CommunicationValue) -> String {
|
||||||
.get_comm_type_enum()
|
.get_comm_type_enum()
|
||||||
.map(|kind| kind.to_string())
|
.map(|kind| kind.to_string())
|
||||||
.unwrap_or_else(|| cv.get_type().to_string());
|
.unwrap_or_else(|| cv.get_type().to_string());
|
||||||
let id = cv
|
parts.push(format!("{} (id={})", comm_type, cv.get_id()));
|
||||||
.id()
|
|
||||||
.map(|id| id.to_string())
|
|
||||||
.unwrap_or_else(|| "missing".to_string());
|
|
||||||
parts.push(format!("{} (id={})", comm_type, id));
|
|
||||||
|
|
||||||
let data = cv.data().unwrap_or(&[]);
|
let data = cv.data();
|
||||||
|
|
||||||
let formated_data = format_data_container(
|
let formated_data = format_data_container(
|
||||||
data.iter().map(|(k, v)| (*k, v.clone())).collect(),
|
data.iter().map(|(k, v)| (*k, v.clone())).collect(),
|
||||||
Version(3, 0),
|
Version(1, 0),
|
||||||
);
|
);
|
||||||
|
|
||||||
parts.push(formated_data.to_string());
|
parts.push(format!("{}", formated_data));
|
||||||
|
|
||||||
parts.join(": ")
|
parts.join(": ")
|
||||||
}
|
}
|
||||||
|
|
@ -273,7 +269,7 @@ fn format_data_container(data: Vec<(DataTypeId, DataValue)>, version: Version) -
|
||||||
let key_str = key.to_string();
|
let key_str = key.to_string();
|
||||||
|
|
||||||
match value {
|
match value {
|
||||||
DataValue::Str(s) => format!("{}=\"{}\"", key_str, abbreviate_string(&s)),
|
DataValue::Str(s) => format!("{}=\"{}\"", key_str, s),
|
||||||
|
|
||||||
DataValue::Container(inner) => {
|
DataValue::Container(inner) => {
|
||||||
let inner_formatted = format_data_container(inner, version.clone());
|
let inner_formatted = format_data_container(inner, version.clone());
|
||||||
|
|
@ -304,7 +300,7 @@ fn format_array(arr: Vec<DataValue>, version: Version) -> String {
|
||||||
let parts: Vec<String> = arr
|
let parts: Vec<String> = arr
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|value| match value {
|
.map(|value| match value {
|
||||||
DataValue::Str(s) => format!("\"{}\"", abbreviate_string(&s)),
|
DataValue::Str(s) => format!("\"{}\"", s),
|
||||||
|
|
||||||
DataValue::Container(inner) => {
|
DataValue::Container(inner) => {
|
||||||
let inner_formatted = format_data_container(inner, version.clone());
|
let inner_formatted = format_data_container(inner, version.clone());
|
||||||
|
|
@ -329,32 +325,6 @@ fn format_array(arr: Vec<DataValue>, version: Version) -> String {
|
||||||
|
|
||||||
parts.join(", ")
|
parts.join(", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn abbreviate_string(value: &str) -> String {
|
|
||||||
const EDGE_LENGTH: usize = 4;
|
|
||||||
|
|
||||||
let chars: Vec<char> = value.chars().collect();
|
|
||||||
if chars.len() <= EDGE_LENGTH * 2 {
|
|
||||||
return value.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
let prefix: String = chars.iter().take(EDGE_LENGTH).collect();
|
|
||||||
let suffix: String = chars.iter().rev().take(EDGE_LENGTH).rev().collect();
|
|
||||||
format!("{prefix}...{suffix}")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::abbreviate_string;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn abbreviates_only_strings_longer_than_eight_characters() {
|
|
||||||
assert_eq!(abbreviate_string("12345678"), "12345678");
|
|
||||||
assert_eq!(abbreviate_string("123456789"), "1234...6789");
|
|
||||||
assert_eq!(abbreviate_string("YWJjZGVmZ2hpag=="), "YWJj...ag==");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! log_cv {
|
macro_rules! log_cv {
|
||||||
($kind:expr, $cv:expr) => {
|
($kind:expr, $cv:expr) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue