From c0878e21baecc3eb2210893bc211d82dd22c51fd Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sat, 4 Oct 2025 11:02:57 +0200 Subject: [PATCH 1/7] Readme --- README.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/README.md b/README.md index aa3c9e6..c1c35dc 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,39 @@ # voxa-node + The server for the DM and notification system of Voxa + +### What is a Voxa Node? + +A voxa node is a server that handles DMs sent to you and push message notifications, a client will keep a constant connection with their chosen node and when a sender wants to send a DM or a message notification they can make a HTTP request to the same port, it's important to note that a node server only handles the receiving side, unless both parties are in the same node. + +A analogy to a node is a email server but real time. + +### What will a Voxa Node store? + +A voxa node stores messages that are sent to you, additionally push notifications and after 3 days of inactivity notifications will be removed. + +### Why Voxa Nodes over traditional PM/DM structure + +Traditional structures often are a privacy threat even with End-To-End encryption, additionally they are very resource intensive which in my situation is necessary to reduce as much as possible. +Another big issue with the traditional single server structure is that it may apply censorship, our biggest enemy. + +```mermaid +sequenceDiagram + participant Bob + participant AuthServer as Alice's Node (3rd Party Server) + participant Alice + participant Cloud as Voxa Cloud + + Bob->>Bob: Hash message (SHA-256) + Bob->>Bob: Sign hash with Private Key + Bob->>AuthServer: Send {message, signature} + + AuthServer->>Alice: Relay {message, signature} + + Alice->>Cloud: Request Bob's Public Key + Cloud-->>Alice: Return Bob's Public Key + + Alice->>Alice: Verify signature using Public Key
and recompute SHA-256(message) + + Alice-->>Alice: Accept message if signature is valid +``` From 82876a3560a50431b2a45c3b086841fd253871f9 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sat, 4 Oct 2025 11:23:50 +0200 Subject: [PATCH 2/7] Simple working server (from voxa-server) --- .gitignore | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.gitignore b/.gitignore index ad67955..191779a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,12 @@ target # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + + +# Added by cargo + +/target + + +*.db +config.json \ No newline at end of file From 480963e1624e15f00f71d25f5285d49a68236884 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sat, 4 Oct 2025 11:23:55 +0200 Subject: [PATCH 3/7] Simple working server (from voxa-server) --- Cargo.lock | 814 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 15 + src/macros.rs | 30 ++ src/main.rs | 21 ++ src/requests/auth.rs | 25 ++ src/requests/message.rs | 59 +++ src/requests/mod.rs | 48 +++ src/server.rs | 174 +++++++++ src/types.rs | 124 ++++++ src/utils/client.rs | 358 ++++++++++++++++++ src/utils/database.rs | 135 +++++++ src/utils/logger.rs | 45 +++ src/utils/mod.rs | 4 + src/utils/vfs.rs | 84 +++++ 14 files changed, 1936 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/macros.rs create mode 100644 src/main.rs create mode 100644 src/requests/auth.rs create mode 100644 src/requests/message.rs create mode 100644 src/requests/mod.rs create mode 100644 src/server.rs create mode 100644 src/types.rs create mode 100644 src/utils/client.rs create mode 100644 src/utils/database.rs create mode 100644 src/utils/logger.rs create mode 100644 src/utils/mod.rs create mode 100644 src/utils/vfs.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3226e2c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,814 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "find-msvc-tools" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.176" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" + +[[package]] +name = "libsqlite3-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys", +] + +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustls" +version = "0.23.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537" +dependencies = [ + "base64", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "ureq-proto", + "utf-8", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "voxa-node" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "chrono", + "once_cell", + "rusqlite", + "serde", + "serde_json", + "sha1", + "ureq", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-core" +version = "0.62.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-result" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..fffe69e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "voxa-node" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.145" +anyhow = "1.0.100" +base64 = "0.22.1" +sha1 = "0.10.6" +once_cell = "1.21.3" +chrono = "0.4.42" +ureq = "3.1.2" +rusqlite = "0.37.0" diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..fd1d31e --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,30 @@ +#[macro_export] +macro_rules! export_plugin { + ($p:expr) => { + #[unsafe(no_mangle)] + pub extern "C" fn load_plugin() -> $crate::utils::plugin::DynPlugin { + $p + } + }; +} + +#[macro_export] +macro_rules! logger { + (const $i:ident $name:expr) => { + pub const $i: $crate::once_cell::sync::Lazy<$crate::utils::logger::Logger> = + $crate::once_cell::sync::Lazy::new(|| $crate::utils::logger::Logger::new($name)); + }; + + ($i:ident $name:expr) => { + pub const $i: $crate::once_cell::sync::Lazy<$crate::utils::logger::Logger> = + $crate::once_cell::sync::Lazy::new(|| $crate::utils::logger::Logger::new($name)); + }; + + (const $name:expr) => { + $crate::once_cell::sync::Lazy::new(|| $crate::utils::logger::Logger::new($name)) + }; + + ($name:expr) => { + $crate::logger::utils::Logger::new($name) + }; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6867d9c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,21 @@ +mod macros; +mod requests; +mod server; +mod types; +mod utils; + +use std::path::PathBuf; + +pub use anyhow::Context as ErrorContext; +pub use anyhow::Result; +pub use once_cell; + +use crate::server::ServerConfig; +use crate::utils::vfs; + +fn main() -> Result<()> { + let root = PathBuf::from(""); + let config: ServerConfig = vfs::read_config(&root.join("config.json"))?; + config.build(&root).run()?; + Ok(()) +} diff --git a/src/requests/auth.rs b/src/requests/auth.rs new file mode 100644 index 0000000..62dd2ba --- /dev/null +++ b/src/requests/auth.rs @@ -0,0 +1,25 @@ +use std::sync::Arc; + +use serde::Deserialize; + +use crate::{ErrorContext, logger}; +use crate::{server::Server, utils::client::Client}; + +logger!(LOGGER "Auth"); + +#[derive(Debug, Deserialize)] +struct AuthApiRes { + user_id: u32, +} + +pub fn auth(_server: &Arc, client: &mut Client, token: &str) -> crate::Result { + let mut res = ureq::get(format!( + "http://localhost:3000/api/auth?intents=server&token={token}" + )) + .call() + .context("Failed to authenticate")?; + let api_res: AuthApiRes = serde_json::from_str(&res.body_mut().read_to_string()?)?; + client.set_uuid(api_res.user_id); + LOGGER.info(format!("{} successfully authenticated", api_res.user_id)); + Ok(api_res.user_id) +} diff --git a/src/requests/message.rs b/src/requests/message.rs new file mode 100644 index 0000000..824afc3 --- /dev/null +++ b/src/requests/message.rs @@ -0,0 +1,59 @@ +use std::sync::Arc; + +use crate::{server::Server, types, utils::client::Client}; + +crate::logger!(LOGGER "Message Manager"); + +pub fn send( + server: &Arc, + client: &Client, + channel_id: &str, + contents: &str, +) -> crate::Result<()> { + LOGGER.info(format!("SendMessage to {channel_id}: {contents}")); + + if contents.is_empty() { + client.send(types::ResponseError::InvalidRequest(format!( + "Invalid message: empty message" + )))?; + + return Ok(()); + } + + let msg = server.db.insert_message( + &channel_id, + client.get_uuid()?, + &contents, + chrono::Utc::now().timestamp(), + )?; + + let server = server.clone(); + + for c in server.clients.lock().unwrap().iter() { + let c = c.clone(); + let server = server.clone(); + let msg = msg.clone(); + std::thread::spawn(move || { + server + .wrap_err(&c, c.send(types::ServerMessage::MessageCreate(msg))) + .expect("Failed to broadcast"); + }); + } + + Ok(()) +} + +pub fn edit( + _server: &Arc, + _client: &Client, + message_id: usize, + new_contents: &str, +) -> crate::Result<()> { + LOGGER.info(format!("EditMessage {message_id}: {new_contents}")); + Ok(()) +} + +pub fn delete(_server: &Arc, _client: &Client, message_id: usize) -> crate::Result<()> { + LOGGER.info(format!("DeleteMessage {message_id}")); + Ok(()) +} diff --git a/src/requests/mod.rs b/src/requests/mod.rs new file mode 100644 index 0000000..7f45b5c --- /dev/null +++ b/src/requests/mod.rs @@ -0,0 +1,48 @@ +pub mod auth; +pub mod message; + +use std::sync::Arc; + +use crate::{ + server::Server, + types::{ClientMessage, WsMessage}, + utils::client::Client, +}; + +impl Server { + pub fn call_request( + self: &Arc, + req: &WsMessage, + client: &Client, + ) -> crate::Result<()> { + match req { + WsMessage::Message(req) => match req { + ClientMessage::SendMessage { + channel_id, + contents, + } => { + message::send(self, client, channel_id, contents)?; + } + + ClientMessage::EditMessage { + message_id, + new_contents, + } => message::edit(self, client, *message_id, new_contents)?, + + ClientMessage::DeleteMessage { message_id } => { + message::delete(self, client, *message_id)? + } + }, + + WsMessage::Binary(b) => { + Self::LOGGER.info(format!("Binary message: {b:?}")); + } + + WsMessage::String(s) => { + Self::LOGGER.info(format!("String message: {s}")); + } + } + + Ok(()) + } +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..110eeec --- /dev/null +++ b/src/server.rs @@ -0,0 +1,174 @@ +use std::{ + collections::HashSet, + net::{TcpListener, TcpStream}, + path::{Path, PathBuf}, + sync::{Arc, Mutex}, +}; + +use crate::{ + requests, types, + utils::{self, client::Client, database}, +}; + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct ServerConfig { + server_name: String, + server_id: String, + port: u16, + channels: Vec, +} + +#[allow(dead_code)] +pub struct Server { + pub root: PathBuf, + pub config: ServerConfig, + pub clients: Mutex>, + pub db: utils::database::Database, +} + +impl Default for ServerConfig { + fn default() -> Self { + Self { + port: 7080, + server_name: format!("Server Name"), + server_id: format!("offline-server"), + channels: Vec::new(), + } + } +} + +impl ServerConfig { + pub fn build(self, root: &Path) -> Arc { + Server::new_config(root, self) + } +} + +impl Server { + crate::logger!(LOGGER "Server"); + + pub fn new(root: &Path) -> Arc { + Self::new_config(root, ServerConfig::default()) + } + + pub fn new_config(root: &Path, config: ServerConfig) -> Arc { + Arc::new(Self { + db: database::Database::new(&config).unwrap(), + root: root.to_path_buf(), + config, + clients: Mutex::new(HashSet::new()), + }) + } + + pub fn run(self: &Arc) -> crate::Result<()> { + // Start server + let listener = TcpListener::bind(format!("0.0.0.0:{}", self.config.port))?; + Self::LOGGER.info(format!("Server listening at 0.0.0.0:{}", self.config.port)); + + for stream in listener.incoming() { + match stream { + Ok(stream) => { + std::thread::spawn({ + let srv = self.clone(); + + move || { + let Some(client) = Self::LOGGER + .extract(srv.init_client(stream), "Failed to initialize client") + else { + return; + }; + + Self::LOGGER.extract( + srv.wrap_err(&client, srv.handle_client(&client)), + "Client handler failed", + ); + } + }); + } + Err(e) => { + Self::LOGGER.error(format!("Connection failed: {e}")); + } + } + } + + Ok(()) + } + + fn init_client(self: &Arc, stream: TcpStream) -> anyhow::Result { + Self::LOGGER.info(format!("New connection: {}", stream.peer_addr()?)); + // Initialize client + let mut client = Client::new(stream)?; + + // Initialize handshake + self.wrap_err( + &client, + client.send(types::handshake::ServerDetails { + name: self.config.server_name.clone(), + id: self.config.server_id.clone(), + version: format!("0.0.1"), + }), + )?; + + match self.wrap_err(&client, client.read_t::())? { + Some(types::WsMessage::Message(types::handshake::ClientDetails { + auth_token, + last_message, + .. + })) => { + let auth_res = requests::auth::auth(self, &mut client, &auth_token); + let uuid = self.wrap_err(&client, auth_res)?; + self.wrap_err( + &client, + client.send(types::ServerMessage::Authenticated { + uuid, + messages: if let Some(i) = last_message { + self.wrap_err(&client, self.db.get_messages_after_id(i))? + } else { + self.wrap_err(&client, self.db.get_messages_after_id(0))? + }, + }), + )?; + } + Some(v) => { + self.wrap_err( + &client, + client.send(types::ResponseError::InvalidHandshake(format!( + "Invalid handshake: {v:?}" + ))), + )?; + } + None => {} + } + + // Insert to the set of all connected clients + self.clients.lock().unwrap().insert(client.clone()); + + Ok(client) + } + + fn handle_client(self: &Arc, client: &Client) -> anyhow::Result<()> { + // The main req/res loop + loop { + let req = client.read()?; + if let Some(r) = &req { + self.wrap_err(&client, self.call_request(r, &client))?; + } + } + } + + /// When there is a error it removes the client + pub fn wrap_err( + self: &Arc, + client: &Client, + res: std::result::Result, + ) -> std::result::Result { + if let Err(e) = &res { + self.clients.lock().unwrap().remove(&client); + if client + .send(types::ResponseError::InternalError(e.to_string())) + .is_err() + {} + } + + res + } +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..90e054a --- /dev/null +++ b/src/types.rs @@ -0,0 +1,124 @@ +use serde::{Deserialize, Serialize}; + +/// Messages sent *from the client* (user’s app) to the server +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "params", rename_all = "snake_case")] +pub enum ClientMessage { + /// Send a message to a channel + SendMessage { + channel_id: String, + contents: String, + }, + + /// Edit a message (if allowed) + EditMessage { + message_id: usize, + new_contents: String, + }, + + /// Delete a message (if allowed) + DeleteMessage { message_id: usize }, +} + +/// Messages sent *from the server* to the client +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "params", rename_all = "snake_case")] +pub enum ServerMessage { + /// Successful authentication + Authenticated { + uuid: u32, + messages: Vec, + }, + + TempMessage { + message: String, + }, + + /// A new message in a channel + MessageCreate(data::Message), + + /// A message was edited + MessageUpdate(data::Message), + + /// A message was deleted + MessageDelete { + channel_id: String, + message_id: usize, + }, + + /// Presence updates + PresenceUpdate { + user_id: String, + status: String, + }, + + /// Typing indicator + Typing { + user_id: String, + channel_id: String, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "error", content = "message", rename_all = "snake_case")] +pub enum ResponseError { + InvalidRequest(String), + InvalidHandshake(String), + Unauthorized(String), + NotFound(String), + InternalError(String), +} + +/// WebSocket wrapper +#[derive(Debug, Clone)] +pub enum WsMessage Deserialize<'de>> { + Message(T), + Binary(Vec), + String(String), +} + +/// Shared data structures +pub mod data { + use serde::{Deserialize, Serialize}; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct Message { + pub id: i64, + pub channel_id: String, + pub from: u32, + pub contents: String, + pub timestamp: i64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct Channel { + pub id: String, + pub name: String, + pub kind: ChannelKind, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + #[serde(rename_all = "snake_case")] + pub enum ChannelKind { + Text, + Voice, + } +} + +pub mod handshake { + use serde::{Deserialize, Serialize}; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct ServerDetails { + pub version: String, + pub name: String, + pub id: String, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct ClientDetails { + pub version: String, + pub auth_token: String, + pub last_message: Option, + } +} diff --git a/src/utils/client.rs b/src/utils/client.rs new file mode 100644 index 0000000..792f9cd --- /dev/null +++ b/src/utils/client.rs @@ -0,0 +1,358 @@ +use std::{ + hash::{Hash, Hasher}, + io::{self, Read, Write}, + net::{SocketAddr, TcpStream}, + time::Duration, +}; + +use anyhow::anyhow; +use serde::{Deserialize, Serialize}; + +use crate::types::{ClientMessage, WsMessage}; + +pub mod handshake { + use base64::Engine; + use base64::engine::general_purpose::STANDARD as Base64; + use sha1::{Digest, Sha1}; + use std::collections::HashMap; + use std::io::{BufRead, BufReader, Write}; + use std::net::TcpStream; + + const WS_GUID: &str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + pub fn handle_websocket_handshake(stream: &mut TcpStream) -> std::io::Result<()> { + let mut reader = BufReader::new(stream.try_clone()?); + let mut request_line = String::new(); + reader.read_line(&mut request_line)?; + + if !request_line.starts_with("GET") { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid HTTP method", + )); + } + + let mut headers = HashMap::new(); + let mut line = String::new(); + loop { + line.clear(); + let bytes = reader.read_line(&mut line)?; + if bytes == 0 || line == "\r\n" { + break; + } + if let Some((k, v)) = line.split_once(':') { + headers.insert(k.trim().to_lowercase(), v.trim().to_string()); + } + } + + let key = headers.get("sec-websocket-key").ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::InvalidData, "Missing Sec-WebSocket-Key") + })?; + + if headers.get("upgrade").map(|v| v.to_lowercase()) != Some("websocket".to_string()) { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Missing or invalid Upgrade header", + )); + } + + if !headers + .get("connection") + .map(|v| v.to_lowercase().contains("upgrade")) + .unwrap_or(false) + { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Missing or invalid Connection header", + )); + } + + // Optional: validate Sec-WebSocket-Version == 13 (most common) + if let Some(ver) = headers.get("sec-websocket-version") { + if ver.trim() != "13" { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Unsupported Sec-WebSocket-Version", + )); + } + } + + // Compute accept key + let mut hasher = Sha1::new(); + hasher.update(key.as_bytes()); + hasher.update(WS_GUID.as_bytes()); + let hash = hasher.finalize(); + let accept_key = Base64.encode(hash); + + // Note: include Sec-WebSocket-Protocol handling if you support subprotocols + let response = format!( + "HTTP/1.1 101 Switching Protocols\r\n\ + Upgrade: websocket\r\n\ + Connection: Upgrade\r\n\ + Sec-WebSocket-Accept: {}\r\n\ + Sec-WebSocket-Version: 13\r\n\r\n", + accept_key + ); + + stream.write_all(response.as_bytes())?; + stream.flush()?; + Ok(()) + } +} + +pub struct Client(TcpStream, Option); + +impl Client { + /// Create a client with no timeouts + pub fn new(mut stream: TcpStream) -> crate::Result { + handshake::handle_websocket_handshake(&mut stream)?; + Ok(Client(stream, None)) + } + + /// Create a client and set read/write timeouts (useful in prod) + pub fn with_timeouts( + mut stream: TcpStream, + read_timeout: Option, + write_timeout: Option, + ) -> crate::Result { + if let Some(t) = read_timeout { + stream.set_read_timeout(Some(t))?; + } + if let Some(t) = write_timeout { + stream.set_write_timeout(Some(t))?; + } + handshake::handle_websocket_handshake(&mut stream)?; + Ok(Client(stream, None)) + } + + /// Send a close frame and flush. `code` is a WebSocket close code (e.g., 1000 normal). + pub fn send_close(&self, code: u16, reason: &str) -> crate::Result<()> { + let mut stream = self.0.try_clone()?; + + // control frames must be <= 125 bytes + let mut payload = Vec::new(); + payload.extend_from_slice(&code.to_be_bytes()); + payload.extend_from_slice(reason.as_bytes()); + if payload.len() > 125 { + return Err(anyhow!("close reason too long").into()); + } + + let mut frame = Vec::with_capacity(2 + payload.len()); + frame.push(0x88); // FIN=1, opcode=0x8 (Close) + frame.push(payload.len() as u8); // server->client MUST NOT mask + frame.extend_from_slice(&payload); + + stream.write_all(&frame)?; + stream.flush()?; + Ok(()) + } + + /// Send a pong with given payload (control frames must be <=125) + fn send_pong(&self, payload: &[u8]) -> crate::Result<()> { + let mut stream = self.0.try_clone()?; + + if payload.len() > 125 { + return Err(anyhow!("pong payload too long").into()); + } + let mut frame = Vec::with_capacity(2 + payload.len()); + frame.push(0x8A); // FIN=1, opcode=0xA (Pong) + frame.push(payload.len() as u8); + frame.extend_from_slice(payload); + stream.write_all(&frame)?; + stream.flush()?; + Ok(()) + } + + /// Send a text/binary frame (server->client must NOT mask) + pub fn send(&self, m: T) -> crate::Result<()> { + let mut stream = self.0.try_clone()?; + + let payload = serde_json::to_string(&m)?; + let payload_bytes = payload.as_bytes(); + let len = payload_bytes.len(); + + let mut header = Vec::new(); + header.push(0x81); // FIN=1, opcode=0x1 (text) + + if len < 126 { + header.push(len as u8); + } else if len <= 65535 { + header.push(126); + header.extend_from_slice(&(len as u16).to_be_bytes()); + } else { + header.push(127); + header.extend_from_slice(&(len as u64).to_be_bytes()); + } + + stream.write_all(&header)?; + stream.write_all(payload_bytes)?; + stream.flush()?; + Ok(()) + } + + /// Read a full WebSocket message, handling fragmentation and control frames. + /// + /// Returns: + /// - Ok(Some(WsMessage)) on an application message (text/binary) + /// - Ok(None) if the connection should be closed (close received / read EOF) + /// - Err on protocol or IO errors. + pub fn read_t Deserialize<'de>>( + &self, + ) -> crate::Result>> { + let mut stream = self.0.try_clone()?; + + let mut message_payload = Vec::new(); + + loop { + // read the 2-byte header + let mut header = [0u8; 2]; + if let Err(e) = stream.read_exact(&mut header) { + if e.kind() == io::ErrorKind::UnexpectedEof || e.kind() == io::ErrorKind::BrokenPipe + { + return Ok(None); // treat EOF as closed + } + return Err(e.into()); + } + + let fin = header[0] & 0x80 != 0; + let opcode = header[0] & 0x0F; + let masked = header[1] & 0x80 != 0; + let mut payload_len = (header[1] & 0x7F) as u64; + + // Extended payload lengths + if payload_len == 126 { + let mut ext_len = [0u8; 2]; + stream.read_exact(&mut ext_len)?; + payload_len = u16::from_be_bytes(ext_len) as u64; + } else if payload_len == 127 { + let mut ext_len = [0u8; 8]; + stream.read_exact(&mut ext_len)?; + payload_len = u64::from_be_bytes(ext_len); + } + + // Mask key (client→server MUST be masked) + let mut mask = [0u8; 4]; + if masked { + stream.read_exact(&mut mask)?; + } else { + let _ = self.send_close(1002, "Client frames must be masked"); + return Ok(None); + } + + // Control frame checks + if matches!(opcode, 0x8 | 0x9 | 0xA) { + if payload_len > 125 { + let _ = self.send_close(1002, "Control frame too large"); + return Ok(None); + } + if !fin { + let _ = self.send_close(1002, "Control frames must not be fragmented"); + return Ok(None); + } + } + + // Read payload + unmask + let mut payload = vec![0u8; payload_len as usize]; + if payload_len > 0 { + stream.read_exact(&mut payload)?; + for i in 0..payload.len() { + payload[i] ^= mask[i % 4]; + } + } + + match opcode { + 0x0 | 0x1 | 0x2 => { + // Continuation / Text / Binary + message_payload.extend(payload); + if fin { + break; // got full message + } else { + continue; // wait for more fragments + } + } + 0x8 => { + // Close + let (code, reason) = if payload.len() >= 2 { + let code = u16::from_be_bytes([payload[0], payload[1]]); + let reason = if payload.len() > 2 { + String::from_utf8_lossy(&payload[2..]).into_owned() + } else { + String::new() + }; + (code, reason) + } else { + (1000, String::new()) + }; + let _ = self.send_close(code, &reason); + return Ok(None); + } + 0x9 => { + // Ping → respond with Pong + let _ = self.send_pong(&payload); + continue; + } + 0xA => { + // Pong → ignore + continue; + } + _ => { + let _ = self.send_close(1002, "Unsupported opcode"); + return Ok(None); + } + } + } + + // Try parsing JSON into ClientMessage + let message = match String::from_utf8(message_payload.clone()) { + Ok(text) => match serde_json::from_str(&text) { + Ok(msg) => WsMessage::Message(msg), + Err(_) => WsMessage::String(text), + }, + Err(_) => WsMessage::Binary(message_payload), + }; + + Ok(Some(message)) + } + + /// Read a full WebSocket message, handling fragmentation and control frames. + /// + /// Returns: + /// - Ok(Some(WsMessage)) on an application message (text/binary) + /// - Ok(None) if the connection should be closed (close received / read EOF) + /// - Err on protocol or IO errors. + pub fn read(&self) -> crate::Result>> { + self.read_t() + } + + pub fn get_uuid(&self) -> crate::Result { + self.1.ok_or(anyhow!("Invalid UUID")) + } + + pub fn set_uuid(&mut self, uuid: u32) { + self.1 = Some(uuid) + } + + pub fn addr(&self) -> crate::Result { + Ok(self.0.peer_addr().unwrap_or(self.0.local_addr()?)) + } +} + +impl Clone for Client { + fn clone(&self) -> Self { + Client(self.0.try_clone().expect("failed to clone TcpStream"), None) + } +} + +impl PartialEq for Client { + fn eq(&self, other: &Self) -> bool { + self.addr().unwrap() == other.addr().unwrap() + } +} + +impl Eq for Client {} + +impl Hash for Client { + fn hash(&self, state: &mut H) { + self.addr().unwrap().hash(state); + } +} diff --git a/src/utils/database.rs b/src/utils/database.rs new file mode 100644 index 0000000..730119c --- /dev/null +++ b/src/utils/database.rs @@ -0,0 +1,135 @@ +use crate::{server::ServerConfig, types::data::Message}; +use rusqlite::{Connection, Result, params}; + +pub struct Database(Connection); + +// General use case +impl Database { + pub fn new(_config: &ServerConfig) -> Option { + let conn = Connection::open("main.db").ok()?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS chat ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + channel_id TEXT NOT NULL, + user_id INTEGER NOT NULL, + contents TEXT NOT NULL, + timestamp INTEGER NOT NULL + )", + [], + ) + .ok()?; + + Some(Database(conn)) + } +} + +// For chat messages +impl Database { + /// Insert a message into the DB + pub fn insert_message( + &self, + channel_id: &str, + user_id: u32, + contents: &str, + timestamp: i64, + ) -> Result { + self.0.execute( + "INSERT INTO chat (channel_id, user_id, contents, timestamp) + VALUES (?1, ?2, ?3, ?4)", + params![channel_id, user_id, contents, timestamp], + )?; + + let id = self.0.last_insert_rowid(); + + Ok(Message { + id, + channel_id: channel_id.to_string(), + from: user_id, + contents: contents.to_string(), + timestamp, + }) + } + + /// Delete a message from the DB + pub fn delete_message(&self, message_id: usize) -> Result<()> { + self.0 + .execute("DELETE FROM chat WHERE id = ?1;", params![message_id])?; + + Ok(()) + } + + /// Delete a message from the DB + pub fn edit_message(&self, message_id: usize, contents: &str) -> Result<()> { + self.0.execute( + "UPDATE table_name + SET contents = ?2 + WHERE id = ?1; + ", + params![message_id, contents], + )?; + + Ok(()) + } + + /// Get a message by its ID + pub fn get_message_by_id(&self, message_id: usize) -> Result> { + let mut stmt = self.0.prepare( + "SELECT id, channel_id, user_id, contents, timestamp + FROM chat + WHERE id = ?1", + )?; + + let mut rows = stmt.query_map(params![message_id], |row| { + Ok(( + row.get::<_, i64>(0)?, // id + row.get::<_, String>(1)?, // channel_id + row.get::<_, u32>(2)?, // user_id + row.get::<_, String>(3)?, // contents + row.get::<_, i64>(4)?, // timestamp + )) + })?; + + if let Some(row) = rows.next() { + let (id, channel_id, user_id, contents, timestamp) = row?; + return Ok(Some(Message { + id, + channel_id, + from: user_id, + contents, + timestamp, + })); + } + Ok(None) + } + + /// Get all messages with an ID greater than the given one + pub fn get_messages_after_id(&self, message_id: usize) -> Result> { + let mut stmt = self.0.prepare( + "SELECT id, channel_id, user_id, contents, timestamp + FROM chat + WHERE id > ?1 + ORDER BY id ASC", + )?; + + let rows = stmt.query_map(params![message_id], |row| { + Ok(Message { + id: row.get::<_, i64>(0)?, + channel_id: row.get::<_, String>(1)?, + from: row.get::<_, u32>(2)?, + contents: row.get::<_, String>(3)?, + timestamp: row.get::<_, i64>(4)?, + }) + })?; + + let mut messages = Vec::new(); + for row in rows { + messages.push(row?); + } + + Ok(messages) + } +} + +unsafe impl Send for Database {} +unsafe impl Sync for Database {} diff --git a/src/utils/logger.rs b/src/utils/logger.rs new file mode 100644 index 0000000..6c6d52c --- /dev/null +++ b/src/utils/logger.rs @@ -0,0 +1,45 @@ +use std::fmt::Display; + +pub struct Logger { + name: String, +} + +impl Logger { + pub fn new(name: T) -> Self { + Logger { + name: name.to_string(), + } + } + + pub fn info(&self, message: T) { + println!("\x1b[32mINFO\x1b[0m ({}) › {}", self.name, message); + } + + pub fn warn(&self, message: T) { + println!("\x1b[33mWARN\x1b[0m ({}) › {}", self.name, message); + } + + pub fn error(&self, message: T) { + println!("\x1b[31mERROR\x1b[0m ({}) › {}", self.name, message); + } + + pub fn extract(&self, v: Result, m: D) -> Option { + match v { + Ok(a) => Some(a), + Err(e) => { + self.error(format!("{m}: {e}")); + None + } + } + } + + pub fn extract_panic(&self, v: Result, m: D) -> T { + match v { + Ok(a) => a, + Err(e) => { + self.error(format!("{m}: {e}")); + panic!() + } + } + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..0e121b6 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,4 @@ +pub mod client; +pub mod database; +pub mod logger; +pub mod vfs; diff --git a/src/utils/vfs.rs b/src/utils/vfs.rs new file mode 100644 index 0000000..0b62a96 --- /dev/null +++ b/src/utils/vfs.rs @@ -0,0 +1,84 @@ +use std::{fs, path::Path}; + +logger! { + const LOGGER "File" +} + +use serde::{Deserialize, Serialize}; + +use crate::logger; + +pub fn dir(path: &Path) -> crate::Result<()> { + if !path.exists() { + LOGGER.info(format!("Directory {path:?} does not exist, creating it")); + fs::create_dir_all(path)?; + } + Ok(()) +} + +pub fn read(path: &Path, default_content: &str) -> crate::Result { + if !path.exists() { + LOGGER.info(format!( + "File {path:?} does not exist, creating it with default contents" + )); + write(path, default_content)?; + return Ok(default_content.to_string()); + } + + Ok(fs::read_to_string(path)?) +} + +pub fn read_bytes<'a>(path: &Path, default_content: Vec) -> crate::Result> { + if !path.exists() { + LOGGER.info(format!( + "File {path:?} does not exist, creating it with default contents" + )); + write_bytes(path, &default_content)?; + return Ok(default_content); + } + + Ok(fs::read(path)?) +} + +pub fn read_config Deserialize<'de>>( + path: &Path, +) -> crate::Result { + if !path.exists() { + LOGGER.info(format!( + "File {path:?} does not exist, creating it with default contents" + )); + let default = T::default(); + write_config(path, &default)?; + return Ok(default); + } + + let read = fs::read_to_string(path)?; + Ok(serde_json::from_str::(&read)?) +} + +pub fn write(path: &Path, content: &str) -> crate::Result<()> { + dir(path.parent().ok_or(std::io::Error::new( + std::io::ErrorKind::InvalidFilename, + "File doesn't have a parent assigned, example: `config/config.json`", + ))?)?; + fs::write(path, content)?; + Ok(()) +} + +pub fn write_bytes(path: &Path, content: &[u8]) -> crate::Result<()> { + dir(path.parent().ok_or(std::io::Error::new( + std::io::ErrorKind::InvalidFilename, + "File doesn't have a parent assigned, example: `config/config.json`", + ))?)?; + fs::write(path, content)?; + Ok(()) +} + +pub fn write_config(path: &Path, content: &T) -> crate::Result<()> { + dir(path.parent().ok_or(std::io::Error::new( + std::io::ErrorKind::InvalidFilename, + "File doesn't have a parent assigned, example: `config/config.json`", + ))?)?; + fs::write(path, &serde_json::to_string_pretty(content)?)?; + Ok(()) +} From 5fa7fd224f933d6f8907b0d3f37888aee8c8ca27 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sat, 4 Oct 2025 14:37:14 +0200 Subject: [PATCH 4/7] Simple working node server --- src/requests/message.rs | 4 ++-- src/requests/mod.rs | 2 +- src/server.rs | 4 +--- src/types.rs | 8 ++++---- src/utils/database.rs | 10 +++++----- 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/requests/message.rs b/src/requests/message.rs index 824afc3..b4a4b41 100644 --- a/src/requests/message.rs +++ b/src/requests/message.rs @@ -7,7 +7,7 @@ crate::logger!(LOGGER "Message Manager"); pub fn send( server: &Arc, client: &Client, - channel_id: &str, + channel_id: u32, contents: &str, ) -> crate::Result<()> { LOGGER.info(format!("SendMessage to {channel_id}: {contents}")); @@ -21,7 +21,7 @@ pub fn send( } let msg = server.db.insert_message( - &channel_id, + channel_id, client.get_uuid()?, &contents, chrono::Utc::now().timestamp(), diff --git a/src/requests/mod.rs b/src/requests/mod.rs index 7f45b5c..2d053cc 100644 --- a/src/requests/mod.rs +++ b/src/requests/mod.rs @@ -21,7 +21,7 @@ impl Server { channel_id, contents, } => { - message::send(self, client, channel_id, contents)?; + message::send(self, client, *channel_id, contents)?; } ClientMessage::EditMessage { diff --git a/src/server.rs b/src/server.rs index 110eeec..93272ad 100644 --- a/src/server.rs +++ b/src/server.rs @@ -15,7 +15,6 @@ pub struct ServerConfig { server_name: String, server_id: String, port: u16, - channels: Vec, } #[allow(dead_code)] @@ -29,10 +28,9 @@ pub struct Server { impl Default for ServerConfig { fn default() -> Self { Self { - port: 7080, + port: 7090, server_name: format!("Server Name"), server_id: format!("offline-server"), - channels: Vec::new(), } } } diff --git a/src/types.rs b/src/types.rs index 90e054a..b291e20 100644 --- a/src/types.rs +++ b/src/types.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; pub enum ClientMessage { /// Send a message to a channel SendMessage { - channel_id: String, + channel_id: u32, contents: String, }, @@ -42,7 +42,7 @@ pub enum ServerMessage { /// A message was deleted MessageDelete { - channel_id: String, + channel_id: u32, message_id: usize, }, @@ -55,7 +55,7 @@ pub enum ServerMessage { /// Typing indicator Typing { user_id: String, - channel_id: String, + channel_id: u32, }, } @@ -84,7 +84,7 @@ pub mod data { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Message { pub id: i64, - pub channel_id: String, + pub channel_id: u32, pub from: u32, pub contents: String, pub timestamp: i64, diff --git a/src/utils/database.rs b/src/utils/database.rs index 730119c..cb354f8 100644 --- a/src/utils/database.rs +++ b/src/utils/database.rs @@ -11,7 +11,7 @@ impl Database { conn.execute( "CREATE TABLE IF NOT EXISTS chat ( id INTEGER PRIMARY KEY AUTOINCREMENT, - channel_id TEXT NOT NULL, + channel_id INTEGER NOT NULL, user_id INTEGER NOT NULL, contents TEXT NOT NULL, timestamp INTEGER NOT NULL @@ -29,7 +29,7 @@ impl Database { /// Insert a message into the DB pub fn insert_message( &self, - channel_id: &str, + channel_id: u32, user_id: u32, contents: &str, timestamp: i64, @@ -44,7 +44,7 @@ impl Database { Ok(Message { id, - channel_id: channel_id.to_string(), + channel_id, from: user_id, contents: contents.to_string(), timestamp, @@ -83,7 +83,7 @@ impl Database { let mut rows = stmt.query_map(params![message_id], |row| { Ok(( row.get::<_, i64>(0)?, // id - row.get::<_, String>(1)?, // channel_id + row.get::<_, u32>(1)?, // channel_id row.get::<_, u32>(2)?, // user_id row.get::<_, String>(3)?, // contents row.get::<_, i64>(4)?, // timestamp @@ -115,7 +115,7 @@ impl Database { let rows = stmt.query_map(params![message_id], |row| { Ok(Message { id: row.get::<_, i64>(0)?, - channel_id: row.get::<_, String>(1)?, + channel_id: row.get::<_, u32>(1)?, from: row.get::<_, u32>(2)?, contents: row.get::<_, String>(3)?, timestamp: row.get::<_, i64>(4)?, From f5162cceebc099ed637b575e365d2a8ae0a14985 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sat, 4 Oct 2025 16:49:29 +0200 Subject: [PATCH 5/7] Actual DM structure --- src/server.rs | 4 ++-- src/utils/database.rs | 15 ++++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/server.rs b/src/server.rs index 93272ad..508df39 100644 --- a/src/server.rs +++ b/src/server.rs @@ -119,9 +119,9 @@ impl Server { client.send(types::ServerMessage::Authenticated { uuid, messages: if let Some(i) = last_message { - self.wrap_err(&client, self.db.get_messages_after_id(i))? + self.wrap_err(&client, self.db.get_messages_after_id(uuid, i))? } else { - self.wrap_err(&client, self.db.get_messages_after_id(0))? + self.wrap_err(&client, self.db.get_messages_after_id(uuid, 0))? }, }), )?; diff --git a/src/utils/database.rs b/src/utils/database.rs index cb354f8..91d599e 100644 --- a/src/utils/database.rs +++ b/src/utils/database.rs @@ -104,15 +104,20 @@ impl Database { } /// Get all messages with an ID greater than the given one - pub fn get_messages_after_id(&self, message_id: usize) -> Result> { + pub fn get_messages_after_id( + &self, + channel_id: u32, + message_id: usize, + ) -> Result> { let mut stmt = self.0.prepare( "SELECT id, channel_id, user_id, contents, timestamp - FROM chat - WHERE id > ?1 - ORDER BY id ASC", + FROM chat + WHERE (channel_id = ?2 OR user_id = ?2) + AND id > ?1 + ORDER BY id ASC;", )?; - let rows = stmt.query_map(params![message_id], |row| { + let rows = stmt.query_map(params![message_id, channel_id], |row| { Ok(Message { id: row.get::<_, i64>(0)?, channel_id: row.get::<_, u32>(1)?, From 4bd671de4cae52b778bc8271bc9d458ecf37f58e Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sat, 4 Oct 2025 21:59:35 +0200 Subject: [PATCH 6/7] Improved messaging and fixed direct sending problems as well as uuid --- src/requests/message.rs | 24 +++++++++++++++++++++--- src/utils/client.rs | 5 ++++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/requests/message.rs b/src/requests/message.rs index b4a4b41..6c08a3a 100644 --- a/src/requests/message.rs +++ b/src/requests/message.rs @@ -33,13 +33,31 @@ pub fn send( let c = c.clone(); let server = server.clone(); let msg = msg.clone(); + std::thread::spawn(move || { - server - .wrap_err(&c, c.send(types::ServerMessage::MessageCreate(msg))) - .expect("Failed to broadcast"); + if let Some(uuid) = + LOGGER.extract(server.wrap_err(&c, c.get_uuid()), "Unable send message") + { + if uuid != channel_id { + return; + } + + LOGGER.extract( + server.wrap_err(&c, c.send(types::ServerMessage::MessageCreate(msg))), + "Failed to send message", + ); + } }); } + LOGGER.extract( + server.wrap_err( + &client, + client.send(types::ServerMessage::MessageCreate(msg)), + ), + "Failed to send message", + ); + Ok(()) } diff --git a/src/utils/client.rs b/src/utils/client.rs index 792f9cd..66626b2 100644 --- a/src/utils/client.rs +++ b/src/utils/client.rs @@ -339,7 +339,10 @@ impl Client { impl Clone for Client { fn clone(&self) -> Self { - Client(self.0.try_clone().expect("failed to clone TcpStream"), None) + Client( + self.0.try_clone().expect("failed to clone TcpStream"), + self.1.clone(), + ) } } From 2910db9e8f09ba11fcff33f4b30704a3e49d7e6e Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sat, 4 Oct 2025 22:01:34 +0200 Subject: [PATCH 7/7] Removed all dead code warnings --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index 6867d9c..3867824 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] mod macros; mod requests; mod server;