Local Auth (start of dezentralization)
Prep for WebUI Upgrade to SSL optional
This commit is contained in:
parent
258404b7fa
commit
5f40158b00
11 changed files with 456 additions and 73 deletions
17
Cargo.lock
generated
17
Cargo.lock
generated
|
|
@ -8,6 +8,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"async-tungstenite",
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"axum",
|
"axum",
|
||||||
"base64",
|
"base64",
|
||||||
|
|
@ -138,6 +139,22 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-tungstenite"
|
||||||
|
version = "0.32.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6f89c129ab749940f95509d84950c62092c8b4bc6e386ddb162229037a6ec91"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tungstenite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ serde = { version = "1.0.219", features = ["derive"] }
|
||||||
sha2 = "*"
|
sha2 = "*"
|
||||||
sys-info = "*"
|
sys-info = "*"
|
||||||
sysinfo = "0.30"
|
sysinfo = "0.30"
|
||||||
tokio = { version = "*", features = ["full"] }
|
tokio = { version = "1.48.0", features = ["full"] }
|
||||||
tokio-util = { version = "*", features = ["full"] }
|
tokio-util = { version = "*", features = ["full"] }
|
||||||
tokio-tungstenite = { version = "*", features = ["native-tls"] }
|
tokio-tungstenite = { version = "*", features = ["native-tls"] }
|
||||||
tower = "*"
|
tower = "*"
|
||||||
|
|
@ -49,3 +49,4 @@ tokio-rustls = "0.26.4"
|
||||||
rustls-pemfile = "2.2.0"
|
rustls-pemfile = "2.2.0"
|
||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
sha1 = "0.10.6"
|
sha1 = "0.10.6"
|
||||||
|
async-tungstenite = "0.32.0"
|
||||||
|
|
|
||||||
19
src/auth/local_auth.rs
Normal file
19
src/auth/local_auth.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
use json::JsonValue;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::util::file_util::load_file;
|
||||||
|
|
||||||
|
pub fn is_private_key_valid(user_id: &Uuid, key_hash: &str) -> bool {
|
||||||
|
let file_contents = load_file("", "users.json");
|
||||||
|
|
||||||
|
let users = json::parse(&file_contents).unwrap();
|
||||||
|
if let JsonValue::Array(users_array) = users {
|
||||||
|
for user in users_array {
|
||||||
|
if user["uuid"] == user_id.to_string() && user["private_key_hash"] == key_hash {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
pub mod auth_connector;
|
pub mod auth_connector;
|
||||||
pub mod crypto_helper;
|
pub mod crypto_helper;
|
||||||
|
pub mod local_auth;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ use uuid::Uuid;
|
||||||
pub enum DataTypes {
|
pub enum DataTypes {
|
||||||
error_type,
|
error_type,
|
||||||
accepted_ids,
|
accepted_ids,
|
||||||
|
uuid,
|
||||||
|
settings,
|
||||||
|
settings_name,
|
||||||
chat_partner_id,
|
chat_partner_id,
|
||||||
iota_id,
|
iota_id,
|
||||||
user_id,
|
user_id,
|
||||||
|
|
@ -84,6 +87,9 @@ impl DataTypes {
|
||||||
match normalized.as_str() {
|
match normalized.as_str() {
|
||||||
"errortype" => DataTypes::error_type,
|
"errortype" => DataTypes::error_type,
|
||||||
"chatpartnerid" => DataTypes::chat_partner_id,
|
"chatpartnerid" => DataTypes::chat_partner_id,
|
||||||
|
"uuid" => DataTypes::uuid,
|
||||||
|
"settings" => DataTypes::settings,
|
||||||
|
"settingsname" => DataTypes::settings_name,
|
||||||
"iotaid" => DataTypes::iota_id,
|
"iotaid" => DataTypes::iota_id,
|
||||||
"userid" => DataTypes::user_id,
|
"userid" => DataTypes::user_id,
|
||||||
"userids" => DataTypes::user_ids,
|
"userids" => DataTypes::user_ids,
|
||||||
|
|
@ -163,6 +169,9 @@ pub enum CommunicationType {
|
||||||
error_invalid_secret,
|
error_invalid_secret,
|
||||||
error_invalid_private_key,
|
error_invalid_private_key,
|
||||||
success,
|
success,
|
||||||
|
settings_save,
|
||||||
|
settings_load,
|
||||||
|
settings_list,
|
||||||
message,
|
message,
|
||||||
message_send,
|
message_send,
|
||||||
message_live,
|
message_live,
|
||||||
|
|
@ -191,6 +200,7 @@ pub enum CommunicationType {
|
||||||
iota_closed,
|
iota_closed,
|
||||||
client_changed,
|
client_changed,
|
||||||
client_connected,
|
client_connected,
|
||||||
|
client_disconnected,
|
||||||
client_closed,
|
client_closed,
|
||||||
public_key,
|
public_key,
|
||||||
private_key,
|
private_key,
|
||||||
|
|
@ -212,6 +222,9 @@ impl CommunicationType {
|
||||||
|
|
||||||
match normalized.as_str() {
|
match normalized.as_str() {
|
||||||
"error" => CommunicationType::error,
|
"error" => CommunicationType::error,
|
||||||
|
"settingssave" => CommunicationType::settings_save,
|
||||||
|
"settingsload" => CommunicationType::settings_load,
|
||||||
|
"settingslist" => CommunicationType::settings_list,
|
||||||
"success" => CommunicationType::success,
|
"success" => CommunicationType::success,
|
||||||
"message" => CommunicationType::message,
|
"message" => CommunicationType::message,
|
||||||
"messagelive" => CommunicationType::message_live,
|
"messagelive" => CommunicationType::message_live,
|
||||||
|
|
@ -241,6 +254,7 @@ impl CommunicationType {
|
||||||
"iotaclosed" => CommunicationType::iota_closed,
|
"iotaclosed" => CommunicationType::iota_closed,
|
||||||
"clientchanged" => CommunicationType::client_changed,
|
"clientchanged" => CommunicationType::client_changed,
|
||||||
"clientconnected" => CommunicationType::client_connected,
|
"clientconnected" => CommunicationType::client_connected,
|
||||||
|
"clientdisconnected" => CommunicationType::client_disconnected,
|
||||||
"clientclosed" => CommunicationType::client_closed,
|
"clientclosed" => CommunicationType::client_closed,
|
||||||
"publickey" => CommunicationType::public_key,
|
"publickey" => CommunicationType::public_key,
|
||||||
"privatekey" => CommunicationType::private_key,
|
"privatekey" => CommunicationType::private_key,
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ use crate::gui::log_panel::{log_message, log_message_trans};
|
||||||
use crate::gui::{log_panel, ratatui_interface};
|
use crate::gui::{log_panel, ratatui_interface};
|
||||||
use crate::langu::language_creator;
|
use crate::langu::language_creator;
|
||||||
use crate::langu::language_manager::format;
|
use crate::langu::language_manager::format;
|
||||||
|
use crate::omikron::omikron_connection::OMIKRON_CONNECTION;
|
||||||
use crate::omikron::omikron_connection::OmikronConnection;
|
use crate::omikron::omikron_connection::OmikronConnection;
|
||||||
use crate::server::server::start;
|
use crate::server::server::start;
|
||||||
use crate::users::user_manager;
|
use crate::users::user_manager;
|
||||||
|
|
@ -31,6 +32,7 @@ use crate::util::config_util::CONFIG;
|
||||||
|
|
||||||
pub static APP_STATE: LazyLock<Arc<Mutex<AppState>>> =
|
pub static APP_STATE: LazyLock<Arc<Mutex<AppState>>> =
|
||||||
LazyLock::new(|| Arc::new(Mutex::new(AppState::new())));
|
LazyLock::new(|| Arc::new(Mutex::new(AppState::new())));
|
||||||
|
|
||||||
#[tokio::main(flavor = "multi_thread", worker_threads = 8)]
|
#[tokio::main(flavor = "multi_thread", worker_threads = 8)]
|
||||||
#[allow(unused_must_use, dead_code)]
|
#[allow(unused_must_use, dead_code)]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
|
@ -115,7 +117,7 @@ async fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let omikron: OmikronConnection = OmikronConnection::new();
|
let omikron: Arc<OmikronConnection> = Arc::new(OmikronConnection::new());
|
||||||
omikron.connect().await;
|
omikron.connect().await;
|
||||||
omikron
|
omikron
|
||||||
.send_message(
|
.send_message(
|
||||||
|
|
@ -131,6 +133,8 @@ async fn main() {
|
||||||
.to_string(),
|
.to_string(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
let mut omikron_connection = OMIKRON_CONNECTION.write().await;
|
||||||
|
*omikron_connection = Some(omikron.clone());
|
||||||
log_message_trans("setup_completed");
|
log_message_trans("setup_completed");
|
||||||
loop {
|
loop {
|
||||||
if !omikron.is_connected().await {
|
if !omikron.is_connected().await {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::auth::local_auth;
|
||||||
use crate::data::communication::{CommunicationType, CommunicationValue, DataTypes};
|
use crate::data::communication::{CommunicationType, CommunicationValue, DataTypes};
|
||||||
use crate::gui::log_panel::{log_cv, log_message, log_message_trans};
|
use crate::gui::log_panel::{log_cv, log_message, log_message_trans};
|
||||||
use crate::langu::language_manager::format;
|
use crate::langu::language_manager::format;
|
||||||
|
|
@ -5,33 +6,39 @@ use crate::users::contact::Contact;
|
||||||
use crate::users::user_community_util::UserCommunityUtil;
|
use crate::users::user_community_util::UserCommunityUtil;
|
||||||
use crate::util::chat_files;
|
use crate::util::chat_files;
|
||||||
use crate::util::chats_util::{get_user, get_users, mod_user};
|
use crate::util::chats_util::{get_user, get_users, mod_user};
|
||||||
|
use crate::util::file_util::{get_children, load_file, save_file};
|
||||||
|
use futures::Stream;
|
||||||
|
use futures::stream::{SplitSink, SplitStream};
|
||||||
|
use futures_util::sink::Sink;
|
||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
|
use hyper::upgrade::Upgraded;
|
||||||
|
use hyper_util::rt::TokioIo;
|
||||||
use json::JsonValue;
|
use json::JsonValue;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, LazyLock};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use tokio::net::TcpStream;
|
use tokio::sync::{Mutex, RwLock};
|
||||||
use tokio::sync::Mutex;
|
|
||||||
use tokio::time::{Duration, Instant, sleep};
|
use tokio::time::{Duration, Instant, sleep};
|
||||||
use tokio_tungstenite::{
|
use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
|
||||||
MaybeTlsStream, WebSocketStream, connect_async, tungstenite::protocol::Message,
|
|
||||||
};
|
|
||||||
use tungstenite::Utf8Bytes;
|
use tungstenite::Utf8Bytes;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
pub static OMIKRON_CONNECTION: LazyLock<Arc<RwLock<Option<Arc<OmikronConnection>>>>> =
|
||||||
|
LazyLock::new(|| Arc::new(RwLock::new(None)));
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum ConnectionVariant {
|
||||||
|
Omikron,
|
||||||
|
ClientUnauthenticated,
|
||||||
|
ClientAuthenticated,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct OmikronConnection {
|
pub struct OmikronConnection {
|
||||||
pub(crate) writer: Arc<
|
pub variant: Arc<RwLock<ConnectionVariant>>,
|
||||||
Mutex<
|
pub user_id: Arc<RwLock<Option<Uuid>>>,
|
||||||
Option<
|
pub(crate) writer:
|
||||||
futures_util::stream::SplitSink<
|
Arc<Mutex<Option<Box<dyn Sink<Message, Error = tungstenite::Error> + Send + Unpin>>>>,
|
||||||
WebSocketStream<MaybeTlsStream<TcpStream>>,
|
|
||||||
Message,
|
|
||||||
>,
|
|
||||||
>,
|
|
||||||
>,
|
|
||||||
>,
|
|
||||||
waiting: Arc<Mutex<HashMap<Uuid, Box<dyn Fn(CommunicationValue) + Send + Sync>>>>, // waiting for responses
|
waiting: Arc<Mutex<HashMap<Uuid, Box<dyn Fn(CommunicationValue) + Send + Sync>>>>, // waiting for responses
|
||||||
pingpong: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>, // ping-pong handler
|
pingpong: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>, // ping-pong handler
|
||||||
pub last_ping: Arc<Mutex<i64>>,
|
pub last_ping: Arc<Mutex<i64>>,
|
||||||
|
|
@ -42,6 +49,8 @@ pub struct OmikronConnection {
|
||||||
impl OmikronConnection {
|
impl OmikronConnection {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
variant: Arc::new(RwLock::new(ConnectionVariant::Omikron)),
|
||||||
|
user_id: Arc::new(RwLock::new(None)),
|
||||||
writer: Arc::new(Mutex::new(None)),
|
writer: Arc::new(Mutex::new(None)),
|
||||||
waiting: Arc::new(Mutex::new(HashMap::new())),
|
waiting: Arc::new(Mutex::new(HashMap::new())),
|
||||||
pingpong: Arc::new(Mutex::new(None)),
|
pingpong: Arc::new(Mutex::new(None)),
|
||||||
|
|
@ -50,17 +59,42 @@ impl OmikronConnection {
|
||||||
is_connected: Arc::new(Mutex::new(false)),
|
is_connected: Arc::new(Mutex::new(false)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub async fn client(
|
||||||
|
writer: SplitSink<tokio_tungstenite::WebSocketStream<TokioIo<Upgraded>>, Message>,
|
||||||
|
reader: SplitStream<tokio_tungstenite::WebSocketStream<TokioIo<Upgraded>>>,
|
||||||
|
) -> Arc<Self> {
|
||||||
|
let connection = Arc::new(Self {
|
||||||
|
variant: Arc::new(RwLock::new(ConnectionVariant::ClientUnauthenticated)),
|
||||||
|
user_id: Arc::new(RwLock::new(None)),
|
||||||
|
writer: Arc::new(Mutex::new(Some(Box::new(writer)
|
||||||
|
as Box<dyn Sink<Message, Error = tungstenite::Error> + Send + Unpin>))),
|
||||||
|
waiting: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
pingpong: Arc::new(Mutex::new(None)),
|
||||||
|
last_ping: Arc::new(Mutex::new(-1)),
|
||||||
|
message_send_times: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
is_connected: Arc::new(Mutex::new(false)),
|
||||||
|
});
|
||||||
|
let boxed_reader: Box<
|
||||||
|
dyn Stream<Item = Result<Message, tungstenite::Error>> + Send + Unpin,
|
||||||
|
> = Box::new(reader);
|
||||||
|
|
||||||
|
connection.spawn_listener(boxed_reader).await;
|
||||||
|
connection
|
||||||
|
}
|
||||||
pub async fn is_connected(&self) -> bool {
|
pub async fn is_connected(&self) -> bool {
|
||||||
*self.is_connected.lock().await
|
*self.is_connected.lock().await
|
||||||
}
|
}
|
||||||
/// Connect loop with retry
|
/// Connect loop with retry
|
||||||
pub async fn connect(&self) {
|
pub async fn connect(self: &Arc<Self>) {
|
||||||
loop {
|
loop {
|
||||||
match connect_async("wss://app.tensamin.net/ws/iota/").await {
|
match connect_async("wss://app.tensamin.net/ws/iota/").await {
|
||||||
Ok((ws_stream, _)) => {
|
Ok((ws_stream, _)) => {
|
||||||
let (write_half, read_half) = ws_stream.split();
|
let (write_half, read_half) = ws_stream.split();
|
||||||
*self.writer.lock().await = Some(write_half);
|
*self.writer.lock().await = Some(Box::new(write_half));
|
||||||
self.spawn_listener(read_half).await;
|
let boxed_reader: Box<
|
||||||
|
dyn Stream<Item = Result<Message, tungstenite::Error>> + Send + Unpin,
|
||||||
|
> = Box::new(read_half);
|
||||||
|
self.clone().spawn_listener(boxed_reader).await;
|
||||||
let cloned_self = self.clone();
|
let cloned_self = self.clone();
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
|
|
@ -85,23 +119,32 @@ impl OmikronConnection {
|
||||||
Self::send_message_static(&self.writer, msg).await
|
Self::send_message_static(&self.writer, msg).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn set_variant(self: &Arc<Self>, variant: ConnectionVariant) {
|
||||||
|
*self.variant.write().await = variant;
|
||||||
|
}
|
||||||
|
pub async fn set_user_id(self: &Arc<Self>, user_id: Uuid) {
|
||||||
|
*self.user_id.write().await = Some(user_id);
|
||||||
|
}
|
||||||
|
|
||||||
/// Listener for all incoming messages
|
/// Listener for all incoming messages
|
||||||
async fn spawn_listener(
|
async fn spawn_listener(
|
||||||
&self,
|
self: &Arc<Self>,
|
||||||
mut read_half: futures_util::stream::SplitStream<
|
mut read_half: Box<dyn Stream<Item = Result<Message, tungstenite::Error>> + Send + Unpin>,
|
||||||
WebSocketStream<MaybeTlsStream<TcpStream>>,
|
|
||||||
>,
|
|
||||||
) {
|
) {
|
||||||
let waiting_out = self.waiting.clone();
|
let waiting_out = self.waiting.clone();
|
||||||
let writer_out = self.writer.clone();
|
let writer_out = self.writer.clone();
|
||||||
let is_connected_out = self.is_connected.clone();
|
let is_connected_out = self.is_connected.clone();
|
||||||
let sel_out = self.clone();
|
let sel_out = self.clone();
|
||||||
|
let variant = self.variant.clone();
|
||||||
|
let sel_arc_out = self.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
while let Some(msg) = read_half.next().await {
|
while let Some(msg) = read_half.next().await {
|
||||||
let waiting = waiting_out.clone();
|
let waiting = waiting_out.clone();
|
||||||
let writer = writer_out.clone();
|
let writer = writer_out.clone();
|
||||||
let is_connected = is_connected_out.clone();
|
let is_connected = is_connected_out.clone();
|
||||||
let sel = sel_out.clone();
|
let sel = sel_out.clone();
|
||||||
|
let variant = variant.clone();
|
||||||
|
let sel_arc = sel_arc_out.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
match msg {
|
match msg {
|
||||||
Ok(Message::Close(Some(frame))) => {
|
Ok(Message::Close(Some(frame))) => {
|
||||||
|
|
@ -115,6 +158,96 @@ impl OmikronConnection {
|
||||||
sel.handle_pong(&cv, true).await;
|
sel.handle_pong(&cv, true).await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let com = variant.read().await.clone();
|
||||||
|
if com == ConnectionVariant::ClientUnauthenticated {
|
||||||
|
if cv.is_type(CommunicationType::identification) {
|
||||||
|
// Extract user ID
|
||||||
|
let user_id = match cv.get_data(DataTypes::user_id) {
|
||||||
|
Some(id_str) => {
|
||||||
|
match Uuid::parse_str(&id_str.to_string()) {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(_) => {
|
||||||
|
sel_arc.send_message(
|
||||||
|
CommunicationValue::new(CommunicationType::error_invalid_user_id)
|
||||||
|
.with_id(cv.get_id())
|
||||||
|
.to_json()
|
||||||
|
.to_string()
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
sel_arc
|
||||||
|
.send_message(
|
||||||
|
CommunicationValue::new(
|
||||||
|
CommunicationType::error_invalid_user_id,
|
||||||
|
)
|
||||||
|
.with_id(cv.get_id())
|
||||||
|
.to_json()
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate private key
|
||||||
|
if let Some(private_key_hash) =
|
||||||
|
cv.get_data(DataTypes::private_key_hash)
|
||||||
|
{
|
||||||
|
log_message(format!(
|
||||||
|
"private_key_hash: {}",
|
||||||
|
private_key_hash
|
||||||
|
));
|
||||||
|
let is_valid = local_auth::is_private_key_valid(
|
||||||
|
&user_id,
|
||||||
|
&private_key_hash.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if !is_valid {
|
||||||
|
log_message("Invalid private key");
|
||||||
|
sel_arc.send_message(
|
||||||
|
CommunicationValue::new(
|
||||||
|
CommunicationType::error_invalid_private_key,
|
||||||
|
)
|
||||||
|
.with_id(cv.get_id())
|
||||||
|
.to_json()
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log_message("Missing private key");
|
||||||
|
sel_arc
|
||||||
|
.send_message(
|
||||||
|
CommunicationValue::new(
|
||||||
|
CommunicationType::error_invalid_private_key,
|
||||||
|
)
|
||||||
|
.with_id(cv.get_id())
|
||||||
|
.to_json()
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set identification data
|
||||||
|
|
||||||
|
sel_arc.set_user_id(user_id).await;
|
||||||
|
sel_arc
|
||||||
|
.set_variant(ConnectionVariant::ClientAuthenticated)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let response = CommunicationValue::new(
|
||||||
|
CommunicationType::identification_response,
|
||||||
|
)
|
||||||
|
.with_id(cv.get_id());
|
||||||
|
sel_arc.send_message(response.to_json().to_string()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
// ************************************************ //
|
// ************************************************ //
|
||||||
// Direct messages //
|
// Direct messages //
|
||||||
// ************************************************ //
|
// ************************************************ //
|
||||||
|
|
@ -326,6 +459,79 @@ impl OmikronConnection {
|
||||||
.await;
|
.await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cv.is_type(CommunicationType::settings_save) {
|
||||||
|
let my_id = cv.get_sender().unwrap();
|
||||||
|
let settings_name =
|
||||||
|
cv.get_data(DataTypes::settings_name).unwrap().to_string();
|
||||||
|
let settings_value =
|
||||||
|
cv.get_data(DataTypes::payload).unwrap().to_string();
|
||||||
|
|
||||||
|
save_file(
|
||||||
|
&format!("users/{}/settings/", my_id),
|
||||||
|
&format!("{}.settings", settings_name),
|
||||||
|
&settings_value,
|
||||||
|
);
|
||||||
|
|
||||||
|
let response =
|
||||||
|
CommunicationValue::new(CommunicationType::settings_save)
|
||||||
|
.with_receiver(my_id)
|
||||||
|
.with_id(cv.get_id());
|
||||||
|
|
||||||
|
Self::send_message_static(
|
||||||
|
&writer.clone(),
|
||||||
|
response.to_json().to_string(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if cv.is_type(CommunicationType::settings_load) {
|
||||||
|
let my_id = cv.get_sender().unwrap();
|
||||||
|
let settings_name =
|
||||||
|
cv.get_data(DataTypes::settings_name).unwrap().to_string();
|
||||||
|
let settings_value_str = load_file(
|
||||||
|
&format!("users/{}/settings/", my_id),
|
||||||
|
&format!("{}.settings", settings_name),
|
||||||
|
);
|
||||||
|
let settings_value_json = JsonValue::from(settings_value_str);
|
||||||
|
let response =
|
||||||
|
CommunicationValue::new(CommunicationType::settings_load)
|
||||||
|
.with_id(cv.get_id())
|
||||||
|
.with_receiver(my_id)
|
||||||
|
.add_data(DataTypes::payload, settings_value_json)
|
||||||
|
.add_data_str(DataTypes::settings_name, settings_name);
|
||||||
|
|
||||||
|
Self::send_message_static(
|
||||||
|
&writer.clone(),
|
||||||
|
response.to_json().to_string(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if cv.is_type(CommunicationType::settings_list) {
|
||||||
|
let my_id = cv.get_sender().unwrap();
|
||||||
|
let settings = get_children(&format!("users/{}/settings/", my_id));
|
||||||
|
let mut settings_json = JsonValue::new_array();
|
||||||
|
for s in settings {
|
||||||
|
let s = s.replace(".settings", "");
|
||||||
|
if s.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let _ = settings_json.push(JsonValue::String(s));
|
||||||
|
}
|
||||||
|
let response =
|
||||||
|
CommunicationValue::new(CommunicationType::settings_list)
|
||||||
|
.with_id(cv.get_id())
|
||||||
|
.with_receiver(my_id)
|
||||||
|
.add_data(DataTypes::settings, settings_json);
|
||||||
|
|
||||||
|
Self::send_message_static(
|
||||||
|
&writer.clone(),
|
||||||
|
response.to_json().to_string(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log_message(format!("[Omikron] Error: {}", e));
|
log_message(format!("[Omikron] Error: {}", e));
|
||||||
|
|
@ -340,14 +546,7 @@ impl OmikronConnection {
|
||||||
}
|
}
|
||||||
pub async fn send_message_static(
|
pub async fn send_message_static(
|
||||||
writer: &Arc<
|
writer: &Arc<
|
||||||
Mutex<
|
Mutex<Option<Box<dyn Sink<Message, Error = tungstenite::Error> + Send + Unpin>>>,
|
||||||
Option<
|
|
||||||
futures_util::stream::SplitSink<
|
|
||||||
WebSocketStream<MaybeTlsStream<TcpStream>>,
|
|
||||||
Message,
|
|
||||||
>,
|
|
||||||
>,
|
|
||||||
>,
|
|
||||||
>,
|
>,
|
||||||
msg: String,
|
msg: String,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,18 @@ use hyper::{
|
||||||
};
|
};
|
||||||
use hyper_util::rt::tokio::TokioIo;
|
use hyper_util::rt::tokio::TokioIo;
|
||||||
use hyper_util::service::TowerToHyperService;
|
use hyper_util::service::TowerToHyperService;
|
||||||
|
// FIX: Add necessary rustls imports for builder in minimal-feature environment
|
||||||
use rustls::ServerConfig;
|
use rustls::ServerConfig;
|
||||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
|
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
|
||||||
use sha1::{Digest, Sha1};
|
use sha1::{Digest, Sha1};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::io::{self, BufReader};
|
use std::io::{self, BufReader};
|
||||||
|
use std::net::{IpAddr, SocketAddr};
|
||||||
use std::result::Result::Ok;
|
use std::result::Result::Ok;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{future::Future, pin::Pin, time::Duration};
|
use std::{future::Future, pin::Pin, time::Duration};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
use tokio_tungstenite::WebSocketStream;
|
use tokio_tungstenite::WebSocketStream;
|
||||||
use tower::Service;
|
use tower::Service;
|
||||||
|
|
||||||
|
|
@ -26,8 +28,11 @@ use crate::gui::log_panel::log_message;
|
||||||
use crate::server::socket::handle;
|
use crate::server::socket::handle;
|
||||||
use crate::util::file_util::load_file_buf;
|
use crate::util::file_util::load_file_buf;
|
||||||
use tokio_rustls::TlsAcceptor;
|
use tokio_rustls::TlsAcceptor;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct HttpService;
|
struct HttpService {
|
||||||
|
peer_addr: SocketAddr,
|
||||||
|
}
|
||||||
|
|
||||||
impl Service<HttpRequest<Incoming>> for HttpService {
|
impl Service<HttpRequest<Incoming>> for HttpService {
|
||||||
type Response = HttpResponse<Full<Bytes>>;
|
type Response = HttpResponse<Full<Bytes>>;
|
||||||
|
|
@ -46,6 +51,9 @@ impl Service<HttpRequest<Incoming>> for HttpService {
|
||||||
let headers = req.headers().clone();
|
let headers = req.headers().clone();
|
||||||
let upgrades = upgrade::on(req);
|
let upgrades = upgrade::on(req);
|
||||||
|
|
||||||
|
let peer_ip = self.peer_addr.ip();
|
||||||
|
let is_local = is_local_network(peer_ip);
|
||||||
|
|
||||||
let fut = async move {
|
let fut = async move {
|
||||||
if path.starts_with("/ws")
|
if path.starts_with("/ws")
|
||||||
&& headers
|
&& headers
|
||||||
|
|
@ -104,17 +112,14 @@ impl Service<HttpRequest<Incoming>> for HttpService {
|
||||||
let (status, body_text) = match path.as_str() {
|
let (status, body_text) = match path.as_str() {
|
||||||
"/" => (
|
"/" => (
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
"Server: Try connecting to WebSocket at ws://<host>:<port>/ws or check /status.",
|
"Server: Try connecting to WebSocket at ws[s]://<host>:<port>/ws or check /status.",
|
||||||
),
|
),
|
||||||
"/status" => (StatusCode::OK, "HTTP Server Status: Online"),
|
"/status" => (StatusCode::OK, "Server Status: Online"),
|
||||||
|
"/index" => (StatusCode::OK, include_str!("../../static/web/index.html")),
|
||||||
_ => (StatusCode::NOT_FOUND, "404 Not Found"),
|
_ => (StatusCode::NOT_FOUND, "404 Not Found"),
|
||||||
};
|
};
|
||||||
let body = Full::new(Bytes::from(body_text.to_string()));
|
let body = Full::new(Bytes::from(body_text.to_string()));
|
||||||
let response = HttpResponse::builder()
|
let response = HttpResponse::builder().status(status).body(body).unwrap();
|
||||||
.status(status)
|
|
||||||
.header(hyper::header::CONTENT_TYPE, "text/plain")
|
|
||||||
.body(body)
|
|
||||||
.unwrap();
|
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -128,15 +133,84 @@ impl Service<HttpRequest<Incoming>> for HttpService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start(port: u16) -> bool {
|
fn is_local_network(addr: IpAddr) -> bool {
|
||||||
let tls_config = match load_tls_config() {
|
// 1. Check for standard private ranges (RFC 1918) and loopback
|
||||||
Ok(config) => config,
|
if addr.is_loopback() {
|
||||||
Err(e) => {
|
return true;
|
||||||
log_message(format!("Failed to load TLS configuration: {}", e));
|
}
|
||||||
log_message("Server stopped. Ensure 'certs/cert.pem' and 'certs/key.pem' exist.");
|
|
||||||
|
// 2. Check for Link-Local Addresses (169.254.x.x)
|
||||||
|
if let IpAddr::V4(ipv4) = addr {
|
||||||
|
if ipv4.octets()[0] == 169 && ipv4.octets()[1] == 254 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check for IPv6 Unique Local Addresses (fc00::/7)
|
||||||
|
if let IpAddr::V6(ipv6) = addr {
|
||||||
|
if (ipv6.segments()[0] & 0xfe00) == 0xfc00 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs the standard, unencrypted HTTP/WS server loop.
|
||||||
|
async fn run_http_server(port: u16) -> bool {
|
||||||
|
// Bind to the port
|
||||||
|
let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await;
|
||||||
|
if let Err(e) = listener {
|
||||||
|
log_message(format!("Failed to bind to port {}: {:?}", port, e));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
let listener = listener.unwrap();
|
||||||
|
log_message(format!(
|
||||||
|
"Standard Server listening for HTTP and WS on 0.0.0.0:{}",
|
||||||
|
port
|
||||||
|
));
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
match listener.accept().await {
|
||||||
|
std::result::Result::Ok((stream, addr)) => {
|
||||||
|
let service = HttpService { peer_addr: addr };
|
||||||
|
let io = TokioIo::new(stream);
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(err) = http1::Builder::new()
|
||||||
|
.preserve_header_case(true)
|
||||||
|
.title_case_headers(true)
|
||||||
|
.serve_connection(io, TowerToHyperService::new(service))
|
||||||
|
.with_upgrades()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
if let Some(io_err) =
|
||||||
|
err.source().and_then(|e| e.downcast_ref::<io::Error>())
|
||||||
|
{
|
||||||
|
if io_err.kind() != io::ErrorKind::ConnectionReset
|
||||||
|
&& io_err.kind() != io::ErrorKind::BrokenPipe
|
||||||
|
{
|
||||||
|
log_message(format!("Error serving connection: {:?}", err));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log_message(format!("Error serving connection: {:?}", err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log_message(format!("Error accepting connection: {:?}", e));
|
||||||
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs the encrypted HTTPS/WSS server loop using the provided TLS config.
|
||||||
|
async fn run_tls_server(port: u16, tls_config: Arc<ServerConfig>) -> bool {
|
||||||
let acceptor = TlsAcceptor::from(tls_config);
|
let acceptor = TlsAcceptor::from(tls_config);
|
||||||
|
|
||||||
// Bind to the port
|
// Bind to the port
|
||||||
|
|
@ -147,18 +221,19 @@ pub async fn start(port: u16) -> bool {
|
||||||
}
|
}
|
||||||
let listener = listener.unwrap();
|
let listener = listener.unwrap();
|
||||||
log_message(format!(
|
log_message(format!(
|
||||||
"Server listening for HTTP and WS on 0.0.0.0:{}",
|
"Encrypted Server listening for HTTPS and WSS on 0.0.0.0:{}",
|
||||||
port
|
port
|
||||||
));
|
));
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
match listener.accept().await {
|
match listener.accept().await {
|
||||||
std::result::Result::Ok((stream, _addr)) => {
|
std::result::Result::Ok((stream, addr)) => {
|
||||||
let service = HttpService;
|
let service = HttpService { peer_addr: addr };
|
||||||
let acceptor = acceptor.clone();
|
let acceptor = acceptor.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
// Perform TLS handshake
|
||||||
let tls_stream = match acceptor.accept(stream).await {
|
let tls_stream = match acceptor.accept(stream).await {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -201,6 +276,26 @@ pub async fn start(port: u16) -> bool {
|
||||||
});
|
});
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn start(port: u16) -> bool {
|
||||||
|
let tls_result = load_tls_config();
|
||||||
|
|
||||||
|
match tls_result {
|
||||||
|
Ok(Some(tls_config)) => {
|
||||||
|
// Certificates found and config loaded successfully, run the TLS server
|
||||||
|
run_tls_server(port, tls_config).await
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
// Certificates not found, run the standard HTTP server
|
||||||
|
run_http_server(port).await
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log_message(format!("Fatal error during TLS config load: {}", e));
|
||||||
|
// Error, server cannot start
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
fn calculate_accept_key(key: &str) -> String {
|
fn calculate_accept_key(key: &str) -> String {
|
||||||
let websocket_guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
let websocket_guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||||
let mut sha1 = Sha1::new();
|
let mut sha1 = Sha1::new();
|
||||||
|
|
@ -209,30 +304,54 @@ fn calculate_accept_key(key: &str) -> String {
|
||||||
let result = sha1.finalize();
|
let result = sha1.finalize();
|
||||||
STANDARD.encode(result) // Base64 encode the result
|
STANDARD.encode(result) // Base64 encode the result
|
||||||
}
|
}
|
||||||
fn load_tls_config() -> Result<Arc<ServerConfig>, Box<dyn Error>> {
|
|
||||||
// Load certificate file
|
/// Loads TLS config. Returns Ok(None) if cert files are not found, and an error if parsing fails.
|
||||||
let mut cert_file = BufReader::new(load_file_buf("certs", "cert.pem")?);
|
fn load_tls_config() -> Result<Option<Arc<ServerConfig>>, Box<dyn Error>> {
|
||||||
let cert_ders = rustls_pemfile::certs(&mut cert_file)
|
let cert_file_res = load_file_buf("certs", "cert.pem");
|
||||||
|
let key_file_res = load_file_buf("certs", "cert.key");
|
||||||
|
|
||||||
|
// Check if certificate files are present. If not, return None.
|
||||||
|
let cert_file_buf = match cert_file_res {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) if e.kind() == ErrorKind::NotFound => {
|
||||||
|
log_message("TLS certificate 'certs/cert.pem' not found.");
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Err(e) => return Err(e.into()), // Other IO error
|
||||||
|
};
|
||||||
|
|
||||||
|
let key_file_buf = match key_file_res {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) if e.kind() == ErrorKind::NotFound => {
|
||||||
|
log_message("TLS key 'certs/cert.key' not found.");
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Err(e) => return Err(e.into()), // Other IO error
|
||||||
|
};
|
||||||
|
|
||||||
|
// Continue with configuration if both files were found
|
||||||
|
let mut cert_reader = BufReader::new(cert_file_buf);
|
||||||
|
let cert_ders = rustls_pemfile::certs(&mut cert_reader)
|
||||||
.collect::<Result<Vec<CertificateDer>, io::Error>>()?;
|
.collect::<Result<Vec<CertificateDer>, io::Error>>()?;
|
||||||
|
|
||||||
// PKCS8
|
// PKCS8
|
||||||
let mut key_file = BufReader::new(load_file_buf("certs", "cert.key")?);
|
let mut key_reader = BufReader::new(key_file_buf);
|
||||||
let mut key_ders = rustls_pemfile::pkcs8_private_keys(&mut key_file)
|
let mut key_ders = rustls_pemfile::pkcs8_private_keys(&mut key_reader)
|
||||||
.map(|r| r.map(Into::into)) // Explicit conversion
|
.map(|r| r.map(Into::into)) // Explicit conversion
|
||||||
.collect::<Result<Vec<PrivateKeyDer>, io::Error>>()?;
|
.collect::<Result<Vec<PrivateKeyDer>, io::Error>>()?;
|
||||||
|
|
||||||
if key_ders.is_empty() {
|
if key_ders.is_empty() {
|
||||||
// RSA
|
// RSA
|
||||||
key_file = BufReader::new(load_file_buf("certs", "cert.key")?);
|
key_reader = BufReader::new(load_file_buf("certs", "cert.key")?); // Re-read key file
|
||||||
key_ders = rustls_pemfile::rsa_private_keys(&mut key_file)
|
key_ders = rustls_pemfile::rsa_private_keys(&mut key_reader)
|
||||||
.map(|r| r.map(Into::into))
|
.map(|r| r.map(Into::into))
|
||||||
.collect::<Result<Vec<PrivateKeyDer>, io::Error>>()?;
|
.collect::<Result<Vec<PrivateKeyDer>, io::Error>>()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if key_ders.is_empty() {
|
if key_ders.is_empty() {
|
||||||
// EC
|
// EC
|
||||||
key_file = BufReader::new(load_file_buf("certs", "cert.key")?);
|
key_reader = BufReader::new(load_file_buf("certs", "cert.key")?); // Re-read key file
|
||||||
key_ders = rustls_pemfile::ec_private_keys(&mut key_file)
|
key_ders = rustls_pemfile::ec_private_keys(&mut key_reader)
|
||||||
.map(|r| r.map(Into::into))
|
.map(|r| r.map(Into::into))
|
||||||
.collect::<Result<Vec<PrivateKeyDer>, io::Error>>()?;
|
.collect::<Result<Vec<PrivateKeyDer>, io::Error>>()?;
|
||||||
}
|
}
|
||||||
|
|
@ -246,5 +365,5 @@ fn load_tls_config() -> Result<Arc<ServerConfig>, Box<dyn Error>> {
|
||||||
.with_single_cert(cert_ders, key_ders.remove(0))
|
.with_single_cert(cert_ders, key_ders.remove(0))
|
||||||
.map_err(|e| io::Error::new(ErrorKind::Other, e.to_string()))?;
|
.map_err(|e| io::Error::new(ErrorKind::Other, e.to_string()))?;
|
||||||
|
|
||||||
Ok(Arc::new(config))
|
Ok(Some(Arc::new(config)))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::communities::{community_connection::CommunityConnection, community_manager};
|
use crate::communities::{community_connection::CommunityConnection, community_manager};
|
||||||
use crate::gui::log_panel::log_message;
|
use crate::gui::log_panel::log_message;
|
||||||
|
use crate::omikron::omikron_connection::OmikronConnection;
|
||||||
|
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use futures::stream::SplitSink;
|
use futures::stream::SplitSink;
|
||||||
|
|
@ -15,7 +16,9 @@ pub fn handle(
|
||||||
reader: SplitStream<tokio_tungstenite::WebSocketStream<TokioIo<Upgraded>>>,
|
reader: SplitStream<tokio_tungstenite::WebSocketStream<TokioIo<Upgraded>>>,
|
||||||
) {
|
) {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if path.starts_with("/ws/community/") {
|
if path.starts_with("/ws/users/") {
|
||||||
|
OmikronConnection::client(writer, reader).await;
|
||||||
|
} else if path.starts_with("/ws/community/") {
|
||||||
let community_id = path.split("/").nth(3).unwrap();
|
let community_id = path.split("/").nth(3).unwrap();
|
||||||
log_message(format!("Community: {}", community_id));
|
log_message(format!("Community: {}", community_id));
|
||||||
if let Some(community) = community_manager::get_community(community_id).await {
|
if let Some(community) = community_manager::get_community(community_id).await {
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,6 @@ pub fn load_file_buf(path: &str, name: &str) -> io::Result<BufReader<File>> {
|
||||||
// Ensure the directory exists, create if necessary
|
// Ensure the directory exists, create if necessary
|
||||||
if !dir.exists() {
|
if !dir.exists() {
|
||||||
if let Err(e) = fs::create_dir_all(&dir) {
|
if let Err(e) = fs::create_dir_all(&dir) {
|
||||||
println!("[IMPORTANT] Couldn't create directories: {}", e);
|
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::NotFound,
|
io::ErrorKind::NotFound,
|
||||||
"Directory creation failed",
|
"Directory creation failed",
|
||||||
|
|
@ -59,14 +58,11 @@ pub fn load_file_buf(path: &str, name: &str) -> io::Result<BufReader<File>> {
|
||||||
|
|
||||||
// Create the file if it doesn't exist
|
// Create the file if it doesn't exist
|
||||||
if !file_path.exists() {
|
if !file_path.exists() {
|
||||||
if let Err(e) = File::create(&file_path) {
|
|
||||||
println!("[IMPORTANT] Couldn't create file: {}", e);
|
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::NotFound,
|
io::ErrorKind::NotFound,
|
||||||
"File creation failed",
|
"File creation failed",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Open the file and return a BufReader for efficient reading
|
// Open the file and return a BufReader for efficient reading
|
||||||
let file = File::open(&file_path)?;
|
let file = File::open(&file_path)?;
|
||||||
|
|
|
||||||
10
static/web/index.html
Normal file
10
static/web/index.html
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Iota</title>
|
||||||
|
<link rel="stylesheet" href="styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hallo</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in a new issue