Start basic work on tunneling protocol and move some code into a common crate

This commit is contained in:
Josiah Glosson
2025-01-21 17:45:13 -06:00
parent c9c589c575
commit 0ede08415f
55 changed files with 590 additions and 385 deletions

1
.idea/code.iml generated
View File

@@ -10,6 +10,7 @@
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/tests" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/packages/app-lib/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/packages/rust-common/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />

80
Cargo.lock generated
View File

@@ -1204,7 +1204,7 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
dependencies = [
"byteorder",
"fnv",
"uuid 1.10.0",
"uuid 1.12.0",
]
[[package]]
@@ -1291,7 +1291,7 @@ dependencies = [
"time",
"tokio 1.42.0",
"url",
"uuid 1.10.0",
"uuid 1.12.0",
]
[[package]]
@@ -1995,7 +1995,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
dependencies = [
"serde",
"uuid 1.10.0",
"uuid 1.12.0",
]
[[package]]
@@ -2511,7 +2511,7 @@ checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4"
dependencies = [
"bit_field",
"flume",
"half",
"half 2.4.1",
"lebe",
"miniz_oxide 0.7.4",
"rayon-core",
@@ -3242,6 +3242,12 @@ dependencies = [
"tracing",
]
[[package]]
name = "half"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403"
[[package]]
name = "half"
version = "2.4.1"
@@ -4245,6 +4251,7 @@ dependencies = [
"deadpool-redis",
"derive-new",
"dotenvy",
"either",
"env_logger",
"flate2",
"futures 0.3.30",
@@ -4270,6 +4277,7 @@ dependencies = [
"redis",
"regex",
"reqwest 0.11.27",
"rust-common",
"rust-s3",
"rust_decimal",
"rust_iso3166",
@@ -4277,6 +4285,7 @@ dependencies = [
"sentry",
"sentry-actix",
"serde",
"serde_cbor",
"serde_json",
"serde_with",
"sha1 0.6.1",
@@ -4290,7 +4299,7 @@ dependencies = [
"totp-rs",
"url",
"urlencoding",
"uuid 1.10.0",
"uuid 1.12.0",
"validator",
"webp",
"woothee",
@@ -4657,7 +4666,7 @@ dependencies = [
"serde_json",
"thiserror 1.0.64",
"time",
"uuid 1.10.0",
"uuid 1.12.0",
"wasm-bindgen-futures",
"web-sys",
"yaup",
@@ -6724,7 +6733,7 @@ dependencies = [
"rkyv_derive",
"seahash",
"tinyvec",
"uuid 1.10.0",
"uuid 1.12.0",
]
[[package]]
@@ -6758,6 +6767,22 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rust-common"
version = "0.1.0"
dependencies = [
"actix-ws",
"chrono",
"either",
"rand 0.8.5",
"serde",
"serde_bytes",
"serde_cbor",
"serde_json",
"thiserror 2.0.7",
"uuid 1.12.0",
]
[[package]]
name = "rust-ini"
version = "0.18.0"
@@ -7108,7 +7133,7 @@ dependencies = [
"serde",
"serde_json",
"url",
"uuid 1.10.0",
"uuid 1.12.0",
]
[[package]]
@@ -7361,7 +7386,7 @@ dependencies = [
"thiserror 1.0.64",
"time",
"url",
"uuid 1.10.0",
"uuid 1.12.0",
]
[[package]]
@@ -7396,6 +7421,25 @@ dependencies = [
"xml-rs",
]
[[package]]
name = "serde_bytes"
version = "0.11.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a"
dependencies = [
"serde",
]
[[package]]
name = "serde_cbor"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
dependencies = [
"half 1.8.3",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.210"
@@ -8536,7 +8580,7 @@ dependencies = [
"thiserror 2.0.7",
"time",
"url",
"uuid 1.10.0",
"uuid 1.12.0",
"walkdir",
]
@@ -8629,7 +8673,7 @@ dependencies = [
"thiserror 2.0.7",
"toml 0.8.19",
"url",
"uuid 1.10.0",
"uuid 1.12.0",
]
[[package]]
@@ -8810,7 +8854,7 @@ dependencies = [
"toml 0.8.19",
"url",
"urlpattern",
"uuid 1.10.0",
"uuid 1.12.0",
"walkdir",
]
@@ -8884,6 +8928,7 @@ dependencies = [
"dirs 5.0.1",
"discord-rich-presence",
"dunce",
"either",
"flate2",
"futures 0.3.30",
"indicatif",
@@ -8895,6 +8940,7 @@ dependencies = [
"rand 0.8.5",
"regex",
"reqwest 0.12.7",
"rust-common",
"serde",
"serde_ini",
"serde_json",
@@ -8913,7 +8959,7 @@ dependencies = [
"tracing-subscriber",
"url",
"urlencoding",
"uuid 1.10.0",
"uuid 1.12.0",
"whoami",
"winreg 0.52.0",
"zip 0.6.6",
@@ -8955,7 +9001,7 @@ dependencies = [
"tracing",
"tracing-error",
"url",
"uuid 1.10.0",
"uuid 1.12.0",
"window-shadows",
]
@@ -8974,7 +9020,7 @@ dependencies = [
"tracing-error",
"tracing-subscriber",
"url",
"uuid 1.10.0",
"uuid 1.12.0",
"webbrowser",
]
@@ -9819,9 +9865,9 @@ dependencies = [
[[package]]
name = "uuid"
version = "1.10.0"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4"
dependencies = [
"getrandom 0.2.15",
"rand 0.8.5",

View File

@@ -2,11 +2,12 @@
resolver = '2'
members = [
'./packages/app-lib',
'./packages/rust-common',
'./apps/app-playground',
'./apps/app',
'./apps/labrinth',
'./apps/daedalus_client',
'./packages/daedalus',
'./apps/daedalus_client',
'./packages/daedalus',
]
# Optimize for speed and reduce size on release builds
@@ -21,4 +22,4 @@ strip = true # Remove debug symbols
opt-level = 3
[patch.crates-io]
wry = { git = "https://github.com/modrinth/wry", rev ="cdbf938" }
wry = { git = "https://github.com/modrinth/wry", rev ="cdbf938" }

View File

@@ -37,6 +37,7 @@ hyper = { version = "0.14", features = ["full"] }
hyper-tls = "0.5.0"
serde_json = "1.0"
serde_cbor = "0.11"
serde = { version = "1.0", features = ["derive"] }
serde_with = "3.0.0"
chrono = { version = "0.4.26", features = ["serde"] }
@@ -74,6 +75,7 @@ dotenvy = "0.15.7"
log = "0.4.20"
env_logger = "0.10.1"
thiserror = "1.0.56"
either = "1.13"
sqlx = { version = "0.8.2", features = [
"runtime-tokio-rustls",
@@ -124,6 +126,8 @@ async-stripe = { version = "0.39.1", features = ["runtime-tokio-hyper-rustls"] }
rusty-money = "0.4.1"
json-patch = "*"
rust-common = { path = "../../packages/rust-common" }
[dev-dependencies]
actix-http = "3.4.0"

View File

@@ -34,7 +34,7 @@ pub enum AuthenticationError {
#[error("Error uploading user profile picture")]
FileHosting(#[from] FileHostingError),
#[error("Error while decoding PAT: {0}")]
Decoding(#[from] crate::models::ids::DecodingError),
Decoding(#[from] rust_common::ids::DecodingError),
#[error("{0}")]
Mail(#[from] email::MailError),
#[error("Invalid Authentication Credentials")]

View File

@@ -1,9 +1,9 @@
use super::ValidatedRedirectUri;
use crate::auth::AuthenticationError;
use crate::models::error::ApiError;
use crate::models::ids::DecodingError;
use actix_web::http::{header::LOCATION, StatusCode};
use actix_web::HttpResponse;
use rust_common::ids::DecodingError;
#[derive(thiserror::Error, Debug)]
#[error("{}", .error_type)]

View File

@@ -1,9 +1,9 @@
use super::DatabaseError;
use crate::models::ids::base62_impl::to_base62;
use crate::models::ids::{random_base62_rng, random_base62_rng_range};
use censor::Censor;
use rand::SeedableRng;
use rand_chacha::ChaCha20Rng;
use rust_common::ids::base62_impl::to_base62;
use rust_common::ids::{random_base62_rng, random_base62_rng_range};
use serde::{Deserialize, Serialize};
use sqlx::sqlx_macros::Type;

View File

@@ -1,8 +1,7 @@
use crate::{
database::redis::RedisPool, models::ids::base62_impl::parse_base62,
};
use crate::database::redis::RedisPool;
use dashmap::DashMap;
use futures::TryStreamExt;
use rust_common::ids::base62_impl::parse_base62;
use std::fmt::{Debug, Display};
use std::hash::Hash;

View File

@@ -1,11 +1,11 @@
use super::ids::*;
use crate::database::models::DatabaseError;
use crate::database::redis::RedisPool;
use crate::models::ids::base62_impl::parse_base62;
use crate::models::pats::Scopes;
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use futures::TryStreamExt;
use rust_common::ids::base62_impl::parse_base62;
use serde::{Deserialize, Serialize};
use std::fmt::{Debug, Display};
use std::hash::Hash;

View File

@@ -6,12 +6,12 @@ use super::{ids::*, User};
use crate::database::models;
use crate::database::models::DatabaseError;
use crate::database::redis::RedisPool;
use crate::models::ids::base62_impl::parse_base62;
use crate::models::projects::{MonetizationStatus, ProjectStatus};
use chrono::{DateTime, Utc};
use dashmap::{DashMap, DashSet};
use futures::TryStreamExt;
use itertools::Itertools;
use rust_common::ids::base62_impl::parse_base62;
use serde::{Deserialize, Serialize};
use std::fmt::{Debug, Display};
use std::hash::Hash;
@@ -300,7 +300,7 @@ impl Project {
slug, color, monetization_status, organization_id
)
VALUES (
$1, $2, $3, $4, $5, $6,
$1, $2, $3, $4, $5, $6,
$7, $8, $9, $10, $11,
$12, $13,
LOWER($14), $15, $16, $17

View File

@@ -1,9 +1,9 @@
use super::ids::*;
use crate::database::models::DatabaseError;
use crate::database::redis::RedisPool;
use crate::models::ids::base62_impl::parse_base62;
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use rust_common::ids::base62_impl::parse_base62;
use serde::{Deserialize, Serialize};
use std::fmt::{Debug, Display};
use std::hash::Hash;

View File

@@ -3,10 +3,10 @@ use super::{CollectionId, ReportId, ThreadId};
use crate::database::models;
use crate::database::models::{DatabaseError, OrganizationId};
use crate::database::redis::RedisPool;
use crate::models::ids::base62_impl::{parse_base62, to_base62};
use crate::models::users::Badges;
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use rust_common::ids::base62_impl::{parse_base62, to_base62};
use serde::{Deserialize, Serialize};
use std::fmt::{Debug, Display};
use std::hash::Hash;

View File

@@ -1,9 +1,9 @@
use super::models::DatabaseError;
use crate::models::ids::base62_impl::{parse_base62, to_base62};
use chrono::{TimeZone, Utc};
use dashmap::DashMap;
use deadpool_redis::{Config, Runtime};
use redis::{cmd, Cmd, ExistenceCheck, SetExpiry, SetOptions};
use rust_common::ids::base62_impl::{parse_base62, to_base62};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

View File

@@ -297,7 +297,7 @@ pub fn app_setup(
}
let ip_salt = Pepper {
pepper: models::ids::Base62Id(models::ids::random_base62(11))
pepper: rust_common::ids::Base62Id(rust_common::ids::random_base62(11))
.to_string(),
};

View File

@@ -16,114 +16,10 @@ pub use super::users::UserId;
pub use crate::models::billing::{
ChargeId, ProductId, ProductPriceId, UserSubscriptionId,
};
use thiserror::Error;
use rust_common::ids::base62_id_impl;
pub use rust_common::ids::Base62Id;
/// Generates a random 64 bit integer that is exactly `n` characters
/// long when encoded as base62.
///
/// Uses `rand`'s thread rng on every call.
///
/// # Panics
///
/// This method panics if `n` is 0 or greater than 11, since a `u64`
/// can only represent up to 11 character base62 strings
#[inline]
pub fn random_base62(n: usize) -> u64 {
random_base62_rng(&mut rand::thread_rng(), n)
}
/// Generates a random 64 bit integer that is exactly `n` characters
/// long when encoded as base62, using the given rng.
///
/// # Panics
///
/// This method panics if `n` is 0 or greater than 11, since a `u64`
/// can only represent up to 11 character base62 strings
pub fn random_base62_rng<R: rand::RngCore>(rng: &mut R, n: usize) -> u64 {
random_base62_rng_range(rng, n, n)
}
pub fn random_base62_rng_range<R: rand::RngCore>(
rng: &mut R,
n_min: usize,
n_max: usize,
) -> u64 {
use rand::Rng;
assert!(n_min > 0 && n_max <= 11 && n_min <= n_max);
// gen_range is [low, high): max value is `MULTIPLES[n] - 1`,
// which is n characters long when encoded
rng.gen_range(MULTIPLES[n_min - 1]..MULTIPLES[n_max])
}
const MULTIPLES: [u64; 12] = [
1,
62,
62 * 62,
62 * 62 * 62,
62 * 62 * 62 * 62,
62 * 62 * 62 * 62 * 62,
62 * 62 * 62 * 62 * 62 * 62,
62 * 62 * 62 * 62 * 62 * 62 * 62,
62 * 62 * 62 * 62 * 62 * 62 * 62 * 62,
62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62,
62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62,
u64::MAX,
];
/// An ID encoded as base62 for use in the API.
///
/// All ids should be random and encode to 8-10 character base62 strings,
/// to avoid enumeration and other attacks.
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct Base62Id(pub u64);
/// An error decoding a number from base62.
#[derive(Error, Debug)]
pub enum DecodingError {
/// Encountered a non-base62 character in a base62 string
#[error("Invalid character {0:?} in base62 encoding")]
InvalidBase62(char),
/// Encountered integer overflow when decoding a base62 id.
#[error("Base62 decoding overflowed")]
Overflow,
}
macro_rules! from_base62id {
($($struct:ty, $con:expr;)+) => {
$(
impl From<Base62Id> for $struct {
fn from(id: Base62Id) -> $struct {
$con(id.0)
}
}
impl From<$struct> for Base62Id {
fn from(id: $struct) -> Base62Id {
Base62Id(id.0)
}
}
)+
};
}
macro_rules! impl_base62_display {
($struct:ty) => {
impl std::fmt::Display for $struct {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&base62_impl::to_base62(self.0))
}
}
};
}
impl_base62_display!(Base62Id);
macro_rules! base62_id_impl {
($struct:ty, $cons:expr) => {
from_base62id!($struct, $cons;);
impl_base62_display!($struct);
}
}
base62_id_impl!(ProjectId, ProjectId);
base62_id_impl!(UserId, UserId);
base62_id_impl!(VersionId, VersionId);
base62_id_impl!(CollectionId, CollectionId);
base62_id_impl!(TeamId, TeamId);
@@ -143,91 +39,3 @@ base62_id_impl!(ProductId, ProductId);
base62_id_impl!(ProductPriceId, ProductPriceId);
base62_id_impl!(UserSubscriptionId, UserSubscriptionId);
base62_id_impl!(ChargeId, ChargeId);
pub mod base62_impl {
use serde::de::{self, Deserializer, Visitor};
use serde::ser::Serializer;
use serde::{Deserialize, Serialize};
use super::{Base62Id, DecodingError};
impl<'de> Deserialize<'de> for Base62Id {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct Base62Visitor;
impl Visitor<'_> for Base62Visitor {
type Value = Base62Id;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
formatter.write_str("a base62 string id")
}
fn visit_str<E>(self, string: &str) -> Result<Base62Id, E>
where
E: de::Error,
{
parse_base62(string).map(Base62Id).map_err(E::custom)
}
}
deserializer.deserialize_str(Base62Visitor)
}
}
impl Serialize for Base62Id {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&to_base62(self.0))
}
}
const BASE62_CHARS: [u8; 62] =
*b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
pub fn to_base62(mut num: u64) -> String {
let length = (num as f64).log(62.0).ceil() as usize;
let mut output = String::with_capacity(length);
while num > 0 {
// Could be done more efficiently, but requires byte
// manipulation of strings & Vec<u8> -> String conversion
output.insert(0, BASE62_CHARS[(num % 62) as usize] as char);
num /= 62;
}
output
}
pub fn parse_base62(string: &str) -> Result<u64, DecodingError> {
let mut num: u64 = 0;
for c in string.chars() {
let next_digit;
if c.is_ascii_digit() {
next_digit = (c as u8 - b'0') as u64;
} else if c.is_ascii_uppercase() {
next_digit = 10 + (c as u8 - b'A') as u64;
} else if c.is_ascii_lowercase() {
next_digit = 36 + (c as u8 - b'a') as u64;
} else {
return Err(DecodingError::InvalidBase62(c));
}
// We don't want this panicking or wrapping on integer overflow
if let Some(n) =
num.checked_mul(62).and_then(|n| n.checked_add(next_digit))
{
num = n;
} else {
return Err(DecodingError::Overflow);
}
}
Ok(num)
}
}

View File

@@ -1,14 +1,9 @@
use super::ids::Base62Id;
use crate::{auth::AuthProvider, bitflags_serde_impl};
use chrono::{DateTime, Utc};
pub use rust_common::users::{UserId, UserStatus};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
#[serde(from = "Base62Id")]
#[serde(into = "Base62Id")]
pub struct UserId(pub u64);
pub const DELETED_USER: UserId = UserId(127155982985829);
bitflags::bitflags! {
@@ -211,10 +206,3 @@ impl UserFriend {
}
}
}
#[derive(Serialize, Deserialize, Clone)]
pub struct UserStatus {
pub user_id: UserId,
pub profile_name: Option<String>,
pub last_update: DateTime<Utc>,
}

View File

@@ -15,12 +15,12 @@ use crate::search::SearchConfig;
use crate::util::date::get_current_tenths_of_ms;
use crate::util::guards::admin_key_guard;
use actix_web::{get, patch, post, web, HttpRequest, HttpResponse};
use log::info;
use serde::Deserialize;
use sqlx::PgPool;
use std::collections::HashMap;
use std::net::Ipv4Addr;
use std::sync::Arc;
use log::info;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
@@ -74,7 +74,7 @@ pub async fn count_download(
let project_id: crate::database::models::ids::ProjectId =
download_body.project_id.into();
let id_option = crate::models::ids::base62_impl::parse_base62(
let id_option = rust_common::ids::base62_impl::parse_base62(
&download_body.version_name,
)
.ok()

View File

@@ -10,7 +10,6 @@ use crate::models::billing::{
Product, ProductMetadata, ProductPrice, SubscriptionMetadata,
SubscriptionStatus, UserSubscription,
};
use crate::models::ids::base62_impl::{parse_base62, to_base62};
use crate::models::pats::Scopes;
use crate::models::users::Badges;
use crate::queue::session::AuthQueue;
@@ -18,6 +17,7 @@ use crate::routes::ApiError;
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
use chrono::Utc;
use log::{info, warn};
use rust_common::ids::base62_impl::{parse_base62, to_base62};
use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal;
use serde::Serialize;

View File

@@ -4,8 +4,6 @@ use crate::auth::{get_user_from_headers, AuthProvider, AuthenticationError};
use crate::database::models::flow_item::Flow;
use crate::database::redis::RedisPool;
use crate::file_hosting::FileHost;
use crate::models::ids::base62_impl::{parse_base62, to_base62};
use crate::models::ids::random_base62_rng;
use crate::models::pats::Scopes;
use crate::models::users::{Badges, Role};
use crate::queue::session::AuthQueue;
@@ -25,6 +23,8 @@ use chrono::{Duration, Utc};
use rand_chacha::rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
use reqwest::header::AUTHORIZATION;
use rust_common::ids::base62_impl::{parse_base62, to_base62};
use rust_common::ids::random_base62_rng;
use serde::{Deserialize, Serialize};
use sqlx::postgres::PgPool;
use std::collections::HashMap;

View File

@@ -1,12 +1,12 @@
use super::ApiError;
use crate::database;
use crate::database::redis::RedisPool;
use crate::models::ids::random_base62;
use crate::models::projects::ProjectStatus;
use crate::queue::moderation::{ApprovalType, IdentifiedFile, MissingMetadata};
use crate::queue::session::AuthQueue;
use crate::{auth::check_is_moderator_from_headers, models::pats::Scopes};
use actix_web::{web, HttpRequest, HttpResponse};
use rust_common::ids::random_base62;
use serde::Deserialize;
use sqlx::PgPool;
use std::collections::HashMap;

View File

@@ -2,9 +2,8 @@ use crate::auth::validate::get_user_record_from_bearer_token;
use crate::auth::AuthenticationError;
use crate::database::models::friend_item::FriendItem;
use crate::database::redis::RedisPool;
use crate::models::ids::UserId;
use crate::models::pats::Scopes;
use crate::models::users::{User, UserStatus};
use crate::models::users::User;
use crate::queue::session::AuthQueue;
use crate::queue::socket::ActiveSockets;
use crate::routes::ApiError;
@@ -12,30 +11,20 @@ use actix_web::web::{Data, Payload};
use actix_web::{get, web, HttpRequest, HttpResponse};
use actix_ws::Message;
use chrono::Utc;
use either::Either;
use futures_util::{StreamExt, TryStreamExt};
use serde::{Deserialize, Serialize};
use rust_common::ids::UserId;
use rust_common::networking::message::{
ClientToServerMessage, ServerToClientMessage,
};
use rust_common::users::UserStatus;
use serde::Deserialize;
use sqlx::PgPool;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(ws_init);
}
#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ClientToServerMessage {
StatusUpdate { profile_name: Option<String> },
}
#[derive(Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ServerToClientMessage {
StatusUpdate { status: UserStatus },
UserOffline { id: UserId },
FriendStatuses { statuses: Vec<UserStatus> },
FriendRequest { from: UserId },
FriendRequestRejected { from: UserId },
}
#[derive(Deserialize)]
struct LauncherHeartbeatInit {
code: String,
@@ -133,57 +122,59 @@ pub async fn ws_init(
actix_web::rt::spawn(async move {
// receive messages from websocket
while let Some(msg) = stream.next().await {
match msg {
Ok(Message::Text(text)) => {
if let Ok(message) =
serde_json::from_str::<ClientToServerMessage>(&text)
{
match message {
ClientToServerMessage::StatusUpdate {
profile_name,
} => {
if let Some(mut pair) =
db.auth_sockets.get_mut(&user.id)
if msg.is_err() {
continue;
}
match ClientToServerMessage::deserialize(msg.unwrap()) {
Ok(Either::Left(message)) => {
match message {
ClientToServerMessage::StatusUpdate {
profile_name,
} => {
if let Some(mut pair) =
db.auth_sockets.get_mut(&user.id)
{
let (status, _) = pair.value_mut();
if status
.profile_name
.as_ref()
.map(|x| x.len() > 64)
.unwrap_or(false)
{
let (status, _) = pair.value_mut();
if status
.profile_name
.as_ref()
.map(|x| x.len() > 64)
.unwrap_or(false)
{
continue;
}
status.profile_name = profile_name;
status.last_update = Utc::now();
let user_status = status.clone();
// We drop the pair to avoid holding the lock for too long
drop(pair);
let _ = broadcast_friends(
user.id,
ServerToClientMessage::StatusUpdate {
status: user_status,
},
&pool,
&db,
None,
)
.await;
return;
}
status.profile_name = profile_name;
status.last_update = Utc::now();
let user_status = status.clone();
// We drop the pair to avoid holding the lock for too long
drop(pair);
let _ = broadcast_friends(
user.id,
ServerToClientMessage::StatusUpdate {
status: user_status,
},
&pool,
&db,
None,
)
.await;
}
}
ClientToServerMessage::SocketOpen
| ClientToServerMessage::SocketClose { .. }
| ClientToServerMessage::SocketSend { .. } => todo!(),
}
}
Ok(Message::Close(_)) => {
Ok(Either::Right(Message::Close(_))) => {
let _ = close_socket(user.id, &pool, &db).await;
}
Ok(Message::Ping(msg)) => {
Ok(Either::Right(Message::Ping(msg))) => {
if let Some(socket) = db.auth_sockets.get(&user.id) {
let (_, socket) = socket.value();
let _ = socket.clone().pong(&msg).await;
@@ -207,6 +198,7 @@ pub async fn broadcast_friends(
sockets: &ActiveSockets,
friends: Option<Vec<FriendItem>>,
) -> Result<(), crate::database::models::DatabaseError> {
// FIXME Probably shouldn't be using database errors for this
let friends = if let Some(friends) = friends {
friends
} else {
@@ -224,8 +216,7 @@ pub async fn broadcast_friends(
if let Some(socket) = sockets.auth_sockets.get(&friend_id.into()) {
let (_, socket) = socket.value();
let _ =
socket.clone().text(serde_json::to_string(&message)?).await;
let _ = message.send(&mut socket.clone()).await; // FIXME Probably shouldn't swallow this error
}
}
}

View File

@@ -164,7 +164,7 @@ async fn find_version(
pool: &PgPool,
redis: &RedisPool,
) -> Result<Option<QueryVersion>, ApiError> {
let id_option = crate::models::ids::base62_impl::parse_base62(vcoords)
let id_option = rust_common::ids::base62_impl::parse_base62(vcoords)
.ok()
.map(|x| x as i64);

View File

@@ -117,7 +117,7 @@ pub enum ApiError {
#[error("Captcha Error. Try resubmitting the form.")]
Turnstile,
#[error("Error while decoding Base62: {0}")]
Decoding(#[from] crate::models::ids::DecodingError),
Decoding(#[from] rust_common::ids::DecodingError),
#[error("Image Parsing Error: {0}")]
ImageParse(#[from] image::ImageError),
#[error("Password Hashing Error: {0}")]

View File

@@ -6,13 +6,14 @@ use crate::{
auth::get_user_from_headers,
database::models::user_item,
models::{
ids::{base62_impl::to_base62, ProjectId, VersionId},
ids::{ProjectId, VersionId},
pats::Scopes,
},
queue::session::AuthQueue,
};
use actix_web::{web, HttpRequest, HttpResponse};
use chrono::{DateTime, Duration, Utc};
use rust_common::ids::base62_impl::to_base62;
use serde::{Deserialize, Serialize};
use sqlx::postgres::types::PgInterval;
use sqlx::PgPool;

View File

@@ -6,7 +6,6 @@ use crate::database::models::{
use crate::database::redis::RedisPool;
use crate::file_hosting::FileHost;
use crate::models::collections::{Collection, CollectionStatus};
use crate::models::ids::base62_impl::parse_base62;
use crate::models::ids::{CollectionId, ProjectId};
use crate::models::pats::Scopes;
use crate::queue::session::AuthQueue;
@@ -20,6 +19,7 @@ use actix_web::web::Data;
use actix_web::{web, HttpRequest, HttpResponse};
use chrono::Utc;
use itertools::Itertools;
use rust_common::ids::base62_impl::parse_base62;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use std::sync::Arc;

View File

@@ -5,10 +5,11 @@ use crate::models::pats::Scopes;
use crate::models::users::UserFriend;
use crate::queue::session::AuthQueue;
use crate::queue::socket::ActiveSockets;
use crate::routes::internal::statuses::{close_socket, ServerToClientMessage};
use crate::routes::internal::statuses::close_socket;
use crate::routes::ApiError;
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
use chrono::Utc;
use rust_common::networking::message::ServerToClientMessage;
use sqlx::PgPool;
pub fn config(cfg: &mut web::ServiceConfig) {

View File

@@ -1,18 +1,5 @@
use std::{collections::HashSet, fmt::Display, sync::Arc};
use actix_web::{
delete, get, patch, post,
web::{self, scope},
HttpRequest, HttpResponse,
};
use chrono::Utc;
use itertools::Itertools;
use rand::{distributions::Alphanumeric, Rng, SeedableRng};
use rand_chacha::ChaCha20Rng;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use validator::Validate;
use super::ApiError;
use crate::{
auth::{checks::ValidateAuthorized, get_user_from_headers},
@@ -35,13 +22,22 @@ use crate::{
util::validate::validation_errors_to_string,
};
use crate::{
file_hosting::FileHost,
models::{
ids::base62_impl::parse_base62,
oauth_clients::DeleteOAuthClientQueryParam,
},
file_hosting::FileHost, models::oauth_clients::DeleteOAuthClientQueryParam,
util::routes::read_from_payload,
};
use actix_web::{
delete, get, patch, post,
web::{self, scope},
HttpRequest, HttpResponse,
};
use chrono::Utc;
use itertools::Itertools;
use rand::{distributions::Alphanumeric, Rng, SeedableRng};
use rand_chacha::ChaCha20Rng;
use rust_common::ids::base62_impl::parse_base62;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use validator::Validate;
use crate::database::models::oauth_client_item::OAuthClient as DBOAuthClient;
use crate::models::ids::OAuthClientId as ApiOAuthClientId;

View File

@@ -9,7 +9,6 @@ use crate::database::models::{
};
use crate::database::redis::RedisPool;
use crate::file_hosting::FileHost;
use crate::models::ids::base62_impl::parse_base62;
use crate::models::ids::UserId;
use crate::models::organizations::OrganizationId;
use crate::models::pats::Scopes;
@@ -22,6 +21,7 @@ use crate::util::validate::validation_errors_to_string;
use crate::{database, models};
use actix_web::{web, HttpRequest, HttpResponse};
use futures::TryStreamExt;
use rust_common::ids::base62_impl::parse_base62;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
@@ -786,7 +786,7 @@ pub async fn organization_projects_add(
let organization_owner_user_id = sqlx::query!(
"
SELECT u.id
SELECT u.id
FROM team_members
INNER JOIN users u ON u.id = team_members.user_id
WHERE team_id = $1 AND is_owner = TRUE
@@ -969,7 +969,7 @@ pub async fn organization_projects_remove(
sqlx::query!(
"
UPDATE team_members
SET
SET
is_owner = TRUE,
accepted = TRUE,
permissions = $2,

View File

@@ -8,7 +8,6 @@ use crate::database::models::{self, image_item, User};
use crate::database::redis::RedisPool;
use crate::file_hosting::{FileHost, FileHostingError};
use crate::models::error::ApiError;
use crate::models::ids::base62_impl::to_base62;
use crate::models::ids::{ImageId, OrganizationId};
use crate::models::images::{Image, ImageContext};
use crate::models::pats::Scopes;
@@ -32,6 +31,7 @@ use chrono::Utc;
use futures::stream::StreamExt;
use image::ImageError;
use itertools::Itertools;
use rust_common::ids::base62_impl::to_base62;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use sqlx::postgres::PgPool;

View File

@@ -11,7 +11,6 @@ use crate::database::redis::RedisPool;
use crate::database::{self, models as db_models};
use crate::file_hosting::FileHost;
use crate::models;
use crate::models::ids::base62_impl::parse_base62;
use crate::models::images::ImageContext;
use crate::models::notifications::NotificationBody;
use crate::models::pats::Scopes;
@@ -33,6 +32,7 @@ use actix_web::{web, HttpRequest, HttpResponse};
use chrono::Utc;
use futures::TryStreamExt;
use itertools::Itertools;
use rust_common::ids::base62_impl::parse_base62;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sqlx::PgPool;

View File

@@ -6,9 +6,7 @@ use crate::database::models::thread_item::{
};
use crate::database::redis::RedisPool;
use crate::models::ids::ImageId;
use crate::models::ids::{
base62_impl::parse_base62, ProjectId, UserId, VersionId,
};
use crate::models::ids::{ProjectId, UserId, VersionId};
use crate::models::images::{Image, ImageContext};
use crate::models::pats::Scopes;
use crate::models::reports::{ItemType, Report};
@@ -19,6 +17,7 @@ use crate::util::img;
use actix_web::{web, HttpRequest, HttpResponse};
use chrono::Utc;
use futures::StreamExt;
use rust_common::ids::base62_impl::parse_base62;
use serde::Deserialize;
use sqlx::PgPool;
use validator::Validate;

View File

@@ -13,7 +13,6 @@ use crate::database::models::version_item::{DependencyBuilder, LoaderVersion};
use crate::database::models::{image_item, Organization};
use crate::database::redis::RedisPool;
use crate::models;
use crate::models::ids::base62_impl::parse_base62;
use crate::models::ids::VersionId;
use crate::models::images::ImageContext;
use crate::models::pats::Scopes;
@@ -29,6 +28,7 @@ use crate::util::img;
use crate::util::validate::validation_errors_to_string;
use actix_web::{web, HttpRequest, HttpResponse};
use itertools::Itertools;
use rust_common::ids::base62_impl::parse_base62;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use validator::Validate;
@@ -444,7 +444,7 @@ pub async fn version_edit_helper(
.collect::<Vec<i32>>();
sqlx::query!(
"
DELETE FROM version_fields
DELETE FROM version_fields
WHERE version_id = $1
AND field_id = ANY($2)
",

View File

@@ -2,13 +2,13 @@
pub mod local_import;
use crate::database::redis::RedisPool;
use crate::models::ids::base62_impl::to_base62;
use crate::search::{SearchConfig, UploadSearchProject};
use local_import::index_local;
use log::info;
use meilisearch_sdk::client::{Client, SwapIndexes};
use meilisearch_sdk::indexes::Index;
use meilisearch_sdk::settings::{PaginationSetting, Settings};
use rust_common::ids::base62_impl::to_base62;
use sqlx::postgres::PgPool;
use thiserror::Error;
#[derive(Error, Debug)]

View File

@@ -1,9 +1,9 @@
use crate::database::models::legacy_loader_fields::MinecraftGameVersion;
use crate::database::redis::RedisPool;
use crate::models::ids::base62_impl::to_base62;
use crate::models::projects::ProjectId;
use crate::routes::ApiError;
use chrono::{DateTime, Utc};
use rust_common::ids::base62_impl::to_base62;
use serde::Serialize;
use sqlx::PgPool;

View File

@@ -7,9 +7,9 @@ use common::{
environment::{with_test_environment, TestEnvironment},
};
use itertools::Itertools;
use labrinth::models::ids::base62_impl::parse_base62;
use labrinth::models::teams::ProjectPermissions;
use labrinth::queue::payouts;
use rust_common::ids::base62_impl::parse_base62;
use rust_decimal::{prelude::ToPrimitive, Decimal};
mod common;

View File

@@ -12,10 +12,10 @@ use futures::StreamExt;
use labrinth::database::models::project_item::{
PROJECTS_NAMESPACE, PROJECTS_SLUGS_NAMESPACE,
};
use labrinth::models::ids::base62_impl::parse_base62;
use labrinth::models::projects::ProjectId;
use labrinth::models::teams::ProjectPermissions;
use labrinth::util::actix::{MultipartSegment, MultipartSegmentData};
use rust_common::ids::base62_impl::parse_base62;
use serde_json::json;
use crate::common::api_common::models::CommonProject;

View File

@@ -18,10 +18,10 @@ use common::environment::{
with_test_environment, with_test_environment_all, TestEnvironment,
};
use common::{database::*, scopes::ScopeTest};
use labrinth::models::ids::base62_impl::parse_base62;
use labrinth::models::pats::Scopes;
use labrinth::models::projects::ProjectId;
use labrinth::models::users::UserId;
use rust_common::ids::base62_impl::parse_base62;
use serde_json::json;
// For each scope, we (using test_scope):

View File

@@ -8,7 +8,7 @@ use common::environment::with_test_environment;
use common::environment::TestEnvironment;
use common::search::setup_search_projects;
use futures::stream::StreamExt;
use labrinth::models::ids::base62_impl::parse_base62;
use rust_common::ids::base62_impl::parse_base62;
use serde_json::json;
use crate::common::api_common::Api;

View File

@@ -20,12 +20,10 @@ use futures::StreamExt;
use itertools::Itertools;
use labrinth::{
database::models::project_item::PROJECTS_SLUGS_NAMESPACE,
models::{
ids::base62_impl::parse_base62, projects::ProjectId,
teams::ProjectPermissions,
},
models::{projects::ProjectId, teams::ProjectPermissions},
util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData},
};
use rust_common::ids::base62_impl::parse_base62;
use serde_json::json;
#[actix_rt::test]

View File

@@ -6,9 +6,9 @@ use crate::common::dummy_data::TestFile;
use crate::common::environment::with_test_environment;
use crate::common::environment::TestEnvironment;
use crate::common::scopes::ScopeTest;
use labrinth::models::ids::base62_impl::parse_base62;
use labrinth::models::pats::Scopes;
use labrinth::models::projects::ProjectId;
use rust_common::ids::base62_impl::parse_base62;
// Project version creation scopes
#[actix_rt::test]

View File

@@ -11,7 +11,7 @@ use crate::common::environment::with_test_environment;
use crate::common::environment::TestEnvironment;
use actix_http::StatusCode;
use futures::stream::StreamExt;
use labrinth::models::ids::base62_impl::parse_base62;
use rust_common::ids::base62_impl::parse_base62;
use serde_json::json;
use std::collections::HashMap;
use std::sync::Arc;

View File

@@ -14,11 +14,11 @@ use common::database::USER_USER_PAT;
use common::environment::{with_test_environment, with_test_environment_all};
use futures::StreamExt;
use labrinth::database::models::version_item::VERSIONS_NAMESPACE;
use labrinth::models::ids::base62_impl::parse_base62;
use labrinth::models::projects::{
Dependency, DependencyType, VersionId, VersionStatus, VersionType,
};
use labrinth::routes::v3::version_file::FileUpdateData;
use rust_common::ids::base62_impl::parse_base62;
use serde_json::json;
// importing common module.

View File

@@ -29,6 +29,7 @@ regex = "1.5"
sys-info = "0.9.0"
sysinfo = "0.30.8"
thiserror = "1.0"
either = "1.13"
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.18", features = ["chrono", "env-filter"] }
@@ -62,6 +63,8 @@ base64 = "0.22.0"
sqlx = { version = "0.8.2", features = [ "runtime-tokio", "sqlite", "macros" ] }
rust-common = { path = "../rust-common" }
[target.'cfg(windows)'.dependencies]
winreg = "0.52.0"

View File

@@ -1,4 +1,5 @@
use crate::state::{FriendsSocket, UserFriend, UserStatus};
use crate::state::{FriendsSocket, UserFriend};
use rust_common::users::UserStatus;
#[tracing::instrument]
pub async fn friends() -> crate::Result<Vec<UserFriend>> {

View File

@@ -19,8 +19,9 @@ pub mod data {
Hooks, JavaVersion, LinkedData, MemorySettings, ModLoader,
ModrinthCredentials, Organization, ProcessMetadata, ProfileFile,
Project, ProjectType, SearchResult, SearchResults, Settings,
TeamMember, Theme, User, UserFriend, UserStatus, Version, WindowSize,
TeamMember, Theme, User, UserFriend, Version, WindowSize,
};
pub use rust_common::users::UserStatus;
}
pub mod prelude {

View File

@@ -1,6 +1,6 @@
//! Theseus state management system
use crate::state::UserStatus;
use dashmap::DashMap;
use rust_common::users::{UserId, UserStatus};
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, sync::Arc};
#[cfg(feature = "tauri")]
@@ -262,8 +262,8 @@ pub enum EventError {
#[serde(rename_all = "snake_case")]
#[serde(tag = "event")]
pub enum FriendPayload {
FriendRequest { from: String },
UserOffline { id: String },
FriendRequest { from: UserId },
UserOffline { id: UserId },
StatusUpdate { user_status: UserStatus },
StatusSync,
}

View File

@@ -14,6 +14,10 @@ use futures::stream::SplitSink;
use futures::{SinkExt, StreamExt};
use reqwest::header::HeaderValue;
use reqwest::Method;
use rust_common::networking::message::{
ClientToServerMessage, ServerToClientMessage,
};
use rust_common::users::{UserId, UserStatus};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
@@ -23,7 +27,7 @@ type WriteSocket =
pub struct FriendsSocket {
write: WriteSocket,
user_statuses: Arc<DashMap<String, UserStatus>>,
user_statuses: Arc<DashMap<UserId, UserStatus>>,
}
#[derive(Deserialize, Serialize)]
@@ -34,28 +38,6 @@ pub struct UserFriend {
pub created: DateTime<Utc>,
}
#[derive(Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ClientToServerMessage {
StatusUpdate { profile_name: Option<String> },
}
#[derive(Deserialize, Debug)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ServerToClientMessage {
StatusUpdate { status: UserStatus },
UserOffline { id: String },
FriendStatuses { statuses: Vec<UserStatus> },
FriendRequest { from: String },
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct UserStatus {
pub user_id: String,
pub profile_name: Option<String>,
pub last_update: DateTime<Utc>,
}
impl Default for FriendsSocket {
fn default() -> Self {
Self::new()
@@ -126,6 +108,7 @@ impl FriendsSocket {
while let Some(msg_result) = read_stream.next().await {
match msg_result {
Ok(msg) => {
// TODO: Make wire package work with this other library
let server_message = match msg {
Message::Text(text) => {
serde_json::from_str::<
@@ -165,7 +148,7 @@ impl FriendsSocket {
{
match server_message {
ServerToClientMessage::StatusUpdate { status } => {
statuses.insert(status.user_id.clone(), status.clone());
statuses.insert(status.user_id, status.clone());
let _ = emit_friend(FriendPayload::StatusUpdate { user_status: status }).await;
},
ServerToClientMessage::UserOffline { id } => {
@@ -175,13 +158,18 @@ impl FriendsSocket {
ServerToClientMessage::FriendStatuses { statuses: new_statuses } => {
statuses.clear();
new_statuses.into_iter().for_each(|status| {
statuses.insert(status.user_id.clone(), status);
statuses.insert(status.user_id, status);
});
let _ = emit_friend(FriendPayload::StatusSync).await;
}
ServerToClientMessage::FriendRequest { from } => {
let _ = emit_friend(FriendPayload::FriendRequest { from }).await;
}
ServerToClientMessage::FriendRequestRejected { .. } => todo!(),
ServerToClientMessage::SocketOpened { .. } => todo!(),
ServerToClientMessage::SocketClosed { .. } => todo!(),
ServerToClientMessage::FriendSocketOpened { .. } => todo!(),
ServerToClientMessage::SocketData { .. } => todo!(),
}
}
}

View File

@@ -0,0 +1,19 @@
[package]
name = "rust-common"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_bytes = "0.11"
serde_json = "1.0"
serde_cbor = "0.11"
uuid = { version = "1.12", features = ["serde"] }
chrono = { version = "0.4", features = ["serde"] }
actix-ws = "0.3"
thiserror = "2.0"
rand = "0.8"
either = "1.13"

View File

@@ -0,0 +1,9 @@
{
"name": "@modrinth/networking",
"scripts": {
"build": "cargo build --release",
"lint": "cargo fmt --check && cargo clippy --all-targets -- -D warnings",
"fix": "cargo fmt && cargo clippy --fix",
"test": "cargo test"
}
}

View File

@@ -0,0 +1,201 @@
pub use super::users::UserId;
use thiserror::Error;
/// Generates a random 64 bit integer that is exactly `n` characters
/// long when encoded as base62.
///
/// Uses `rand`'s thread rng on every call.
///
/// # Panics
///
/// This method panics if `n` is 0 or greater than 11, since a `u64`
/// can only represent up to 11 character base62 strings
#[inline]
pub fn random_base62(n: usize) -> u64 {
random_base62_rng(&mut rand::thread_rng(), n)
}
/// Generates a random 64 bit integer that is exactly `n` characters
/// long when encoded as base62, using the given rng.
///
/// # Panics
///
/// This method panics if `n` is 0 or greater than 11, since a `u64`
/// can only represent up to 11 character base62 strings
pub fn random_base62_rng<R: rand::RngCore>(rng: &mut R, n: usize) -> u64 {
random_base62_rng_range(rng, n, n)
}
pub fn random_base62_rng_range<R: rand::RngCore>(
rng: &mut R,
n_min: usize,
n_max: usize,
) -> u64 {
use rand::Rng;
assert!(n_min > 0 && n_max <= 11 && n_min <= n_max);
// gen_range is [low, high): max value is `MULTIPLES[n] - 1`,
// which is n characters long when encoded
rng.gen_range(MULTIPLES[n_min - 1]..MULTIPLES[n_max])
}
const MULTIPLES: [u64; 12] = [
1,
62,
62 * 62,
62 * 62 * 62,
62 * 62 * 62 * 62,
62 * 62 * 62 * 62 * 62,
62 * 62 * 62 * 62 * 62 * 62,
62 * 62 * 62 * 62 * 62 * 62 * 62,
62 * 62 * 62 * 62 * 62 * 62 * 62 * 62,
62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62,
62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62,
u64::MAX,
];
/// An ID encoded as base62 for use in the API.
///
/// All ids should be random and encode to 8-10 character base62 strings,
/// to avoid enumeration and other attacks.
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct Base62Id(pub u64);
/// An error decoding a number from base62.
#[derive(Error, Debug)]
pub enum DecodingError {
/// Encountered a non-base62 character in a base62 string
#[error("Invalid character {0:?} in base62 encoding")]
InvalidBase62(char),
/// Encountered integer overflow when decoding a base62 id.
#[error("Base62 decoding overflowed")]
Overflow,
}
#[macro_export]
macro_rules! from_base62id {
($($struct:ty, $con:expr;)+) => {
$(
impl From<Base62Id> for $struct {
fn from(id: Base62Id) -> $struct {
$con(id.0)
}
}
impl From<$struct> for Base62Id {
fn from(id: $struct) -> Base62Id {
Base62Id(id.0)
}
}
)+
};
}
#[macro_export]
macro_rules! impl_base62_display {
($struct:ty) => {
impl std::fmt::Display for $struct {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&$crate::ids::base62_impl::to_base62(self.0))
}
}
};
}
impl_base62_display!(Base62Id);
#[macro_export]
macro_rules! base62_id_impl {
($struct:ty, $cons:expr) => {
$crate::ids::from_base62id!($struct, $cons;);
$crate::ids::impl_base62_display!($struct);
}
}
base62_id_impl!(UserId, UserId);
pub use {base62_id_impl, from_base62id, impl_base62_display};
pub mod base62_impl {
use serde::de::{self, Deserializer, Visitor};
use serde::ser::Serializer;
use serde::{Deserialize, Serialize};
use super::{Base62Id, DecodingError};
impl<'de> Deserialize<'de> for Base62Id {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct Base62Visitor;
impl Visitor<'_> for Base62Visitor {
type Value = Base62Id;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
formatter.write_str("a base62 string id")
}
fn visit_str<E>(self, string: &str) -> Result<Base62Id, E>
where
E: de::Error,
{
parse_base62(string).map(Base62Id).map_err(E::custom)
}
}
deserializer.deserialize_str(Base62Visitor)
}
}
impl Serialize for Base62Id {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&to_base62(self.0))
}
}
const BASE62_CHARS: [u8; 62] =
*b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
pub fn to_base62(mut num: u64) -> String {
let length = (num as f64).log(62.0).ceil() as usize;
let mut output = String::with_capacity(length);
while num > 0 {
// Could be done more efficiently, but requires byte
// manipulation of strings & Vec<u8> -> String conversion
output.insert(0, BASE62_CHARS[(num % 62) as usize] as char);
num /= 62;
}
output
}
pub fn parse_base62(string: &str) -> Result<u64, DecodingError> {
let mut num: u64 = 0;
for c in string.chars() {
let next_digit;
if c.is_ascii_digit() {
next_digit = (c as u8 - b'0') as u64;
} else if c.is_ascii_uppercase() {
next_digit = 10 + (c as u8 - b'A') as u64;
} else if c.is_ascii_lowercase() {
next_digit = 36 + (c as u8 - b'a') as u64;
} else {
return Err(DecodingError::InvalidBase62(c));
}
// We don't want this panicking or wrapping on integer overflow
if let Some(n) =
num.checked_mul(62).and_then(|n| n.checked_add(next_digit))
{
num = n;
} else {
return Err(DecodingError::Overflow);
}
}
Ok(num)
}
}

View File

@@ -0,0 +1,3 @@
pub mod ids;
pub mod networking;
pub mod users;

View File

@@ -0,0 +1,58 @@
use crate::ids::UserId;
use crate::users::UserStatus;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ClientToServerMessage {
StatusUpdate {
profile_name: Option<String>,
},
SocketOpen,
SocketClose {
socket: Uuid,
},
SocketSend {
socket: Uuid,
#[serde(with = "serde_bytes")]
data: Vec<u8>,
},
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ServerToClientMessage {
StatusUpdate {
status: UserStatus,
},
UserOffline {
id: UserId,
},
FriendStatuses {
statuses: Vec<UserStatus>,
},
FriendRequest {
from: UserId,
},
FriendRequestRejected {
from: UserId,
},
SocketOpened {
socket: Uuid,
},
SocketClosed {
socket: Uuid,
},
FriendSocketOpened {
user: UserId,
socket: Uuid,
},
SocketData {
socket: Uuid,
#[serde(with = "serde_bytes")]
data: Vec<u8>,
},
}

View File

@@ -0,0 +1,2 @@
pub mod message;
pub mod wire;

View File

@@ -0,0 +1,72 @@
use super::message::{ClientToServerMessage, ServerToClientMessage};
use actix_ws::{Closed, Message, Session};
use either::Either;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum DeserializationError {
#[error("Failed to deserialize message: {0}")]
DeserializationFailed(#[from] serde_json::Error),
#[error("Failed to deserialize binary message: {0}")]
BinaryDeserializationFailed(#[from] serde_cbor::Error),
}
#[derive(Debug, Error)]
pub enum SendError {
#[error("Failed to serialize message: {0}")]
SerializationFailed(#[from] serde_json::Error),
#[error("Failed to serialize binary message: {0}")]
BinarySerializationFailed(#[from] serde_cbor::Error),
#[error("Websocket closed")]
Closed,
}
impl From<Closed> for SendError {
fn from(_: Closed) -> Self {
SendError::Closed
}
}
macro_rules! message_wire {
($message_enum:ty, $binary_pattern:pat) => {
impl $message_enum {
pub fn deserialize(
msg: Message,
) -> Result<Either<Self, Message>, DeserializationError> {
Ok(match msg {
Message::Text(text) => {
Either::Left(serde_json::from_str(&text)?)
}
Message::Binary(bytes) => {
Either::Left(serde_cbor::from_slice(&bytes)?)
}
other => Either::Right(other),
})
}
pub async fn send(
&self,
session: &mut Session,
) -> Result<(), SendError> {
Ok(match self {
$binary_pattern => {
session.binary(serde_cbor::to_vec(self)?).await?
}
_ => session.text(serde_json::to_string(self)?).await?,
})
}
}
};
}
message_wire!(
ClientToServerMessage,
ClientToServerMessage::SocketSend { .. }
);
message_wire!(
ServerToClientMessage,
ServerToClientMessage::SocketData { .. }
);

View File

@@ -0,0 +1,15 @@
use super::ids::Base62Id;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)]
#[serde(from = "Base62Id")]
#[serde(into = "Base62Id")]
pub struct UserId(pub u64);
#[derive(Serialize, Deserialize, Clone)]
pub struct UserStatus {
pub user_id: UserId,
pub profile_name: Option<String>,
pub last_update: DateTime<Utc>,
}