Compare commits

...

123 Commits

Author SHA1 Message Date
JustTestingV 9582160d2c xmpp: logopass auth (#19)
Reviewed-on: lavina/lavina#19
Co-authored-by: JustTestingV <JustTestingV@gmail.com>
Co-committed-by: JustTestingV <JustTestingV@gmail.com>
2023-10-08 13:53:39 +00:00
Nikita Vilunov 3ff8c98f14 update dependencies 2023-10-07 18:08:27 +02:00
Nikita Vilunov 1a1136187e sanitize IRC parsing (#23) 2023-10-04 18:27:43 +00:00
Nikita Vilunov 0b98102580 add author id in messages schema 2023-10-04 18:32:08 +02:00
Nikita Vilunov 1373767d7f xmpp: add muc proto tests 2023-10-04 15:55:34 +02:00
Nikita Vilunov 887fd95194 xmpp: fix parsing of unknown elements in messages 2023-10-03 00:17:48 +02:00
Nikita Vilunov 4621470bde fix graceful shutdown 2023-10-02 23:35:23 +02:00
Nikita Vilunov 9fca913430 xmpp: fix parsing of unknown elements in messages (#20)
Reviewed-on: lavina/lavina#20
2023-10-01 23:16:25 +00:00
Nikita Vilunov 8047a97baa remove /test/ 2023-10-01 18:16:11 +02:00
Nikita Vilunov 47195f5eee add crates/README.md 2023-10-01 18:15:37 +02:00
Nikita Vilunov 2f034284cf rename telemetry mod to http 2023-10-01 01:50:04 +02:00
Nikita Vilunov dc0a101fe6 split xmpp projection into a separate crate 2023-10-01 01:47:18 +02:00
Nikita Vilunov 854a244dbc split irc proejction into a separate crate (#18) 2023-09-30 23:34:35 +00:00
Nikita Vilunov a1db17c779 split core into a separate crate (#17) 2023-09-30 23:12:11 +00:00
Nikita Vilunov 1b3551f108 xmpp: rewrite bind request parser as a coroutine (#12)
Reviewed-on: lavina/lavina#12
2023-09-30 16:02:18 +00:00
Nikita Vilunov 444b608e96 split proto xmpp defs into its own crate (#14)
Reviewed-on: lavina/lavina#14
2023-09-30 15:43:46 +00:00
Nikita Vilunov 563811cbca pull-request workflow (#15) 2023-09-30 15:38:51 +00:00
Nikita Vilunov 58f6a5d90a add management API endpoints 2023-09-24 22:59:34 +02:00
Nikita Vilunov df6cdd4861 add a bit more logging 2023-09-23 01:12:03 +02:00
Nikita Vilunov 3d59f6aae5 read config path from cli args 2023-09-22 17:24:36 +02:00
Nikita Vilunov ad49703714 split proto irc defs into its own crate (#13)
Reviewed-on: lavina/lavina#13
2023-09-22 13:20:47 +00:00
JustTestingV 87d73af811 [irc] used nonempty prefixed nicks in 353 reply (#9)
Reviewed-on: lavina/lavina#9
Co-authored-by: JustTestingV <JustTestingV@gmail.com>
Co-committed-by: JustTestingV <JustTestingV@gmail.com>
2023-09-15 16:33:25 +00:00
Nikita Vilunov 298245f3f5 bump version 2023-09-15 17:12:12 +02:00
Nikita Vilunov 3de7a131f0 add dockerfile 2023-09-15 17:11:29 +02:00
Nikita Vilunov c662b64f11 disable tls support in sqlx, remove webpki-roots 2023-09-14 18:55:03 +02:00
JustTestingV 53f218c58f [irc] send 502 if not sender tries to change mode for other users (#4)
Reviewed-on: lavina/lavina#4
Co-authored-by: JustTestingV <JustTestingV@gmail.com>
Co-committed-by: JustTestingV <JustTestingV@gmail.com>
2023-09-06 18:43:07 +00:00
Nikita Vilunov 377d9c32d2 fix message sending 2023-09-06 16:34:20 +02:00
Nikita Vilunov 43ea27b655 configure cargo deny 2023-08-28 14:55:01 +02:00
JustTestingV e9fc74b46b used regex_static in Jid (#1)
Reviewed-on: lavina/lavina#1
Co-authored-by: JustTestingV <JustTestingV@gmail.com>
Co-committed-by: JustTestingV <JustTestingV@gmail.com>
2023-08-26 15:50:05 +00:00
Nikita Vilunov 6ed1f6be40 readme.md 2023-08-24 18:31:05 +02:00
Nikita Vilunov b9724cd995 stop player fibers on shutdown 2023-08-24 14:10:31 +02:00
Nikita Vilunov 5c07c8368d persistence of chat messages 2023-08-18 16:45:48 +02:00
Nikita Vilunov f401aa786e Fix Storage closing during app shutdown.
Storage was still co-owned by the RoomRegistry, which did not allow us to shut it down gracefully. Dropping RoomRegistry explicitly helps.
2023-08-17 18:35:50 +02:00
Nikita Vilunov 1b5ac1491a persistence for rooms 2023-08-17 15:41:28 +02:00
Nikita Vilunov ef5c0dbbf6 update rust 2023-08-17 11:46:53 +02:00
Nikita Vilunov f8151699db implement irc auth via PASS 2023-08-16 16:30:02 +02:00
Nikita Vilunov c39928799d fix tls key parsing usage 2023-08-05 00:38:56 +02:00
Nikita Vilunov 70b12c9a0d concept mapping 2023-08-05 00:09:42 +02:00
Nikita Vilunov 9f0bcb9279 storage module init 2023-08-05 00:06:48 +02:00
Nikita Vilunov 1a43a3c2d7 update deps 2023-08-04 23:50:50 +02:00
Nikita Vilunov 4b04696a4f fix for windows 2023-07-30 18:59:33 +02:00
Nikita Vilunov c1a461a09e fix irc tests 2023-07-23 15:21:26 +02:00
Nikita Vilunov b80daf9648 update libs 2023-07-23 15:18:17 +02:00
Nikita Vilunov 50915afcf6 end server msgs in \r\n 2023-07-22 21:11:01 +02:00
Nikita Vilunov 51d7278617 add sender to ping response 2023-07-22 16:22:49 +02:00
Nikita Vilunov 1895084ded lock rust version 2023-07-18 20:46:09 +02:00
Nikita Vilunov 8efbacc4d0 fix irc parsing
it's not required to start trailing arg with : if it doesn't contain spaces
2023-07-03 22:26:37 +02:00
Nikita Vilunov daf05869a3 tune irc logging 2023-07-03 22:25:57 +02:00
Nikita Vilunov fc9c45b627 update deps 2023-07-03 22:25:45 +02:00
Nikita Vilunov f2ab963f7b add a new nightly flag 2023-05-02 15:41:18 +02:00
Nikita Vilunov 4057b4a910 clean up ByteVec type aliases 2023-04-14 00:38:26 +02:00
Nikita Vilunov 55b69f4c8a rewrite irc projection to use str 2023-04-13 21:15:48 +02:00
Nikita Vilunov d0c579863e feat(xmpp): list muc rooms in disco 2023-04-11 18:28:03 +02:00
Nikita Vilunov c44101d5d0 feat(xmpp): send muc messages to clients 2023-04-11 16:42:09 +02:00
Nikita Vilunov 58582f4e51 feat(xmpp): handle sending messages to muc 2023-04-09 23:31:43 +02:00
Nikita Vilunov 2b54260f0b feat(xmpp): improve disco responses 2023-04-05 22:37:33 +02:00
Nikita Vilunov f71d098420 feat(xmpp): handle stream stop, dump tls keys 2023-04-05 18:57:35 +02:00
Nikita Vilunov fb8329a187 feat(xmpp): add stream id 2023-04-05 14:31:44 +02:00
Nikita Vilunov 99435a020e cargo update 2023-03-31 19:02:46 +02:00
Nikita Vilunov 65471a6c7f split xmpp stanzas handling into functions 2023-03-30 14:49:39 +02:00
Nikita Vilunov 123781d145 feat(xmpp): disco stub handlers 2023-03-30 14:31:20 +02:00
Nikita Vilunov fbb7349585 feat(xmpp): improve jid model 2023-03-29 01:12:12 +02:00
Nikita Vilunov a2a0a8914d feat(xmpp): rewrite handling of output xml events 2023-03-29 00:36:12 +02:00
Nikita Vilunov fb2cbf8a8c feat(xmpp): serialization of disco iqs 2023-03-29 00:34:12 +02:00
Nikita Vilunov 4ce97f8e13 add disco iqs to all iqs 2023-03-27 23:52:31 +02:00
Nikita Vilunov 63704d6010 use macro in muc parsing 2023-03-27 23:47:14 +02:00
Nikita Vilunov 7b2bfae147 feat(xmpp): parse disco queries 2023-03-27 23:45:44 +02:00
Nikita Vilunov 0e78f24fbd feat(xmpp): implement muc base element parsing 2023-03-25 17:56:21 +01:00
Nikita Vilunov a73bbdb5f1 fix handling of \r in xmpp stream 2023-03-24 01:38:42 +01:00
Nikita Vilunov c449f18f97 a few notes about supported xmpp features 2023-03-24 01:28:09 +01:00
Nikita Vilunov 9110ab9beb rewrite presence parser as a generator 2023-03-23 02:20:30 +01:00
Nikita Vilunov d0f807841c generator-based parsing of xmpp stanzas 2023-03-23 01:37:02 +01:00
Nikita Vilunov bba1ea107d fix some warnings 2023-03-21 22:50:40 +01:00
Nikita Vilunov 71d7323534 remove unused struct 2023-03-21 01:17:48 +01:00
Nikita Vilunov 3e1bb53c1b feat(xmpp): respond to unknown IQs with errors 2023-03-21 01:16:02 +01:00
Nikita Vilunov a65ea89ce1 feat(xmpp): make presence polymorphic wrt any fields 2023-03-20 17:25:14 +01:00
Nikita Vilunov 1cc4761aeb feat(xmpp): presence parsing 2023-03-15 15:27:48 +01:00
Nikita Vilunov 6add6db371 feat(xmpp): roster query stub 2023-03-12 22:50:28 +01:00
Nikita Vilunov 33dbfba116 feat(xmpp): implement session iq 2023-03-12 14:15:13 +01:00
Nikita Vilunov 4107c5b663 feat(xmpp): answer to bind requests 2023-03-12 13:25:23 +01:00
Nikita Vilunov 4730526fee feat(xmpp): glue parsing of incoming messages together 2023-03-12 00:30:48 +01:00
Nikita Vilunov 443f6a2c90 feat(xmpp): iq body ADT with parsing 2023-03-11 18:36:38 +01:00
Nikita Vilunov f131454cb2 feat(xmpp): parsing of bind request 2023-03-11 16:07:02 +01:00
Nikita Vilunov d444fc407b feat(xmpp): iq parsing 2023-03-08 19:56:53 +01:00
Nikita Vilunov 27bbabbbbd feat(xmpp): initial parsing of ordinary stream events 2023-03-07 16:28:29 +01:00
Nikita Vilunov d1dad72c08 feat(xmpp): push-based message parser 2023-03-07 14:56:31 +01:00
Nikita Vilunov 25c4d02ed2 small certificates tip 2023-03-06 19:52:53 +01:00
Nikita Vilunov dc788a89c4 feat(xmpp): extract tls xml defns into a separate module 2023-03-06 12:49:51 +01:00
Nikita Vilunov f1eff730a2 feat(xmpp): refactor the projection 2023-03-06 12:05:33 +01:00
Nikita Vilunov 42c22d045f feat(xmpp): implement socket start negotiation up to auth 2023-03-05 22:04:28 +01:00
Nikita Vilunov 435da6663a feat(xmpp): serialization of stream start 2023-02-28 12:12:03 +01:00
Nikita Vilunov 494ddc7ee1 feat(xmpp): placeholder for xmpp projection and example of xml 2023-02-28 00:05:30 +01:00
Nikita Vilunov 0adc19558d remove Terminator::from_raw 2023-02-22 16:05:28 +01:00
Nikita Vilunov bbd68853ae graceful shutdown of irc socket listener 2023-02-22 15:40:05 +01:00
Nikita Vilunov 49a975e66e fix irc integration tests 2023-02-17 22:12:29 +01:00
Nikita Vilunov 266eafe6e6 handle QUIT cmd 2023-02-17 00:38:34 +01:00
Nikita Vilunov e813fb7c69 implement room bans 2023-02-16 22:49:17 +01:00
Nikita Vilunov 204126b9fb return user's mode to themselves 2023-02-16 20:53:37 +01:00
Nikita Vilunov 63f31aa42f support who for global channels 2023-02-16 19:47:51 +01:00
Nikita Vilunov 69bccef3bf fix WHO replies 2023-02-16 19:33:36 +01:00
Nikita Vilunov 81ee1c1044 implement WHO irc command for queries on self 2023-02-16 18:39:54 +01:00
Nikita Vilunov 30db029390 make irc update handler a separate function 2023-02-16 16:33:44 +01:00
Nikita Vilunov 1e17e017cf make PlayerCommand mod-private 2023-02-15 21:49:52 +01:00
Nikita Vilunov 8b4e963d39 context and sequence diagrams of sending a message 2023-02-15 18:55:43 +01:00
Nikita Vilunov 203db3b207 handle part commands 2023-02-15 18:54:48 +01:00
Nikita Vilunov 1bc305962e endpoint with a list of rooms 2023-02-15 18:10:54 +01:00
Nikita Vilunov a03a3a11a3 handle connection termination 2023-02-15 17:47:48 +01:00
Nikita Vilunov 23898038e1 fix some warnings 2023-02-14 23:49:56 +01:00
Nikita Vilunov 7dfe6e0295 fix some stuff 2023-02-14 23:43:59 +01:00
Nikita Vilunov 3950ee1d7a refactor player actor a bit 2023-02-14 23:38:40 +01:00
Nikita Vilunov 05f8c5e502 rework commands and updates.
updates from rooms are send only to users other than the initiator.
updates from player are send only to connections other than the one the command was sent from.
2023-02-14 23:22:04 +01:00
Nikita Vilunov 39fed80106 warn on unhandled irc message 2023-02-14 20:56:31 +01:00
Nikita Vilunov 4e5ccd604c rename socket => connection for consistency 2023-02-14 20:45:22 +01:00
Nikita Vilunov 57ea2dd2d7 introduce Updates as a common player and connection event 2023-02-14 20:42:52 +01:00
Nikita Vilunov 265b78dc51 improve newtypes 2023-02-14 20:07:07 +01:00
Nikita Vilunov c845f5d4ca handle topic command 2023-02-14 19:46:42 +01:00
Nikita Vilunov d10cddec61 send channels on connect 2023-02-14 19:28:49 +01:00
Nikita Vilunov a8d6a98a5b produce join messages on joins from other connections 2023-02-14 18:55:09 +01:00
Nikita Vilunov 7d6ae661c4 update deps 2023-02-14 16:07:05 +01:00
Nikita Vilunov cef0269828 send chat members list on connection 2023-02-14 01:44:03 +01:00
Nikita Vilunov ec819d37ea make room a data structure behind a rwlock instead of an actor 2023-02-14 01:42:04 +01:00
Nikita Vilunov 315b7e638b add irc integration tests 2023-02-13 21:58:44 +01:00
Nikita Vilunov b1b8ec800e tests for irc 2023-02-13 21:04:08 +01:00
72 changed files with 7824 additions and 1866 deletions

6
.dockerignore Normal file
View File

@ -0,0 +1,6 @@
*
!/src/
!/migrations/
!Cargo.lock
!Cargo.toml
!rust-toolchain

View File

@ -0,0 +1,18 @@
name: check-and-test
on: [push, pull_request]
jobs:
check-and-test:
runs-on: ubuntu-latest
steps:
- name: git checkout repository
uses: actions/checkout@v4
- name: setup rust
uses: https://github.com/actions-rs/toolchain@v1
- name: cargo check
uses: https://github.com/actions-rs/cargo@v1
with:
command: check
- name: test
uses: https://github.com/actions-rs/cargo@v1
with:
command: test

2
.gitignore vendored
View File

@ -1 +1,3 @@
/target
/db.sqlite
.idea/

1786
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +1,61 @@
[workspace]
members = [
"crates/*"
".",
"crates/lavina-core",
"crates/proto-irc",
"crates/projection-irc",
"crates/proto-xmpp",
"crates/mgmt-api",
]
[workspace.package]
version = "0.0.1-dev"
[workspace.dependencies]
nom = "7.1.3"
assert_matches = "1.5.0"
tokio = { version = "1.24.1", features = ["full"] } # async runtime
futures-util = "0.3.25"
anyhow = "1.0.68" # error utils
nonempty = "0.8.1"
quick-xml = { version = "0.30.0", features = ["async-tokio"] }
lazy_static = "1.4.0"
regex = "1.7.1"
derive_more = "0.99.17"
clap = { version = "4.4.4", features = ["derive"] }
serde = { version = "1.0.152", features = ["rc", "serde_derive"] }
tracing = "0.1.37" # logging & tracing api
prometheus = { version = "0.13.3", default-features = false }
base64 = "0.21.3"
lavina-core = { path = "crates/lavina-core" }
[package]
name = "lavina"
version = "0.1.0"
version.workspace = true
edition = "2021"
publish = false
[dependencies]
anyhow = "1.0.68" # error utils
anyhow.workspace = true
figment = { version = "0.10.8", features = ["env", "toml"] } # configuration files
hyper = { version = "1.0.0-rc.2", features = ["server", "http1"] } # http server
http-body-util = "0.1.0-rc.2"
serde = { version = "1.0.152", features = ["rc", "serde_derive"] }
tokio = { version = "1.24.1", features = ["full"] } # async runtime
tracing = "0.1.37" # logging & tracing api
hyper = { version = "1.0.0-rc.3,<1.0.0-rc.4", features = ["server", "http1"] } # http server
http-body-util = "0.1.0-rc.3"
serde.workspace = true
serde_json = "1.0.93"
tokio.workspace = true
tracing.workspace = true
tracing-subscriber = "0.3.16"
tokio-tungstenite = "0.18.0"
futures-util = "0.3.25"
prometheus = { version = "0.13.3", default-features = false }
regex = "1.7.1"
nom = "7.1.3"
futures-util.workspace = true
prometheus.workspace = true
nonempty.workspace = true
derive_more.workspace = true
lavina-core.workspace = true
projection-irc = { path = "crates/projection-irc" }
projection-xmpp = { path = "crates/projection-xmpp" }
mgmt-api = { path = "crates/mgmt-api" }
clap.workspace = true
[dev-dependencies]
assert_matches = "1.5.0"
assert_matches.workspace = true
regex = "1.7.1"
reqwest = { version = "0.11", default-features = false }

2
certs/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.pem
*.key

View File

@ -3,4 +3,12 @@ listen_on = "127.0.0.1:8080"
[irc]
listen_on = "127.0.0.1:6667"
server_name = "irc.localhost"
server_name = "irc.localhost"
[xmpp]
listen_on = "127.0.0.1:5222"
cert = "./certs/xmpp.pem"
key = "./certs/xmpp.key"
[storage]
db_path = "db.sqlite"

30
crates/README.md Normal file
View File

@ -0,0 +1,30 @@
## Dependency diagram of the project
```mermaid
graph TD;
lavina-->mgmt-api;
lavina-->projection-irc;
lavina-->projection-xmpp;
lavina-->lavina-core;
projection-irc-->proto-irc;
projection-irc-->lavina-core;
projection-xmpp-->proto-xmpp;
projection-xmpp-->lavina-core;
sim-irc-->proto-irc;
sim-irc-->mgmt-api;
sim-xmpp-->proto-xmpp;
sim-xmpp-->mgmt-api;
workspace-->lavina;
workspace-->sim-irc;
workspace-->sim-xmpp;
```
A few rules:
- Only projections should be direct deps of `lavina`, there is no need to depend on `proto-*` crates.
- On the other hand, projections should not be dependencies of `sim-*` crates.
- `lavina-core` does not depend on protocol-specific crates.

View File

@ -0,0 +1,12 @@
[package]
name = "lavina-core"
edition = "2021"
version.workspace = true
[dependencies]
anyhow.workspace = true
sqlx = { version = "0.7.0-alpha.2", features = ["sqlite", "migrate"] }
serde.workspace = true
tokio.workspace = true
tracing.workspace = true
prometheus.workspace = true

View File

@ -0,0 +1,31 @@
create table users(
id integer primary key autoincrement not null,
name string unique not null
);
-- for development only, replace with properly hashed passwords later
create table challenges_plain_password(
user_id integer primary key not null,
password string not null
);
create table rooms(
id integer primary key autoincrement not null,
name string unique not null,
topic string not null,
message_count integer not null default 0
);
create table messages(
room_id integer not null,
id integer not null, -- unique per room, sequential in one room
content string not null,
primary key (room_id, id)
);
create table memberships(
user_id integer not null,
room_id integer not null,
status integer not null, -- 0 for not-joined, 1 for joined, 2 for banned
primary key (user_id, room_id)
);

View File

@ -0,0 +1 @@
alter table messages add author_id integer null references users(id);

View File

@ -1,3 +1,8 @@
//! Domain definitions and implementation of common chat logic.
pub mod player;
pub mod prelude;
pub mod repo;
pub mod room;
pub mod terminator;
mod table;

View File

@ -0,0 +1,420 @@
//! Domain of chat participants.
//!
//! Player is a single user account, which is used to participate in chats,
//! including sending messages, receiving messaged, retrieving history and running privileged actions.
//! A player corresponds to a single user account. Usually a person has only one account,
//! but it is possible to have multiple accounts for one person and therefore multiple player entities.
//!
//! A player actor is a serial handler of commands from a single player. It is preferable to run all per-player validations in the player actor,
//! so that they don't overload the room actor.
use std::{
collections::{HashMap, HashSet},
sync::{Arc, RwLock},
};
use prometheus::{IntGauge, Registry as MetricsRegistry};
use serde::Serialize;
use tokio::{
sync::mpsc::{channel, Receiver, Sender},
task::JoinHandle,
};
use crate::prelude::*;
use crate::room::{RoomHandle, RoomId, RoomInfo, RoomRegistry};
use crate::table::{AnonTable, Key as AnonKey};
/// Opaque player identifier. Cannot contain spaces, must be shorter than 32.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
pub struct PlayerId(Str);
impl PlayerId {
pub fn from(str: impl Into<Str>) -> Result<PlayerId> {
let bytes = str.into();
if bytes.len() > 32 {
return Err(fail("Nickname cannot be longer than 32 symbols"));
}
if bytes.contains(' ') {
return Err(anyhow::Error::msg("Nickname cannot contain spaces"));
}
Ok(PlayerId(bytes))
}
pub fn as_inner(&self) -> &Str {
&self.0
}
pub fn into_inner(self) -> Str {
self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ConnectionId(pub AnonKey);
pub struct PlayerConnection {
pub connection_id: ConnectionId,
pub receiver: Receiver<Updates>,
player_handle: PlayerHandle,
}
impl PlayerConnection {
pub async fn send_message(&mut self, room_id: RoomId, body: Str) -> Result<()> {
self.player_handle
.send_message(room_id, self.connection_id.clone(), body)
.await
}
pub async fn join_room(&mut self, room_id: RoomId) -> Result<JoinResult> {
self.player_handle.join_room(room_id, self.connection_id.clone()).await
}
pub async fn change_topic(&mut self, room_id: RoomId, new_topic: Str) -> Result<()> {
let (promise, deferred) = oneshot();
let cmd = Cmd::ChangeTopic {
room_id,
new_topic,
promise,
};
self.player_handle
.send(PlayerCommand::Cmd(cmd, self.connection_id.clone()))
.await;
Ok(deferred.await?)
}
pub async fn leave_room(&mut self, room_id: RoomId) -> Result<()> {
let (promise, deferred) = oneshot();
self.player_handle
.send(PlayerCommand::Cmd(
Cmd::LeaveRoom { room_id, promise },
self.connection_id.clone(),
))
.await;
Ok(deferred.await?)
}
pub async fn terminate(self) {
self.player_handle
.send(PlayerCommand::TerminateConnection(self.connection_id))
.await;
}
pub async fn get_rooms(&self) -> Result<Vec<RoomInfo>> {
let (promise, deferred) = oneshot();
self.player_handle.send(PlayerCommand::GetRooms(promise)).await;
Ok(deferred.await?)
}
}
/// Handle to a player actor.
#[derive(Clone)]
pub struct PlayerHandle {
tx: Sender<PlayerCommand>,
}
impl PlayerHandle {
pub async fn subscribe(&self) -> PlayerConnection {
let (sender, receiver) = channel(32);
let (promise, deferred) = oneshot();
let cmd = PlayerCommand::AddConnection { sender, promise };
let _ = self.tx.send(cmd).await;
let connection_id = deferred.await.unwrap();
PlayerConnection {
connection_id,
player_handle: self.clone(),
receiver,
}
}
pub async fn send_message(&self, room_id: RoomId, connection_id: ConnectionId, body: Str) -> Result<()> {
let (promise, deferred) = oneshot();
let cmd = Cmd::SendMessage { room_id, body, promise };
let _ = self.tx.send(PlayerCommand::Cmd(cmd, connection_id)).await;
Ok(deferred.await?)
}
pub async fn join_room(&self, room_id: RoomId, connection_id: ConnectionId) -> Result<JoinResult> {
let (promise, deferred) = oneshot();
let cmd = Cmd::JoinRoom { room_id, promise };
let _ = self.tx.send(PlayerCommand::Cmd(cmd, connection_id)).await;
Ok(deferred.await?)
}
async fn send(&self, command: PlayerCommand) {
let _ = self.tx.send(command).await;
}
pub async fn update(&self, update: Updates) {
self.send(PlayerCommand::Update(update)).await;
}
}
enum PlayerCommand {
/** Commands from connections */
AddConnection {
sender: Sender<Updates>,
promise: Promise<ConnectionId>,
},
TerminateConnection(ConnectionId),
Cmd(Cmd, ConnectionId),
/// Query - responds with a list of rooms the player is a member of.
GetRooms(Promise<Vec<RoomInfo>>),
/** Events from rooms */
Update(Updates),
Stop,
}
pub enum Cmd {
JoinRoom {
room_id: RoomId,
promise: Promise<JoinResult>,
},
LeaveRoom {
room_id: RoomId,
promise: Promise<()>,
},
SendMessage {
room_id: RoomId,
body: Str,
promise: Promise<()>,
},
ChangeTopic {
room_id: RoomId,
new_topic: Str,
promise: Promise<()>,
},
}
pub enum JoinResult {
Success(RoomInfo),
Banned,
}
/// Player update event type which is sent to a player actor and from there to a connection handler.
#[derive(Clone, Debug)]
pub enum Updates {
RoomTopicChanged {
room_id: RoomId,
new_topic: Str,
},
NewMessage {
room_id: RoomId,
author_id: PlayerId,
body: Str,
},
RoomJoined {
room_id: RoomId,
new_member_id: PlayerId,
},
RoomLeft {
room_id: RoomId,
former_member_id: PlayerId,
},
/// The player was banned from the room and left it immediately.
BannedFrom(RoomId),
}
/// Handle to a player registry — a shared data structure containing information about players.
#[derive(Clone)]
pub struct PlayerRegistry(Arc<RwLock<PlayerRegistryInner>>);
impl PlayerRegistry {
pub fn empty(room_registry: RoomRegistry, metrics: &mut MetricsRegistry) -> Result<PlayerRegistry> {
let metric_active_players = IntGauge::new("chat_players_active", "Number of alive player actors")?;
metrics.register(Box::new(metric_active_players.clone()))?;
let inner = PlayerRegistryInner {
room_registry,
players: HashMap::new(),
metric_active_players,
};
Ok(PlayerRegistry(Arc::new(RwLock::new(inner))))
}
pub async fn get_or_create_player(&mut self, id: PlayerId) -> PlayerHandle {
let mut inner = self.0.write().unwrap();
if let Some((handle, _)) = inner.players.get(&id) {
handle.clone()
} else {
let (handle, fiber) = Player::launch(id.clone(), inner.room_registry.clone());
inner.players.insert(id, (handle.clone(), fiber));
inner.metric_active_players.inc();
handle
}
}
pub async fn connect_to_player(&mut self, id: PlayerId) -> PlayerConnection {
let player_handle = self.get_or_create_player(id).await;
player_handle.subscribe().await
}
pub async fn shutdown_all(&mut self) -> Result<()> {
let mut inner = self.0.write().unwrap();
for (i, (k, j)) in inner.players.drain() {
k.send(PlayerCommand::Stop).await;
drop(k);
j.await?;
log::debug!("Player stopped #{i:?}")
}
log::debug!("All players stopped");
Ok(())
}
}
/// The player registry state representation.
struct PlayerRegistryInner {
room_registry: RoomRegistry,
players: HashMap<PlayerId, (PlayerHandle, JoinHandle<Player>)>,
metric_active_players: IntGauge,
}
/// Player actor inner state representation.
struct Player {
player_id: PlayerId,
connections: AnonTable<Sender<Updates>>,
my_rooms: HashMap<RoomId, RoomHandle>,
banned_from: HashSet<RoomId>,
rx: Receiver<PlayerCommand>,
handle: PlayerHandle,
rooms: RoomRegistry,
}
impl Player {
fn launch(player_id: PlayerId, rooms: RoomRegistry) -> (PlayerHandle, JoinHandle<Player>) {
let (tx, rx) = channel(32);
let handle = PlayerHandle { tx };
let handle_clone = handle.clone();
let player = Player {
player_id,
connections: AnonTable::new(),
my_rooms: HashMap::new(),
banned_from: HashSet::from([RoomId::from("Empty").unwrap()]),
rx,
handle,
rooms,
};
let fiber = tokio::task::spawn(player.main_loop());
(handle_clone, fiber)
}
async fn main_loop(mut self) -> Self {
while let Some(cmd) = self.rx.recv().await {
match cmd {
PlayerCommand::AddConnection { sender, promise } => {
let connection_id = self.connections.insert(sender);
if let Err(connection_id) = promise.send(ConnectionId(connection_id)) {
log::warn!("Connection {connection_id:?} terminated before finalization");
self.terminate_connection(connection_id);
}
}
PlayerCommand::TerminateConnection(connection_id) => {
self.terminate_connection(connection_id);
}
PlayerCommand::GetRooms(promise) => {
let mut response = vec![];
for (_, handle) in &self.my_rooms {
response.push(handle.get_room_info().await);
}
let _ = promise.send(response);
}
PlayerCommand::Update(update) => {
log::info!(
"Player received an update, broadcasting to {} connections",
self.connections.len()
);
match update {
Updates::BannedFrom(ref room_id) => {
self.banned_from.insert(room_id.clone());
self.my_rooms.remove(room_id);
}
_ => {}
}
for (_, connection) in &self.connections {
let _ = connection.send(update.clone()).await;
}
}
PlayerCommand::Cmd(cmd, connection_id) => self.handle_cmd(cmd, connection_id).await,
PlayerCommand::Stop => break,
}
}
log::debug!("Shutting down player actor #{:?}", self.player_id);
self
}
fn terminate_connection(&mut self, connection_id: ConnectionId) {
if let None = self.connections.pop(connection_id.0) {
log::warn!("Connection {connection_id:?} already terminated");
}
}
async fn handle_cmd(&mut self, cmd: Cmd, connection_id: ConnectionId) {
match cmd {
Cmd::JoinRoom { room_id, promise } => {
if self.banned_from.contains(&room_id) {
let _ = promise.send(JoinResult::Banned);
return;
}
let room = match self.rooms.get_or_create_room(room_id.clone()).await {
Ok(room) => room,
Err(e) => {
log::error!("Failed to get or create room: {e}");
return;
}
};
room.subscribe(self.player_id.clone(), self.handle.clone()).await;
self.my_rooms.insert(room_id.clone(), room.clone());
let room_info = room.get_room_info().await;
let _ = promise.send(JoinResult::Success(room_info));
let update = Updates::RoomJoined {
room_id,
new_member_id: self.player_id.clone(),
};
self.broadcast_update(update, connection_id).await;
}
Cmd::LeaveRoom { room_id, promise } => {
let room = self.my_rooms.remove(&room_id);
if let Some(room) = room {
room.unsubscribe(&self.player_id).await;
let room_info = room.get_room_info().await;
}
let _ = promise.send(());
let update = Updates::RoomLeft {
room_id,
former_member_id: self.player_id.clone(),
};
self.broadcast_update(update, connection_id).await;
}
Cmd::SendMessage { room_id, body, promise } => {
let room = self.rooms.get_room(&room_id).await;
if let Some(room) = room {
room.send_message(self.player_id.clone(), body.clone()).await;
} else {
tracing::info!("no room found");
}
let _ = promise.send(());
let update = Updates::NewMessage {
room_id,
author_id: self.player_id.clone(),
body,
};
self.broadcast_update(update, connection_id).await;
}
Cmd::ChangeTopic {
room_id,
new_topic,
promise,
} => {
let room = self.rooms.get_room(&room_id).await;
if let Some(mut room) = room {
room.set_topic(self.player_id.clone(), new_topic.clone()).await;
} else {
tracing::info!("no room found");
}
let _ = promise.send(());
let update = Updates::RoomTopicChanged { room_id, new_topic };
self.broadcast_update(update, connection_id).await;
}
}
}
async fn broadcast_update(&self, update: Updates, except: ConnectionId) {
for (a, b) in &self.connections {
if ConnectionId(a) == except {
continue;
}
let _ = b.send(update.clone()).await;
}
}
}

View File

@ -2,6 +2,7 @@ pub use std::future::Future;
pub use tokio::pin;
pub use tokio::select;
pub use tokio::sync::oneshot::{channel as oneshot, Receiver as Deferred, Sender as Promise};
pub use tokio::task::JoinHandle;
pub mod log {
@ -9,5 +10,8 @@ pub mod log {
}
pub type Result<T> = std::result::Result<T, anyhow::Error>;
pub type Str = std::sync::Arc<str>;
pub type ByteVec = Vec<u8>;
pub fn fail(msg: &str) -> anyhow::Error {
anyhow::Error::msg(msg.to_owned())
}

View File

@ -0,0 +1,173 @@
//! Storage and persistence logic.
use std::str::FromStr;
use std::sync::Arc;
use anyhow::anyhow;
use serde::Deserialize;
use sqlx::sqlite::SqliteConnectOptions;
use sqlx::{ConnectOptions, Connection, FromRow, Sqlite, SqliteConnection, Transaction};
use tokio::sync::Mutex;
use crate::prelude::*;
#[derive(Deserialize, Debug, Clone)]
pub struct StorageConfig {
pub db_path: String,
}
#[derive(Clone)]
pub struct Storage {
conn: Arc<Mutex<SqliteConnection>>,
}
impl Storage {
pub async fn open(config: StorageConfig) -> Result<Storage> {
let opts = SqliteConnectOptions::from_str(&*config.db_path)?.create_if_missing(true);
let mut conn = opts.connect().await?;
let migrator = sqlx::migrate!();
migrator.run(&mut conn).await?;
log::info!("Migrations passed");
let conn = Arc::new(Mutex::new(conn));
Ok(Storage { conn })
}
pub async fn retrieve_user_by_name(&mut self, name: &str) -> Result<Option<StoredUser>> {
let mut executor = self.conn.lock().await;
let res = sqlx::query_as(
"select u.id, u.name, c.password
from users u left join challenges_plain_password c on u.id = c.user_id
where u.name = ?;",
)
.bind(name)
.fetch_optional(&mut *executor)
.await?;
Ok(res)
}
pub async fn retrieve_room_by_name(&mut self, name: &str) -> Result<Option<StoredRoom>> {
let mut executor = self.conn.lock().await;
let res = sqlx::query_as(
"select id, name, topic, message_count
from rooms
where name = ?;",
)
.bind(name)
.fetch_optional(&mut *executor)
.await?;
Ok(res)
}
pub async fn create_new_room(&mut self, name: &str, topic: &str) -> Result<u32> {
let mut executor = self.conn.lock().await;
let (id,): (u32,) = sqlx::query_as(
"insert into rooms(name, topic)
values (?, ?)
returning id;",
)
.bind(name)
.bind(topic)
.fetch_one(&mut *executor)
.await?;
Ok(id)
}
pub async fn insert_message(&mut self, room_id: u32, id: u32, content: &str, author_id: &str) -> Result<()> {
let mut executor = self.conn.lock().await;
let res: Option<(u32,)> = sqlx::query_as("select id from users where name = ?;")
.bind(author_id)
.fetch_optional(&mut *executor)
.await?;
let Some((author_id,)) = res else {
return Err(anyhow!("No such user"));
};
sqlx::query(
"insert into messages(room_id, id, content, author_id)
values (?, ?, ?, ?);
update rooms set message_count = message_count + 1 where id = ?;",
)
.bind(room_id)
.bind(id)
.bind(content)
.bind(author_id)
.bind(room_id)
.execute(&mut *executor)
.await?;
Ok(())
}
pub async fn close(self) -> Result<()> {
let res = match Arc::try_unwrap(self.conn) {
Ok(e) => e,
Err(_) => return Err(fail("failed to acquire DB ownership on shutdown")),
};
let res = res.into_inner();
res.close().await?;
Ok(())
}
pub async fn create_user(&mut self, name: &str) -> Result<()> {
let query = sqlx::query(
"insert into users(name)
values (?);",
)
.bind(name);
let mut executor = self.conn.lock().await;
query.execute(&mut *executor).await?;
Ok(())
}
pub async fn set_password<'a>(&'a mut self, name: &'a str, pwd: &'a str) -> Result<Option<()>> {
async fn inner(txn: &mut Transaction<'_, Sqlite>, name: &str, pwd: &str) -> Result<Option<()>> {
let id: Option<(u32,)> = sqlx::query_as("select * from users where name = ? limit 1;")
.bind(name)
.fetch_optional(&mut **txn)
.await?;
let Some((id,)) = id else {
return Ok(None);
};
sqlx::query("insert or replace into challenges_plain_password(user_id, password) values (?, ?);")
.bind(id)
.bind(pwd)
.execute(&mut **txn)
.await?;
Ok(Some(()))
}
let mut executor = self.conn.lock().await;
let mut tx = executor.begin().await?;
let res = inner(&mut tx, name, pwd).await;
match res {
Ok(e) => {
tx.commit().await?;
Ok(e)
}
Err(e) => {
tx.rollback().await?;
Err(e)
}
}
}
}
#[derive(FromRow)]
pub struct StoredUser {
pub id: u32,
pub name: String,
pub password: Option<String>,
}
#[derive(FromRow)]
pub struct StoredRoom {
pub id: u32,
pub name: String,
pub topic: String,
pub message_count: u32,
}

View File

@ -0,0 +1,213 @@
//! Domain of rooms — chats with multiple participants.
use std::{collections::HashMap, hash::Hash, sync::Arc};
use prometheus::{IntGauge, Registry as MetricRegistry};
use serde::Serialize;
use tokio::sync::RwLock as AsyncRwLock;
use crate::player::{PlayerHandle, PlayerId, Updates};
use crate::prelude::*;
use crate::repo::Storage;
/// Opaque room id
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
pub struct RoomId(Str);
impl RoomId {
pub fn from(str: impl Into<Str>) -> Result<RoomId> {
let bytes = str.into();
if bytes.len() > 32 {
return Err(anyhow::Error::msg("Room name cannot be longer than 32 symbols"));
}
if bytes.contains(' ') {
return Err(anyhow::Error::msg("Room name cannot contain spaces"));
}
Ok(RoomId(bytes))
}
pub fn as_inner(&self) -> &Str {
&self.0
}
pub fn into_inner(self) -> Str {
self.0
}
}
/// Shared datastructure for storing metadata about rooms.
#[derive(Clone)]
pub struct RoomRegistry(Arc<AsyncRwLock<RoomRegistryInner>>);
impl RoomRegistry {
pub fn new(metrics: &mut MetricRegistry, storage: Storage) -> Result<RoomRegistry> {
let metric_active_rooms = IntGauge::new("chat_rooms_active", "Number of alive room actors")?;
metrics.register(Box::new(metric_active_rooms.clone()))?;
let inner = RoomRegistryInner {
rooms: HashMap::new(),
metric_active_rooms,
storage,
};
Ok(RoomRegistry(Arc::new(AsyncRwLock::new(inner))))
}
pub async fn get_or_create_room(&mut self, room_id: RoomId) -> Result<RoomHandle> {
let mut inner = self.0.write().await;
if let Some(room_handle) = inner.rooms.get(&room_id) {
// room was already loaded into memory
log::debug!("Room {} was loaded already", &room_id.0);
Ok(room_handle.clone())
} else if let Some(stored_room) = inner.storage.retrieve_room_by_name(&*room_id.0).await? {
// room exists, but was not loaded
log::debug!("Loading room {}...", &room_id.0);
let room = Room {
storage_id: stored_room.id,
room_id: room_id.clone(),
subscriptions: HashMap::new(), // TODO figure out how to populate subscriptions
topic: stored_room.topic.into(),
message_count: stored_room.message_count,
storage: inner.storage.clone(),
};
let room_handle = RoomHandle(Arc::new(AsyncRwLock::new(room)));
inner.rooms.insert(room_id, room_handle.clone());
inner.metric_active_rooms.inc();
Ok(room_handle)
} else {
// room does not exist, create it and load
log::debug!("Creating room {}...", &room_id.0);
let topic = "New room";
let id = inner.storage.create_new_room(&*room_id.0, &*topic).await?;
let room = Room {
storage_id: id,
room_id: room_id.clone(),
subscriptions: HashMap::new(),
topic: topic.into(),
message_count: 0,
storage: inner.storage.clone(),
};
let room_handle = RoomHandle(Arc::new(AsyncRwLock::new(room)));
inner.rooms.insert(room_id, room_handle.clone());
inner.metric_active_rooms.inc();
Ok(room_handle)
}
}
pub async fn get_room(&self, room_id: &RoomId) -> Option<RoomHandle> {
let inner = self.0.read().await;
let res = inner.rooms.get(room_id);
res.map(|r| r.clone())
}
pub async fn get_all_rooms(&self) -> Vec<RoomInfo> {
let handles = {
let inner = self.0.read().await;
let handles = inner.rooms.values().cloned().collect::<Vec<_>>();
handles
};
let mut res = vec![];
for i in handles {
res.push(i.get_room_info().await)
}
res
}
}
struct RoomRegistryInner {
rooms: HashMap<RoomId, RoomHandle>,
metric_active_rooms: IntGauge,
storage: Storage,
}
#[derive(Clone)]
pub struct RoomHandle(Arc<AsyncRwLock<Room>>);
impl RoomHandle {
pub async fn subscribe(&self, player_id: PlayerId, player_handle: PlayerHandle) {
let mut lock = self.0.write().await;
lock.add_subscriber(player_id, player_handle).await;
}
pub async fn unsubscribe(&self, player_id: &PlayerId) {
let mut lock = self.0.write().await;
lock.subscriptions.remove(player_id);
let update = Updates::RoomLeft {
room_id: lock.room_id.clone(),
former_member_id: player_id.clone(),
};
lock.broadcast_update(update, player_id).await;
}
pub async fn send_message(&self, player_id: PlayerId, body: Str) {
let mut lock = self.0.write().await;
let res = lock.send_message(player_id, body).await;
if let Err(err) = res {
log::warn!("Failed to send message: {err:?}");
}
}
pub async fn get_room_info(&self) -> RoomInfo {
let lock = self.0.read().await;
RoomInfo {
id: lock.room_id.clone(),
members: lock.subscriptions.keys().map(|x| x.clone()).collect::<Vec<_>>(),
topic: lock.topic.clone(),
}
}
pub async fn set_topic(&mut self, changer_id: PlayerId, new_topic: Str) {
let mut lock = self.0.write().await;
lock.topic = new_topic.clone();
let update = Updates::RoomTopicChanged {
room_id: lock.room_id.clone(),
new_topic: new_topic.clone(),
};
lock.broadcast_update(update, &changer_id).await;
}
}
struct Room {
storage_id: u32,
room_id: RoomId,
subscriptions: HashMap<PlayerId, PlayerHandle>,
message_count: u32,
topic: Str,
storage: Storage,
}
impl Room {
async fn add_subscriber(&mut self, player_id: PlayerId, player_handle: PlayerHandle) {
tracing::info!("Adding a subscriber to room");
self.subscriptions.insert(player_id.clone(), player_handle);
let update = Updates::RoomJoined {
room_id: self.room_id.clone(),
new_member_id: player_id.clone(),
};
self.broadcast_update(update, &player_id).await;
}
async fn send_message(&mut self, author_id: PlayerId, body: Str) -> Result<()> {
tracing::info!("Adding a message to room");
self.storage
.insert_message(self.storage_id, self.message_count, &body, &*author_id.as_inner())
.await?;
self.message_count += 1;
let update = Updates::NewMessage {
room_id: self.room_id.clone(),
author_id: author_id.clone(),
body,
};
self.broadcast_update(update, &author_id).await;
Ok(())
}
async fn broadcast_update(&self, update: Updates, except: &PlayerId) {
tracing::debug!("Broadcasting an update to {} subs", self.subscriptions.len());
for (player_id, sub) in &self.subscriptions {
if player_id == except {
continue;
}
log::info!("Sending a message from room to player");
sub.update(update.clone()).await;
}
}
}
#[derive(Serialize)]
pub struct RoomInfo {
pub id: RoomId,
pub members: Vec<PlayerId>,
pub topic: Str,
}

View File

@ -21,7 +21,7 @@ impl<V> AnonTable<V> {
pub fn insert(&mut self, value: V) -> Key {
let id = self.next;
self.next += 1;
self.inner.insert(id, value); // should be always empty
self.inner.insert(id, value); // should be always Empty
Key(id)
}
@ -44,15 +44,15 @@ impl<V> AnonTable<V> {
pub struct AnonTableIterator<'a, V>(<&'a HashMap<u32, V> as IntoIterator>::IntoIter);
impl<'a, V> Iterator for AnonTableIterator<'a, V> {
type Item = &'a V;
type Item = (Key, &'a V);
fn next(&mut self) -> Option<&'a V> {
self.0.next().map(|a| a.1)
fn next(&mut self) -> Option<(Key, &'a V)> {
self.0.next().map(|(k, v)| (Key(*k), v))
}
}
impl<'a, V> IntoIterator for &'a AnonTable<V> {
type Item = &'a V;
type Item = (Key, &'a V);
type IntoIter = AnonTableIterator<'a, V>;
@ -63,15 +63,15 @@ impl<'a, V> IntoIterator for &'a AnonTable<V> {
pub struct AnonTableMutIterator<'a, V>(<&'a mut HashMap<u32, V> as IntoIterator>::IntoIter);
impl<'a, V> Iterator for AnonTableMutIterator<'a, V> {
type Item = &'a mut V;
type Item = (Key, &'a mut V);
fn next(&mut self) -> Option<&'a mut V> {
self.0.next().map(|a| a.1)
fn next(&mut self) -> Option<(Key, &'a mut V)> {
self.0.next().map(|(k, v)| (Key(*k), v))
}
}
impl<'a, V> IntoIterator for &'a mut AnonTable<V> {
type Item = &'a mut V;
type Item = (Key, &'a mut V);
type IntoIter = AnonTableMutIterator<'a, V>;

View File

@ -0,0 +1,28 @@
use crate::prelude::*;
pub struct Terminator {
signal: Promise<()>,
completion: JoinHandle<Result<()>>,
}
impl Terminator {
pub async fn terminate(self) -> Result<()> {
match self.signal.send(()) {
Ok(()) => {}
Err(_) => log::warn!("Termination channel is dropped"),
}
self.completion.await??;
Ok(())
}
/// Used to spawn managed tasks with support for graceful shutdown
pub fn spawn<Fun, Fut>(launcher: Fun) -> Terminator
where
Fun: FnOnce(Deferred<()>) -> Fut,
Fut: Future<Output = Result<()>> + Send + 'static,
{
let (signal, rx) = oneshot();
let future = launcher(rx);
let completion = tokio::task::spawn(future);
Terminator { signal, completion }
}
}

View File

@ -0,0 +1,8 @@
[package]
name = "mgmt-api"
edition = "2021"
version.workspace = true
publish = false
[dependencies]
serde.workspace = true

View File

@ -0,0 +1,29 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct ErrorResponse<'a> {
pub code: &'a str,
pub message: &'a str,
}
#[derive(Serialize, Deserialize)]
pub struct CreatePlayerRequest<'a> {
pub name: &'a str,
}
#[derive(Serialize, Deserialize)]
pub struct ChangePasswordRequest<'a> {
pub player_name: &'a str,
pub password: &'a str,
}
pub mod paths {
pub const CREATE_PLAYER: &'static str = "/mgmt/create_player";
pub const SET_PASSWORD: &'static str = "/mgmt/set_password";
}
pub mod errors {
pub const INVALID_PATH: &'static str = "invalid_path";
pub const MALFORMED_REQUEST: &'static str = "malformed_request";
pub const PLAYER_NOT_FOUND: &'static str = "player_not_found";
}

View File

@ -0,0 +1,16 @@
[package]
name = "projection-irc"
edition = "2021"
version.workspace = true
[dependencies]
lavina-core.workspace = true
tracing.workspace = true
anyhow.workspace = true
serde.workspace = true
tokio.workspace = true
prometheus.workspace = true
futures-util.workspace = true
nonempty.workspace = true
proto-irc = { path = "../proto-irc" }

View File

@ -0,0 +1,786 @@
use std::collections::HashMap;
use std::net::SocketAddr;
use anyhow::{anyhow, Result};
use futures_util::future::join_all;
use nonempty::nonempty;
use nonempty::NonEmpty;
use prometheus::{IntCounter, IntGauge, Registry as MetricsRegistry};
use serde::Deserialize;
use tokio::io::AsyncReadExt;
use tokio::io::{AsyncBufReadExt, AsyncWrite, AsyncWriteExt, BufReader, BufWriter};
use tokio::net::tcp::{ReadHalf, WriteHalf};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::mpsc::channel;
use lavina_core::player::*;
use lavina_core::prelude::*;
use lavina_core::repo::Storage;
use lavina_core::room::{RoomId, RoomInfo, RoomRegistry};
use lavina_core::terminator::Terminator;
use proto_irc::client::{client_message, ClientMessage};
use proto_irc::server::{AwayStatus, ServerMessage, ServerMessageBody};
use proto_irc::user::PrefixedNick;
use proto_irc::{Chan, Recipient};
#[derive(Deserialize, Debug, Clone)]
pub struct ServerConfig {
pub listen_on: SocketAddr,
pub server_name: Str,
}
#[derive(Debug)]
struct RegisteredUser {
nickname: Str,
/**
* Username is mostly unused in modern IRC.
*
* [https://stackoverflow.com/questions/31666247/what-is-the-difference-between-the-nick-username-and-real-name-in-irc-and-wha]
*/
username: Str,
realname: Str,
}
async fn handle_socket(
config: ServerConfig,
mut stream: TcpStream,
socket_addr: &SocketAddr,
players: PlayerRegistry,
rooms: RoomRegistry,
termination: Deferred<()>, // TODO use it to stop the connection gracefully
mut storage: Storage,
) -> Result<()> {
log::info!("Received an IRC connection from {socket_addr}");
let (reader, writer) = stream.split();
let mut reader: BufReader<ReadHalf> = BufReader::new(reader);
let mut writer = BufWriter::new(writer);
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone().into()),
body: ServerMessageBody::Notice {
first_target: "*".into(),
rest_targets: vec![],
text: "Welcome to my server!".into(),
},
}
.write_async(&mut writer)
.await?;
writer.flush().await?;
let registered_user: Result<RegisteredUser> = handle_registration(&mut reader, &mut writer, &mut storage).await;
match registered_user {
Ok(user) => {
log::debug!("User registered");
handle_registered_socket(config, players, rooms, &mut reader, &mut writer, user).await?;
}
Err(_) => {
log::debug!("Registration failed");
}
}
stream.shutdown().await?;
Ok(())
}
async fn handle_registration<'a>(
reader: &mut BufReader<ReadHalf<'a>>,
writer: &mut BufWriter<WriteHalf<'a>>,
storage: &mut Storage,
) -> Result<RegisteredUser> {
let mut buffer = vec![];
let mut future_nickname: Option<Str> = None;
let mut future_username: Option<(Str, Str)> = None;
let mut pass: Option<Str> = None;
let user = loop {
let res = read_irc_message(reader, &mut buffer).await;
let res = match res {
Ok(len) => {
if len == 0 {
log::info!("Terminating socket");
break Err(anyhow::Error::msg("EOF"));
}
match std::str::from_utf8(&buffer[..len-2]) {
Ok(res) => res,
Err(e) => break Err(e.into()),
}
}
Err(err) => {
log::warn!("Failed to read from socket: {err}");
break Err(err.into());
}
};
log::debug!("Incoming raw IRC message: '{res}'");
let parsed = client_message(res);
match parsed {
Ok(msg) => {
log::debug!("Incoming IRC message: {msg:?}");
match msg {
ClientMessage::Pass { password } => {
pass = Some(password);
}
ClientMessage::Nick { nickname } => {
if let Some((username, realname)) = future_username {
break Ok(RegisteredUser {
nickname,
username,
realname,
});
} else {
future_nickname = Some(nickname);
}
}
ClientMessage::User { username, realname } => {
if let Some(nickname) = future_nickname {
break Ok(RegisteredUser {
nickname,
username,
realname,
});
} else {
future_username = Some((username, realname));
}
}
_ => {}
}
}
Err(err) => {
log::warn!("Failed to parse IRC message: {err}");
}
}
buffer.clear();
}?;
let stored_user = storage.retrieve_user_by_name(&*user.nickname).await?;
let stored_user = match stored_user {
Some(u) => u,
None => {
log::info!("User '{}' not found", user.nickname);
return Err(anyhow!("no user found"));
}
};
if stored_user.password.is_none() {
log::info!("Password not defined for user '{}'", user.nickname);
return Err(anyhow!("password is not defined"));
}
if stored_user.password.as_deref() != pass.as_deref() {
log::info!("Incorrect password supplied for user '{}'", user.nickname);
return Err(anyhow!("passwords do not match"));
}
// TODO properly implement session temination
Ok(user)
}
async fn handle_registered_socket<'a>(
config: ServerConfig,
mut players: PlayerRegistry,
rooms: RoomRegistry,
reader: &mut BufReader<ReadHalf<'a>>,
writer: &mut BufWriter<WriteHalf<'a>>,
user: RegisteredUser,
) -> Result<()> {
let mut buffer = vec![];
log::info!("Handling registered user: {user:?}");
let player_id = PlayerId::from(user.nickname.clone())?;
let mut connection = players.connect_to_player(player_id.clone()).await;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone()),
body: ServerMessageBody::N001Welcome {
client: user.nickname.clone(),
text: "Welcome to Kek Server".into(),
},
}
.write_async(writer)
.await?;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone()),
body: ServerMessageBody::N002YourHost {
client: user.nickname.clone(),
text: "Welcome to Kek Server".into(),
},
}
.write_async(writer)
.await?;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone()),
body: ServerMessageBody::N003Created {
client: user.nickname.clone(),
text: "Welcome to Kek Server".into(),
},
}
.write_async(writer)
.await?;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone()),
body: ServerMessageBody::N004MyInfo {
client: user.nickname.clone(),
hostname: config.server_name.clone(),
softname: "kek-0.1.alpha.3".into(),
},
}
.write_async(writer)
.await?;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone()),
body: ServerMessageBody::N005ISupport {
client: user.nickname.clone(),
params: "CHANTYPES=#".into(),
},
}
.write_async(writer)
.await?;
let rooms_list = connection.get_rooms().await?;
for room in &rooms_list {
produce_on_join_cmd_messages(&config, &user, &Chan::Global(room.id.as_inner().clone()), room, writer).await?;
}
writer.flush().await?;
loop {
select! {
biased;
len = read_irc_message(reader, &mut buffer) => {
let len = len?;
let len = if len == 0 {
log::info!("EOF, Terminating socket");
break;
} else {
len
};
let incoming = std::str::from_utf8(&buffer[0..len-2])?;
if let HandleResult::Leave = handle_incoming_message(incoming, &config, &user, &rooms, &mut connection, writer).await? {
break;
}
buffer.clear();
},
update = connection.receiver.recv() => {
if let Some(update) = update {
handle_update(&config, &user, &player_id, writer, &rooms, update).await?;
} else {
log::warn!("Player is terminated, must terminate the connection");
break;
}
}
}
}
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone()),
body: ServerMessageBody::Error {
reason: "Leaving the server".into(),
},
}
.write_async(writer)
.await?;
writer.flush().await?;
connection.terminate().await;
Ok(())
}
async fn read_irc_message(reader: &mut BufReader<ReadHalf<'_>>, buf: &mut Vec<u8>) -> Result<usize> {
let mut size = 0;
'outer: loop {
let res = reader.read_until(b'\r', buf).await?;
size += res;
let next = reader.read_u8().await?;
buf.push(next);
size += 1;
if next != b'\n' {
continue 'outer;
}
return Ok(size);
}
}
async fn handle_update(
config: &ServerConfig,
user: &RegisteredUser,
player_id: &PlayerId,
writer: &mut (impl AsyncWrite + Unpin),
rooms: &RoomRegistry,
update: Updates,
) -> Result<()> {
log::debug!("Sending irc message to player {player_id:?} on update {update:?}");
match update {
Updates::RoomJoined { new_member_id, room_id } => {
if player_id == &new_member_id {
if let Some(room) = rooms.get_room(&room_id).await {
let room_info = room.get_room_info().await;
let chan = Chan::Global(room_id.as_inner().clone());
produce_on_join_cmd_messages(&config, &user, &chan, &room_info, writer).await?;
writer.flush().await?;
} else {
log::warn!("Received join to a non-existant room");
}
} else {
ServerMessage {
tags: vec![],
sender: Some(new_member_id.as_inner().clone()),
body: ServerMessageBody::Join(Chan::Global(room_id.as_inner().clone())),
}
.write_async(writer)
.await?;
writer.flush().await?
}
}
Updates::RoomLeft {
room_id,
former_member_id,
} => {
ServerMessage {
tags: vec![],
sender: Some(former_member_id.as_inner().clone()),
body: ServerMessageBody::Part(Chan::Global(room_id.as_inner().clone())),
}
.write_async(writer)
.await?;
writer.flush().await?
}
Updates::NewMessage {
author_id,
room_id,
body,
} => {
ServerMessage {
tags: vec![],
sender: Some(author_id.as_inner().clone()),
body: ServerMessageBody::PrivateMessage {
target: Recipient::Chan(Chan::Global(room_id.as_inner().clone())),
body: body.clone(),
},
}
.write_async(writer)
.await?;
writer.flush().await?
}
Updates::RoomTopicChanged { room_id, new_topic } => {
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone()),
body: ServerMessageBody::N332Topic {
client: user.nickname.clone(),
chat: Chan::Global(room_id.as_inner().clone()),
topic: new_topic,
},
}
.write_async(writer)
.await?;
writer.flush().await?
}
Updates::BannedFrom(room_id) => {
// TODO think about the case when the user was banned, but was not in the room - no need to send PART in this case
ServerMessage {
tags: vec![],
sender: Some(player_id.as_inner().clone()),
body: ServerMessageBody::Part(Chan::Global(room_id.as_inner().clone())),
}
.write_async(writer)
.await?;
writer.flush().await?
}
}
Ok(())
}
enum HandleResult {
Continue,
Leave,
}
async fn handle_incoming_message(
buffer: &str,
config: &ServerConfig,
user: &RegisteredUser,
rooms: &RoomRegistry,
user_handle: &mut PlayerConnection,
writer: &mut (impl AsyncWrite + Unpin),
) -> Result<HandleResult> {
log::debug!("Incoming raw IRC message: '{buffer}'");
let parsed = client_message(buffer);
log::debug!("Incoming IRC message: {parsed:?}");
match parsed {
Ok(msg) => match msg {
ClientMessage::Ping { token } => {
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone()),
body: ServerMessageBody::Pong {
from: config.server_name.clone(),
token,
},
}
.write_async(writer)
.await?;
writer.flush().await?;
}
ClientMessage::Join(ref chan) => {
handle_join(&config, &user, user_handle, chan, writer).await?;
}
ClientMessage::Part { chan, message } => {
handle_part(config, user, user_handle, &chan, writer).await?;
}
ClientMessage::PrivateMessage { recipient, body } => match recipient {
Recipient::Chan(Chan::Global(chan)) => {
let room_id = RoomId::from(chan)?;
user_handle.send_message(room_id, body).await?;
}
_ => log::warn!("Unsupported target type"),
},
ClientMessage::Topic { chan, topic } => {
match chan {
Chan::Global(chan) => {
let room_id = RoomId::from(chan)?;
user_handle.change_topic(room_id.clone(), topic.clone()).await?;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone()),
body: ServerMessageBody::N332Topic {
client: user.nickname.clone(),
chat: Chan::Global(room_id.as_inner().clone()),
topic,
},
}
.write_async(writer)
.await?;
writer.flush().await?;
}
Chan::Local(_) => {}
};
}
ClientMessage::Who { target } => match &target {
Recipient::Nick(nick) => {
// TODO handle non-existing user
let mut username = format!("~{nick}");
let mut host = format!("user/{nick}");
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone()),
body: user_to_who_msg(config, user, nick),
}
.write_async(writer)
.await?;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone()),
body: ServerMessageBody::N315EndOfWho {
client: user.nickname.clone(),
mask: target.clone(),
msg: "End of WHO list".into(),
},
}
.write_async(writer)
.await?;
writer.flush().await?;
}
Recipient::Chan(Chan::Global(chan)) => {
let room = rooms.get_room(&RoomId::from(chan.clone())?).await;
if let Some(room) = room {
let room_info = room.get_room_info().await;
for member in room_info.members {
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone()),
body: user_to_who_msg(config, user, member.as_inner()),
}
.write_async(writer)
.await?;
}
}
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone()),
body: ServerMessageBody::N315EndOfWho {
client: user.nickname.clone(),
mask: target.clone(),
msg: "End of WHO list".into(),
},
}
.write_async(writer)
.await?;
writer.flush().await?;
}
Recipient::Chan(Chan::Local(_)) => {
log::warn!("Local chans not supported");
}
},
ClientMessage::Mode { target } => {
match target {
Recipient::Nick(nickname) => {
if nickname == user.nickname {
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone()),
body: ServerMessageBody::N221UserModeIs {
client: user.nickname.clone(),
modes: "+r".into(),
},
}
.write_async(writer)
.await?;
writer.flush().await?;
} else {
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone()),
body: ServerMessageBody::N502UsersDontMatch {
client: user.nickname.clone(),
message: "Cant change mode for other users".into(),
},
}
.write_async(writer)
.await?;
writer.flush().await?;
}
}
Recipient::Chan(_) => {
// TODO handle chan mode handling
}
}
}
ClientMessage::Quit { reason } => {
log::info!("Received QUIT");
return Ok(HandleResult::Leave);
}
cmd => {
log::warn!("Not implemented handler for client command: {cmd:?}");
}
},
Err(err) => {
log::warn!("Failed to parse IRC message: {err}");
}
}
Ok(HandleResult::Continue)
}
fn user_to_who_msg(config: &ServerConfig, requestor: &RegisteredUser, target_user_nickname: &Str) -> ServerMessageBody {
// Username is equal to nickname
let username = format!("~{target_user_nickname}").into();
// User's host is not public, replace it with `user/<nickname>` pattern
let mut host = format!("user/{target_user_nickname}").into();
ServerMessageBody::N352WhoReply {
client: requestor.nickname.clone(),
username,
host,
server: config.server_name.clone(),
flags: AwayStatus::Here,
nickname: target_user_nickname.clone(),
hops: 0,
// TODO Realname is not available yet, should be matched to a future core's player field
realname: target_user_nickname.clone(),
}
}
async fn handle_join(
config: &ServerConfig,
user: &RegisteredUser,
user_handle: &mut PlayerConnection,
chan: &Chan,
writer: &mut (impl AsyncWrite + Unpin),
) -> Result<()> {
match chan {
Chan::Global(chan_name) => {
let room_id = RoomId::from(chan_name.clone())?;
if let JoinResult::Success(room_info) = user_handle.join_room(room_id).await? {
produce_on_join_cmd_messages(&config, &user, chan, &room_info, writer).await?;
} else {
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone()),
body: ServerMessageBody::N474BannedFromChan {
client: user.nickname.clone(),
chan: chan.clone(),
message: "U dun goofed".into(),
},
}
.write_async(writer)
.await?;
}
writer.flush().await?;
}
Chan::Local(_) => {}
};
Ok(())
}
async fn handle_part(
config: &ServerConfig,
user: &RegisteredUser,
user_handle: &mut PlayerConnection,
chan: &Chan,
writer: &mut (impl AsyncWrite + Unpin),
) -> Result<()> {
if let Chan::Global(chan_name) = chan {
let room_id = RoomId::from(chan_name.clone())?;
user_handle.leave_room(room_id).await?;
ServerMessage {
tags: vec![],
sender: Some(user.nickname.clone()),
body: ServerMessageBody::Part(Chan::Global(chan_name.clone())),
}
.write_async(writer)
.await?;
writer.flush().await?;
} else {
log::warn!("Local chans unsupported");
}
Ok(())
}
async fn produce_on_join_cmd_messages(
config: &ServerConfig,
user: &RegisteredUser,
chan: &Chan,
room_info: &RoomInfo,
writer: &mut (impl AsyncWrite + Unpin),
) -> Result<()> {
ServerMessage {
tags: vec![],
sender: Some(user.nickname.clone()),
body: ServerMessageBody::Join(chan.clone()),
}
.write_async(writer)
.await?;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone()),
body: ServerMessageBody::N332Topic {
client: user.nickname.clone(),
chat: chan.clone(),
topic: room_info.topic.clone(),
},
}
.write_async(writer)
.await?;
let prefixed_members: Vec<PrefixedNick> = room_info
.members
.iter()
.map(|member| PrefixedNick::from_str(member.clone().into_inner()))
.collect();
let non_empty_members: NonEmpty<PrefixedNick> =
NonEmpty::from_vec(prefixed_members).unwrap_or(nonempty![PrefixedNick::from_str(user.nickname.clone())]);
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone()),
body: ServerMessageBody::N353NamesReply {
client: user.nickname.clone(),
chan: chan.clone(),
members: non_empty_members.into(),
},
}
.write_async(writer)
.await?;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone()),
body: ServerMessageBody::N366NamesReplyEnd {
client: user.nickname.clone(),
chan: chan.clone(),
},
}
.write_async(writer)
.await?;
Ok(())
}
pub async fn launch(
config: ServerConfig,
players: PlayerRegistry,
rooms: RoomRegistry,
metrics: MetricsRegistry,
storage: Storage,
) -> Result<Terminator> {
log::info!("Starting IRC projection");
let (stopped_tx, mut stopped_rx) = channel(32);
let current_connections = IntGauge::new("irc_current_connections", "Open and alive TCP connections")?;
let total_connections = IntCounter::new("irc_total_connections", "Total number of opened connections")?;
metrics.register(Box::new(current_connections.clone()))?;
metrics.register(Box::new(total_connections.clone()))?;
let listener = TcpListener::bind(config.listen_on).await?;
let terminator = Terminator::spawn(|mut rx| async move {
// TODO probably should separate logic for accepting new connection and storing them
// into two tasks so that they don't block each other
let mut actors = HashMap::new();
loop {
select! {
biased;
_ = &mut rx => break,
stopped = stopped_rx.recv() => match stopped {
Some(stopped) => { let _ = actors.remove(&stopped); },
None => unreachable!(),
},
new_conn = listener.accept() => {
match new_conn {
Ok((stream, socket_addr)) => {
let config = config.clone();
total_connections.inc();
current_connections.inc();
log::debug!("Incoming connection from {socket_addr}");
if actors.contains_key(&socket_addr) {
log::warn!("Already contains connection form {socket_addr}");
// TODO kill the older connection and restart it
continue;
}
let terminator = Terminator::spawn(|termination| {
let players = players.clone();
let rooms = rooms.clone();
let current_connections_clone = current_connections.clone();
let stopped_tx = stopped_tx.clone();
let storage = storage.clone();
async move {
match handle_socket(config, stream, &socket_addr, players, rooms, termination, storage).await {
Ok(_) => log::info!("Connection terminated"),
Err(err) => log::warn!("Connection failed: {err}"),
}
current_connections_clone.dec();
stopped_tx.send(socket_addr).await?;
Ok(())
}
});
actors.insert(socket_addr, terminator);
},
Err(err) => log::warn!("Failed to accept new connection: {err}"),
}
},
}
}
log::info!("Stopping IRC projection");
join_all(actors.into_iter().map(|(socket_addr, terminator)| async move {
log::debug!("Stopping IRC connection at {socket_addr}");
match terminator.terminate().await {
Ok(_) => log::debug!("Stopped IRC connection at {socket_addr}"),
Err(err) => {
log::warn!("IRC connection to {socket_addr} finished with error: {err}")
}
}
}))
.await;
log::info!("Stopped IRC projection");
Ok(())
});
log::info!("Started IRC projection");
Ok(terminator)
}

View File

@ -0,0 +1,20 @@
[package]
name = "projection-xmpp"
edition = "2021"
version.workspace = true
[dependencies]
lavina-core.workspace = true
tracing.workspace = true
anyhow.workspace = true
serde.workspace = true
tokio.workspace = true
prometheus.workspace = true
futures-util.workspace = true
quick-xml.workspace = true
proto-xmpp = { path = "../proto-xmpp" }
uuid = { version = "1.3.0", features = ["v4"] }
tokio-rustls = "0.24.1"
rustls-pemfile = "1.0.2"
derive_more.workspace = true

View File

@ -0,0 +1,668 @@
#![feature(generators, generator_trait, type_alias_impl_trait, impl_trait_in_assoc_type)]
mod proto;
use std::collections::HashMap;
use std::fs::File;
use std::io::BufReader as SyncBufReader;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::anyhow;
use futures_util::future::join_all;
use prometheus::Registry as MetricsRegistry;
use quick_xml::events::{BytesDecl, Event};
use quick_xml::{NsReader, Writer};
use rustls_pemfile::{certs, read_one, Item as PemItem};
use serde::Deserialize;
use tokio::io::{AsyncBufRead, AsyncWrite, AsyncWriteExt, BufReader, BufWriter};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::mpsc::channel;
use tokio_rustls::rustls::{Certificate, PrivateKey};
use tokio_rustls::TlsAcceptor;
use lavina_core::player::{PlayerConnection, PlayerId, PlayerRegistry};
use lavina_core::prelude::*;
use lavina_core::room::{RoomId, RoomRegistry};
use lavina_core::terminator::Terminator;
use lavina_core::repo::Storage;
use proto_xmpp::bind::{BindResponse, Jid, Name, Resource, Server};
use proto_xmpp::client::{Iq, Message, MessageType, Presence};
use proto_xmpp::disco::*;
use proto_xmpp::roster::RosterQuery;
use proto_xmpp::sasl::AuthBody;
use proto_xmpp::session::Session;
use proto_xmpp::stream::*;
use proto_xmpp::xml::{Continuation, FromXml, Parser, ToXml};
use self::proto::{ClientPacket, IqClientBody};
#[derive(Deserialize, Debug, Clone)]
pub struct ServerConfig {
pub listen_on: SocketAddr,
pub cert: PathBuf,
pub key: PathBuf,
}
struct LoadedConfig {
cert: Certificate,
key: PrivateKey,
}
struct Authenticated {
player_id: PlayerId,
xmpp_name: Name,
xmpp_resource: Resource,
xmpp_muc_name: Resource,
}
pub async fn launch(
config: ServerConfig,
players: PlayerRegistry,
rooms: RoomRegistry,
metrics: MetricsRegistry,
storage: Storage,
) -> Result<Terminator> {
log::info!("Starting XMPP projection");
let certs = certs(&mut SyncBufReader::new(File::open(config.cert)?))?;
let certs = certs.into_iter().map(Certificate).collect::<Vec<_>>();
let key = match read_one(&mut SyncBufReader::new(File::open(config.key)?))? {
Some(PemItem::ECKey(k) | PemItem::PKCS8Key(k) | PemItem::RSAKey(k)) => PrivateKey(k),
_ => panic!("no keys in file"),
};
let loaded_config = Arc::new(LoadedConfig {
cert: certs.into_iter().next().expect("no certs in file"),
key,
});
let listener = TcpListener::bind(config.listen_on).await?;
let terminator = Terminator::spawn(|mut termination| async move {
let (stopped_tx, mut stopped_rx) = channel(32);
let mut actors = HashMap::new();
loop {
select! {
biased;
_ = &mut termination => break,
stopped = stopped_rx.recv() => match stopped {
Some(stopped) => { let _ = actors.remove(&stopped); },
None => unreachable!(),
},
new_conn = listener.accept() => {
match new_conn {
Ok((stream, socket_addr)) => {
log::debug!("Incoming connection from {socket_addr}");
if actors.contains_key(&socket_addr) {
log::warn!("Already contains connection form {socket_addr}");
// TODO kill the older connection and restart it
continue;
}
let players = players.clone();
let rooms = rooms.clone();
let storage = storage.clone();
let terminator = Terminator::spawn(|termination| {
let stopped_tx = stopped_tx.clone();
let loaded_config = loaded_config.clone();
async move {
match handle_socket(loaded_config, stream, &socket_addr, players, rooms, storage, termination).await {
Ok(_) => log::info!("Connection terminated"),
Err(err) => log::warn!("Connection failed: {err}"),
}
stopped_tx.send(socket_addr).await?;
Ok(())
}
});
actors.insert(socket_addr, terminator);
},
Err(err) => log::warn!("Failed to accept new connection: {err}"),
}
},
}
}
log::info!("Stopping XMPP projection");
join_all(actors.into_iter().map(|(socket_addr, terminator)| async move {
log::debug!("Stopping XMPP connection at {socket_addr}");
match terminator.terminate().await {
Ok(_) => log::debug!("Stopped XMPP connection at {socket_addr}"),
Err(err) => {
log::warn!("XMPP connection to {socket_addr} finished with error: {err}")
}
}
}))
.await;
log::info!("Stopped XMPP projection");
Ok(())
});
log::info!("Started XMPP projection");
Ok(terminator)
}
async fn handle_socket(
config: Arc<LoadedConfig>,
mut stream: TcpStream,
socket_addr: &SocketAddr,
mut players: PlayerRegistry,
rooms: RoomRegistry,
mut storage: Storage,
termination: Deferred<()>, // TODO use it to stop the connection gracefully
) -> Result<()> {
log::info!("Received an XMPP connection from {socket_addr}");
let mut reader_buf = vec![];
let (reader, writer) = stream.split();
let mut buf_reader = BufReader::new(reader);
let mut buf_writer = BufWriter::new(writer);
socket_force_tls(&mut buf_reader, &mut buf_writer, &mut reader_buf).await?;
let mut config = tokio_rustls::rustls::ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(vec![config.cert.clone()], config.key.clone())?;
config.key_log = Arc::new(tokio_rustls::rustls::KeyLogFile::new());
let acceptor = TlsAcceptor::from(Arc::new(config));
let new_stream = acceptor.accept(stream).await?;
log::debug!("TLS connection established");
let (a, b) = tokio::io::split(new_stream);
let mut xml_reader = NsReader::from_reader(BufReader::new(a));
let mut xml_writer = Writer::new(b);
let authenticated = socket_auth(&mut xml_reader, &mut xml_writer, &mut reader_buf, &mut storage).await?;
log::debug!("User authenticated");
let mut connection = players.connect_to_player(authenticated.player_id.clone()).await;
socket_final(
&mut xml_reader,
&mut xml_writer,
&mut reader_buf,
&authenticated,
&mut connection,
&rooms,
)
.await?;
let a = xml_reader.into_inner().into_inner();
let b = xml_writer.into_inner();
a.unsplit(b).shutdown().await?;
Ok(())
}
async fn socket_force_tls(
reader: &mut (impl AsyncBufRead + Unpin),
writer: &mut (impl AsyncWrite + Unpin),
reader_buf: &mut Vec<u8>,
) -> Result<()> {
use proto_xmpp::tls::*;
let xml_reader = &mut NsReader::from_reader(reader);
let xml_writer = &mut Writer::new(writer);
read_xml_header(xml_reader, reader_buf).await?;
let _ = ClientStreamStart::parse(xml_reader, reader_buf).await?;
let event = Event::Decl(BytesDecl::new("1.0", None, None));
xml_writer.write_event_async(event).await?;
let msg = ServerStreamStart {
from: "localhost".into(),
lang: "en".into(),
id: uuid::Uuid::new_v4().to_string(),
version: "1.0".into(),
};
msg.write_xml(xml_writer).await?;
let msg = Features {
start_tls: true,
mechanisms: false,
bind: false,
};
msg.write_xml(xml_writer).await?;
xml_writer.get_mut().flush().await?;
let StartTLS = StartTLS::parse(xml_reader, reader_buf).await?;
ProceedTLS.write_xml(xml_writer).await?;
xml_writer.get_mut().flush().await?;
Ok(())
}
async fn socket_auth(
xml_reader: &mut NsReader<(impl AsyncBufRead + Unpin)>,
xml_writer: &mut Writer<(impl AsyncWrite + Unpin)>,
reader_buf: &mut Vec<u8>,
storage: &mut Storage,
) -> Result<Authenticated> {
read_xml_header(xml_reader, reader_buf).await?;
let _ = ClientStreamStart::parse(xml_reader, reader_buf).await?;
xml_writer
.write_event_async(Event::Decl(BytesDecl::new("1.0", None, None)))
.await?;
ServerStreamStart {
from: "localhost".into(),
lang: "en".into(),
id: uuid::Uuid::new_v4().to_string(),
version: "1.0".into(),
}
.write_xml(xml_writer)
.await?;
Features {
start_tls: false,
mechanisms: true,
bind: false,
}
.write_xml(xml_writer)
.await?;
xml_writer.get_mut().flush().await?;
let auth: proto_xmpp::sasl::Auth = proto_xmpp::sasl::Auth::parse(xml_reader, reader_buf).await?;
proto_xmpp::sasl::Success.write_xml(xml_writer).await?;
match AuthBody::from_str(&auth.body) {
Ok(logopass) => {
let name = &logopass.login;
let stored_user = storage.retrieve_user_by_name(name).await?;
let stored_user = match stored_user {
Some(u) => u,
None => {
log::info!("User '{}' not found", name);
return Err(fail("no user found"));
}
};
// TODO return proper XML errors to the client
if stored_user.password.is_none() {
log::info!("Password not defined for user '{}'", name);
return Err(fail("password is not defined"));
}
if stored_user.password.as_deref() != Some(&logopass.password) {
log::info!("Incorrect password supplied for user '{}'", name);
return Err(fail("passwords do not match"));
}
Ok(Authenticated {
player_id: PlayerId::from(name.as_str())?,
xmpp_name: Name(name.to_string().into()),
xmpp_resource: Resource(name.to_string().into()),
xmpp_muc_name: Resource(name.to_string().into()),
})
},
Err(e) => return Err(e)
}
}
async fn socket_final(
xml_reader: &mut NsReader<(impl AsyncBufRead + Unpin)>,
xml_writer: &mut Writer<(impl AsyncWrite + Unpin)>,
reader_buf: &mut Vec<u8>,
authenticated: &Authenticated,
user_handle: &mut PlayerConnection,
rooms: &RoomRegistry,
) -> Result<()> {
read_xml_header(xml_reader, reader_buf).await?;
let _ = ClientStreamStart::parse(xml_reader, reader_buf).await?;
xml_writer
.write_event_async(Event::Decl(BytesDecl::new("1.0", None, None)))
.await?;
ServerStreamStart {
from: "localhost".into(),
lang: "en".into(),
id: uuid::Uuid::new_v4().to_string(),
version: "1.0".into(),
}
.write_xml(xml_writer)
.await?;
Features {
start_tls: false,
mechanisms: false,
bind: true,
}
.write_xml(xml_writer)
.await?;
xml_writer.get_mut().flush().await?;
let mut parser = proto::ClientPacket::parse();
let mut events = vec![];
reader_buf.clear();
let mut next_xml_event = Box::pin(xml_reader.read_resolved_event_into_async(reader_buf));
'outer: loop {
let should_recreate_xml_future = select! {
biased;
res = &mut next_xml_event => 's: {
let (ns, event) = res?;
if let Event::Text(ref e) = event {
if e.iter().all(|x| *x == 0xA) {
break 's true;
}
}
match parser.consume(ns, &event) {
Continuation::Final(res) => {
let res = res?;
let stop = handle_packet(&mut events, res, authenticated, user_handle, rooms).await?;
for i in &events {
xml_writer.write_event_async(i).await?;
}
events.clear();
xml_writer.get_mut().flush().await?;
if stop {
break 'outer;
}
parser = proto::ClientPacket::parse();
}
Continuation::Continue(p) => parser = p,
}
true
},
update = user_handle.receiver.recv() => {
if let Some(update) = update {
match update {
lavina_core::player::Updates::NewMessage { room_id, author_id, body } => {
Message::<()> {
to: Some(Jid {
name: Some(authenticated.xmpp_name.clone()),
server: Server("localhost".into()),
resource: Some(authenticated.xmpp_resource.clone()),
}),
from: Some(Jid {
name: Some(Name(room_id.into_inner().into())),
server: Server("rooms.localhost".into()),
resource: Some(Resource(author_id.into_inner().into())),
}),
id: None,
r#type: proto_xmpp::client::MessageType::Groupchat,
lang: None,
subject: None,
body: body.into(),
custom: vec![],
}
.serialize(&mut events);
}
_ => {},
}
for i in &events {
xml_writer.write_event_async(i).await?;
}
events.clear();
xml_writer.get_mut().flush().await?;
} else {
log::warn!("Player is terminated, must terminate the connection");
break;
}
false
}
};
if should_recreate_xml_future {
drop(next_xml_event);
next_xml_event = Box::pin(xml_reader.read_resolved_event_into_async(reader_buf));
}
}
Ok(())
}
async fn handle_packet(
output: &mut Vec<Event<'static>>,
packet: ClientPacket,
user: &Authenticated,
user_handle: &mut PlayerConnection,
rooms: &RoomRegistry,
) -> Result<bool> {
Ok(match packet {
proto::ClientPacket::Iq(iq) => {
handle_iq(output, iq, rooms).await;
false
}
proto::ClientPacket::Message(m) => {
if let Some(Jid {
name: Some(name),
server,
resource: _,
}) = m.to
{
if server.0.as_ref() == "rooms.localhost" && m.r#type == MessageType::Groupchat {
user_handle
.send_message(RoomId::from(name.0.clone())?, m.body.clone().into())
.await?;
Message::<()> {
to: Some(Jid {
name: Some(user.xmpp_name.clone()),
server: Server("localhost".into()),
resource: Some(user.xmpp_resource.clone()),
}),
from: Some(Jid {
name: Some(name),
server: Server("rooms.localhost".into()),
resource: Some(user.xmpp_muc_name.clone()),
}),
id: m.id,
r#type: proto_xmpp::client::MessageType::Groupchat,
lang: None,
subject: None,
body: m.body.clone(),
custom: vec![],
}
.serialize(output);
false
} else {
todo!()
}
} else {
todo!()
}
}
proto::ClientPacket::Presence(p) => {
let response = if p.to.is_none() {
Presence::<()> {
to: Some(Jid {
name: Some(user.xmpp_name.clone()),
server: Server("localhost".into()),
resource: Some(user.xmpp_resource.clone()),
}),
from: Some(Jid {
name: Some(user.xmpp_name.clone()),
server: Server("localhost".into()),
resource: Some(user.xmpp_resource.clone()),
}),
..Default::default()
}
} else if let Some(Jid {
name: Some(name),
server,
resource: Some(resource),
}) = p.to
{
let a = user_handle.join_room(RoomId::from(name.0.clone())?).await?;
Presence::<()> {
to: Some(Jid {
name: Some(user.xmpp_name.clone()),
server: Server("localhost".into()),
resource: Some(user.xmpp_resource.clone()),
}),
from: Some(Jid {
name: Some(name.clone()),
server: Server("rooms.localhost".into()),
resource: Some(user.xmpp_muc_name.clone()),
}),
..Default::default()
}
} else {
Presence::<()>::default()
};
response.serialize(output);
false
}
proto::ClientPacket::StreamEnd => {
ServerStreamEnd.serialize(output);
true
}
})
}
async fn handle_iq(output: &mut Vec<Event<'static>>, iq: Iq<IqClientBody>, rooms: &RoomRegistry) {
match iq.body {
proto::IqClientBody::Bind(b) => {
let req = Iq {
from: None,
id: iq.id,
to: None,
r#type: proto_xmpp::client::IqType::Result,
body: BindResponse(Jid {
name: Some(Name("darova".into())),
server: Server("localhost".into()),
resource: Some(Resource("kek".into())),
}),
};
req.serialize(output);
}
proto::IqClientBody::Session(_) => {
let req = Iq {
from: None,
id: iq.id,
to: None,
r#type: proto_xmpp::client::IqType::Result,
body: Session,
};
req.serialize(output);
}
proto::IqClientBody::Roster(_) => {
let req = Iq {
from: None,
id: iq.id,
to: None,
r#type: proto_xmpp::client::IqType::Result,
body: RosterQuery,
};
req.serialize(output);
}
proto::IqClientBody::DiscoInfo(info) => {
let response = disco_info(iq.to.as_deref(), &info);
let req = Iq {
from: iq.to,
id: iq.id,
to: None,
r#type: proto_xmpp::client::IqType::Result,
body: response,
};
req.serialize(output);
}
proto::IqClientBody::DiscoItem(item) => {
let response = disco_items(iq.to.as_deref(), &item, rooms).await;
let req = Iq {
from: iq.to,
id: iq.id,
to: None,
r#type: proto_xmpp::client::IqType::Result,
body: response,
};
req.serialize(output);
}
_ => {
let req = Iq {
from: None,
id: iq.id,
to: None,
r#type: proto_xmpp::client::IqType::Error,
body: (),
};
req.serialize(output);
}
}
}
fn disco_info(to: Option<&str>, req: &InfoQuery) -> InfoQuery {
let identity;
let feature;
match to {
Some("localhost") => {
identity = vec![Identity {
category: "server".into(),
name: None,
r#type: "im".into(),
}];
feature = vec![
Feature::new("http://jabber.org/protocol/disco#info"),
Feature::new("http://jabber.org/protocol/disco#items"),
Feature::new("iq"),
Feature::new("presence"),
]
}
Some("rooms.localhost") => {
identity = vec![Identity {
category: "conference".into(),
name: Some("Chat rooms".into()),
r#type: "text".into(),
}];
feature = vec![
Feature::new("http://jabber.org/protocol/disco#info"),
Feature::new("http://jabber.org/protocol/disco#items"),
Feature::new("http://jabber.org/protocol/muc"),
]
}
_ => {
identity = vec![];
feature = vec![];
}
};
InfoQuery {
node: None,
identity,
feature,
}
}
async fn disco_items(to: Option<&str>, req: &ItemQuery, rooms: &RoomRegistry) -> ItemQuery {
let item = match to {
Some("localhost") => {
vec![Item {
jid: Jid {
name: None,
server: Server("rooms.localhost".into()),
resource: None,
},
name: None,
node: None,
}]
}
Some("rooms.localhost") => {
let room_list = rooms.get_all_rooms().await;
room_list
.into_iter()
.map(|room_info| Item {
jid: Jid {
name: Some(Name(room_info.id.into_inner())),
server: Server("rooms.localhost".into()),
resource: None,
},
name: None,
node: None,
})
.collect()
}
_ => vec![],
};
ItemQuery { item }
}
async fn read_xml_header(
xml_reader: &mut NsReader<(impl AsyncBufRead + Unpin)>,
reader_buf: &mut Vec<u8>,
) -> Result<()> {
if let Event::Decl(bytes) = xml_reader.read_event_into_async(reader_buf).await? {
// this is <?xml ...> header
if let Some(encoding) = bytes.encoding() {
let encoding = encoding?;
if &*encoding == b"UTF-8" {
Ok(())
} else {
Err(anyhow!("Unsupported encoding: {encoding:?}"))
}
} else {
// Err(fail("No XML encoding provided"))
Ok(())
}
} else {
Err(anyhow!("Expected XML header"))
}
}

View File

@ -0,0 +1,92 @@
use anyhow::anyhow;
use derive_more::From;
use quick_xml::events::Event;
use quick_xml::name::{Namespace, ResolveResult};
use lavina_core::prelude::*;
use proto_xmpp::bind::BindRequest;
use proto_xmpp::client::{Iq, Message, Presence};
use proto_xmpp::disco::{InfoQuery, ItemQuery};
use proto_xmpp::roster::RosterQuery;
use proto_xmpp::session::Session;
use proto_xmpp::xml::*;
#[derive(PartialEq, Eq, Debug, From)]
pub enum IqClientBody {
Bind(BindRequest),
Session(Session),
Roster(RosterQuery),
DiscoInfo(InfoQuery),
DiscoItem(ItemQuery),
Unknown(Ignore),
}
impl FromXml for IqClientBody {
type P = impl Parser<Output = Result<Self>>;
fn parse() -> Self::P {
|(namespace, event): (ResolveResult<'static>, &'static Event<'static>)| -> Result<Self> {
let bytes = match event {
Event::Start(bytes) => bytes,
Event::Empty(bytes) => bytes,
_ => return Err(anyhow!("Unexpected XML event: {event:?}")),
};
let name = bytes.name();
match_parser!(name, namespace, event;
BindRequest,
Session,
RosterQuery,
InfoQuery,
ItemQuery,
{
delegate_parsing!(Ignore, namespace, event).into()
}
)
}
}
}
#[derive(PartialEq, Eq, Debug, From)]
pub enum ClientPacket {
Iq(Iq<IqClientBody>),
Message(Message<Ignore>),
Presence(Presence<Ignore>),
StreamEnd,
}
impl FromXml for ClientPacket {
type P = impl Parser<Output = Result<Self>>;
fn parse() -> Self::P {
|(namespace, event): (ResolveResult<'static>, &'static Event<'static>)| -> Result<Self> {
match event {
Event::Start(bytes) | Event::Empty(bytes) => {
let name = bytes.name();
match_parser!(name, namespace, event;
Iq::<IqClientBody>,
Presence::<Ignore>,
Message::<Ignore>,
{
Err(anyhow!(
"Unexpected XML event of name {:?} in namespace {:?}",
name,
namespace
))
}
)
}
Event::End(bytes) => {
let name = bytes.name();
if name.local_name().as_ref() == b"stream" {
return Ok(ClientPacket::StreamEnd);
} else {
return Err(anyhow!("Unexpected XML event: {event:?}"));
}
}
_ => {
return Err(anyhow!("Unexpected XML event: {event:?}"));
}
}
}
}
}

View File

@ -0,0 +1,15 @@
[package]
name = "proto-irc"
edition = "2021"
version.workspace = true
publish = false
[dependencies]
nom.workspace = true
nonempty.workspace = true
tokio.workspace = true
futures-util.workspace = true
anyhow.workspace = true
[dev-dependencies]
assert_matches.workspace = true

View File

@ -0,0 +1,330 @@
use super::*;
use anyhow::{anyhow, Result};
use nom::combinator::{all_consuming, opt};
/// Client-to-server command.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ClientMessage {
/// CAP. Capability-related commands.
Capability {
subcommand: CapabilitySubcommand,
},
/// PING <token>
Ping {
token: Str,
},
/// PONG <token>
Pong {
token: Str,
},
/// NICK <nickname>
Nick {
nickname: Str,
},
/// PASS <password>
Pass {
password: Str,
},
/// USER <username> 0 * :<realname>
User {
username: Str,
realname: Str,
},
/// JOIN <chan>
Join(Chan),
/// MODE <target>
Mode {
target: Recipient,
},
/// WHO <target>
Who {
target: Recipient, // aka mask
},
/// TOPIC <chan> :<topic>
Topic {
chan: Chan,
topic: Str,
},
Part {
chan: Chan,
message: Str,
},
/// PRIVMSG <target> :<msg>
PrivateMessage {
recipient: Recipient,
body: Str,
},
/// QUIT :<reason>
Quit {
reason: Str,
},
}
pub fn client_message(input: &str) -> Result<ClientMessage> {
let res = all_consuming(alt((
client_message_capability,
client_message_ping,
client_message_pong,
client_message_nick,
client_message_pass,
client_message_user,
client_message_join,
client_message_mode,
client_message_who,
client_message_topic,
client_message_part,
client_message_privmsg,
client_message_quit,
)))(input);
match res {
Ok((_, e)) => Ok(e),
Err(e) => Err(anyhow!("Parsing failed: {e}")),
}
}
fn client_message_capability(input: &str) -> IResult<&str, ClientMessage> {
let (input, _) = tag("CAP ")(input)?;
let (input, subcommand) = capability_subcommand(input)?;
Ok((input, ClientMessage::Capability { subcommand }))
}
fn client_message_ping(input: &str) -> IResult<&str, ClientMessage> {
let (input, _) = tag("PING ")(input)?;
let (input, token) = token(input)?;
Ok((input, ClientMessage::Ping { token: token.into() }))
}
fn client_message_pong(input: &str) -> IResult<&str, ClientMessage> {
let (input, _) = tag("PONG ")(input)?;
let (input, token) = token(input)?;
Ok((input, ClientMessage::Pong { token: token.into() }))
}
fn client_message_nick(input: &str) -> IResult<&str, ClientMessage> {
let (input, _) = tag("NICK ")(input)?;
let (input, nickname) = receiver(input)?;
Ok((
input,
ClientMessage::Nick {
nickname: nickname.into(),
},
))
}
fn client_message_pass(input: &str) -> IResult<&str, ClientMessage> {
let (input, _) = tag("PASS ")(input)?;
let (input, r) = opt(tag(":"))(input)?;
let (input, password) = match r {
Some(_) => token(input)?,
None => receiver(input)?,
};
Ok((
input,
ClientMessage::Pass {
password: password.into(),
},
))
}
fn client_message_user(input: &str) -> IResult<&str, ClientMessage> {
let (input, _) = tag("USER ")(input)?;
let (input, username) = receiver(input)?;
let (input, _) = tag(" ")(input)?;
let (input, _) = receiver(input)?;
let (input, _) = tag(" ")(input)?;
let (input, _) = receiver(input)?;
let (input, _) = tag(" ")(input)?;
let (input, r) = opt(tag(":"))(input)?;
let (input, realname) = match r {
Some(_) => token(input)?,
None => receiver(input)?,
};
Ok((
input,
ClientMessage::User {
username: username.into(),
realname: realname.into(),
},
))
}
fn client_message_join(input: &str) -> IResult<&str, ClientMessage> {
let (input, _) = tag("JOIN ")(input)?;
let (input, chan) = chan(input)?;
Ok((input, ClientMessage::Join(chan)))
}
fn client_message_mode(input: &str) -> IResult<&str, ClientMessage> {
let (input, _) = tag("MODE ")(input)?;
let (input, target) = recipient(input)?;
Ok((input, ClientMessage::Mode { target }))
}
fn client_message_who(input: &str) -> IResult<&str, ClientMessage> {
let (input, _) = tag("WHO ")(input)?;
let (input, target) = recipient(input)?;
Ok((input, ClientMessage::Who { target }))
}
fn client_message_topic(input: &str) -> IResult<&str, ClientMessage> {
let (input, _) = tag("TOPIC ")(input)?;
let (input, chan) = chan(input)?;
let (input, _) = tag(" ")(input)?;
let (input, r) = opt(tag(":"))(input)?;
let (input, topic) = match r {
Some(_) => token(input)?,
None => receiver(input)?,
};
let topic = topic.into();
Ok((input, ClientMessage::Topic { chan, topic }))
}
fn client_message_part(input: &str) -> IResult<&str, ClientMessage> {
let (input, _) = tag("PART ")(input)?;
let (input, chan) = chan(input)?;
let (input, _) = tag(" ")(input)?;
let (input, r) = opt(tag(":"))(input)?;
let (input, message) = match r {
Some(_) => token(input)?,
None => receiver(input)?,
};
let message = message.into();
Ok((input, ClientMessage::Part { chan, message }))
}
fn client_message_privmsg(input: &str) -> IResult<&str, ClientMessage> {
let (input, _) = tag("PRIVMSG ")(input)?;
let (input, recipient) = recipient(input)?;
let (input, _) = tag(" ")(input)?;
let (input, r) = opt(tag(":"))(input)?;
let (input, body) = match r {
Some(_) => token(input)?,
None => receiver(input)?,
};
let body = body.into();
Ok((input, ClientMessage::PrivateMessage { recipient, body }))
}
fn client_message_quit(input: &str) -> IResult<&str, ClientMessage> {
let (input, _) = tag("QUIT :")(input)?;
let (input, reason) = token(input)?;
Ok((input, ClientMessage::Quit { reason: reason.into() }))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CapabilitySubcommand {
/// CAP LS {code}
List { code: [u8; 3] },
/// CAP END
End,
}
fn capability_subcommand(input: &str) -> IResult<&str, CapabilitySubcommand> {
alt((capability_subcommand_ls, capability_subcommand_end))(input)
}
fn capability_subcommand_ls(input: &str) -> IResult<&str, CapabilitySubcommand> {
let (input, _) = tag("LS ")(input)?;
let (input, code) = take(3usize)(input)?;
Ok((
input,
CapabilitySubcommand::List {
code: code.as_bytes().try_into().unwrap(),
},
))
}
fn capability_subcommand_end(input: &str) -> IResult<&str, CapabilitySubcommand> {
let (input, _) = tag("END")(input)?;
Ok((input, CapabilitySubcommand::End))
}
#[cfg(test)]
mod test {
use assert_matches::*;
use super::*;
#[test]
fn test_client_message_cap_ls() {
let input = "CAP LS 302";
let expected = ClientMessage::Capability {
subcommand: CapabilitySubcommand::List { code: *b"302" },
};
let result = client_message(input);
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
}
#[test]
fn test_client_message_cap_end() {
let input = "CAP END";
let expected = ClientMessage::Capability {
subcommand: CapabilitySubcommand::End,
};
let result = client_message(input);
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
}
#[test]
fn test_client_message_ping() {
let input = "PING 1337";
let expected = ClientMessage::Ping { token: "1337".into() };
let result = client_message(input);
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
}
#[test]
fn test_client_message_pong() {
let input = "PONG 1337";
let expected = ClientMessage::Pong { token: "1337".into() };
let result = client_message(input);
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
}
#[test]
fn test_client_message_nick() {
let input = "NICK SomeNick";
let expected = ClientMessage::Nick {
nickname: "SomeNick".into(),
};
let result = client_message(input);
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
}
#[test]
fn test_client_message_user() {
let input = "USER SomeNick 8 * :Real Name";
let expected = ClientMessage::User {
username: "SomeNick".into(),
realname: "Real Name".into(),
};
let result = client_message(input);
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
}
#[test]
fn test_client_message_part() {
let input = "PART #chan :Pokasiki !!!";
let expected = ClientMessage::Part {
chan: Chan::Global("chan".into()),
message: "Pokasiki !!!".into(),
};
let result = client_message(input);
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
}
}

View File

@ -1,8 +1,12 @@
//! Client-to-Server IRC protocol.
pub mod client;
mod prelude;
pub mod server;
#[cfg(test)]
mod testkit;
pub mod user;
use std::io::Result;
use crate::prelude::Str;
use nom::{
branch::alt,
@ -11,57 +15,55 @@ use nom::{
};
use tokio::io::{AsyncWrite, AsyncWriteExt};
type ByteVec = Vec<u8>;
/// Single message tag value.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Tag {
key: ByteVec,
key: Str,
value: Option<u8>,
}
fn receiver(input: &[u8]) -> IResult<&[u8], &[u8]> {
take_while(|i| i != b'\n' && i != b'\r' && i != b' ')(input)
fn receiver(input: &str) -> IResult<&str, &str> {
take_while(|i| i != '\n' && i != '\r' && i != ' ')(input)
}
fn token(input: &[u8]) -> IResult<&[u8], &[u8]> {
take_while(|i| i != b'\n' && i != b'\r')(input)
fn token(input: &str) -> IResult<&str, &str> {
take_while(|i| i != '\n' && i != '\r')(input)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Chan {
/// #<name> — network-global channel, available from any server in the network.
Global(ByteVec),
Global(Str),
/// &<name> — server-local channel, available only to connections to the same server. Rarely used in practice.
Local(ByteVec),
Local(Str),
}
impl Chan {
pub async fn write_async(&self, writer: &mut (impl AsyncWrite + Unpin)) -> Result<()> {
pub async fn write_async(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
match self {
Chan::Global(name) => {
writer.write_all(b"#").await?;
writer.write_all(&name).await?;
writer.write_all(name.as_bytes()).await?;
}
Chan::Local(name) => {
writer.write_all(b"&").await?;
writer.write_all(&name).await?;
writer.write_all(name.as_bytes()).await?;
}
}
Ok(())
}
}
fn chan(input: &[u8]) -> IResult<&[u8], Chan> {
fn chan_global(input: &[u8]) -> IResult<&[u8], Chan> {
fn chan(input: &str) -> IResult<&str, Chan> {
fn chan_global(input: &str) -> IResult<&str, Chan> {
let (input, _) = tag("#")(input)?;
let (input, name) = receiver(input)?;
Ok((input, Chan::Global(name.to_vec())))
Ok((input, Chan::Global(name.into())))
}
fn chan_local(input: &[u8]) -> IResult<&[u8], Chan> {
fn chan_local(input: &str) -> IResult<&str, Chan> {
let (input, _) = tag("&")(input)?;
let (input, name) = receiver(input)?;
Ok((input, Chan::Local(name.to_vec())))
Ok((input, Chan::Local(name.into())))
}
alt((chan_global, chan_local))(input)
@ -69,28 +71,28 @@ fn chan(input: &[u8]) -> IResult<&[u8], Chan> {
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Recipient {
Nick(ByteVec),
Nick(Str),
Chan(Chan),
}
impl Recipient {
pub async fn write_async(&self, writer: &mut (impl AsyncWrite + Unpin)) -> Result<()> {
pub async fn write_async(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
match self {
Recipient::Nick(nick) => writer.write_all(&nick).await?,
Recipient::Nick(nick) => writer.write_all(nick.as_bytes()).await?,
Recipient::Chan(chan) => chan.write_async(writer).await?,
}
Ok(())
}
}
fn recipient(input: &[u8]) -> IResult<&[u8], Recipient> {
fn recipient_chan(input: &[u8]) -> IResult<&[u8], Recipient> {
fn recipient(input: &str) -> IResult<&str, Recipient> {
fn recipient_chan(input: &str) -> IResult<&str, Recipient> {
let (input, chan) = chan(input)?;
Ok((input, Recipient::Chan(chan)))
}
fn recipient_nick(input: &[u8]) -> IResult<&[u8], Recipient> {
fn recipient_nick(input: &str) -> IResult<&str, Recipient> {
let (input, nick) = receiver(input)?;
Ok((input, Recipient::Nick(nick.to_vec())))
Ok((input, Recipient::Nick(nick.into())))
}
alt((recipient_chan, recipient_nick))(input)
@ -101,12 +103,12 @@ mod test {
use assert_matches::*;
use super::*;
use crate::util::testkit::*;
use crate::testkit::*;
#[test]
fn test_chan_global() {
let input = b"#testchan";
let expected = Chan::Global(b"testchan".to_vec());
let input = "#testchan";
let expected = Chan::Global("testchan".into());
let result = chan(input);
assert_matches!(result, Ok((_, result)) => assert_eq!(expected, result));
@ -116,13 +118,13 @@ mod test {
.unwrap()
.unwrap();
assert_eq!(bytes.as_slice(), input);
assert_eq!(bytes.as_slice(), input.as_bytes());
}
#[test]
fn test_chan_local() {
let input = b"&localchan";
let expected = Chan::Local(b"localchan".to_vec());
let input = "&localchan";
let expected = Chan::Local("localchan".into());
let result = chan(input);
assert_matches!(result, Ok((_, result)) => assert_eq!(expected, result));
@ -132,13 +134,13 @@ mod test {
.unwrap()
.unwrap();
assert_eq!(bytes.as_slice(), input);
assert_eq!(bytes.as_slice(), input.as_bytes());
}
#[test]
fn test_recipient_user() {
let input = b"User";
let expected = Recipient::Nick(b"User".to_vec());
let input = "User";
let expected = Recipient::Nick("User".into());
let result = recipient(input);
assert_matches!(result, Ok((_, result)) => assert_eq!(expected, result));
@ -148,6 +150,6 @@ mod test {
.unwrap()
.unwrap();
assert_eq!(bytes.as_slice(), input);
assert_eq!(bytes.as_slice(), input.as_bytes());
}
}

View File

@ -0,0 +1,3 @@
use std::sync::Arc;
pub type Str = Arc<str>;

View File

@ -0,0 +1,438 @@
use nonempty::NonEmpty;
use tokio::io::AsyncWrite;
use tokio::io::AsyncWriteExt;
use super::*;
use crate::user::PrefixedNick;
/// Server-to-client message.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ServerMessage {
/// Optional tags section, prefixed with `@`
pub tags: Vec<Tag>,
/// Optional server name, prefixed with `:`.
pub sender: Option<Str>,
pub body: ServerMessageBody,
}
impl ServerMessage {
pub async fn write_async(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
match &self.sender {
Some(ref sender) => {
writer.write_all(b":").await?;
writer.write_all(sender.as_bytes()).await?;
writer.write_all(b" ").await?;
}
None => {}
}
self.body.write_async(writer).await?;
writer.write_all(b"\r\n").await?;
Ok(())
}
}
pub fn server_message(input: &str) -> IResult<&str, ServerMessage> {
let (input, command) = server_message_body(input)?;
let (input, _) = tag("\r\n")(input)?;
let message = ServerMessage {
tags: vec![],
sender: None,
body: command,
};
Ok((input, message))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ServerMessageBody {
Notice {
first_target: Str,
rest_targets: Vec<Str>,
text: Str,
},
Ping {
token: Str,
},
Pong {
from: Str,
token: Str,
},
PrivateMessage {
target: Recipient,
body: Str,
},
Join(Chan),
Part(Chan),
Error {
reason: Str,
},
N001Welcome {
client: Str,
text: Str,
},
N002YourHost {
client: Str,
text: Str,
},
N003Created {
client: Str,
text: Str,
},
N004MyInfo {
client: Str,
hostname: Str,
softname: Str,
// TODO user modes, channel modes, channel modes with a parameter
},
N005ISupport {
client: Str,
params: Str, // TODO make this a datatype
},
/// Reply to a client's [Mode](crate::protos::irc::client::ClientMessage::Mode) request.
N221UserModeIs {
client: Str,
modes: Str,
},
/// Final reply to a client's [Who](crate::protos::irc::client::ClientMessage::Who) request.
N315EndOfWho {
client: Str,
mask: Recipient,
/// Usually `b"End of WHO list"`
msg: Str,
},
N332Topic {
client: Str,
chat: Chan,
topic: Str,
},
/// A reply to a client's [Who](crate::protos::irc::client::ClientMessage::Who) request.
N352WhoReply {
client: Str,
// chan = *
username: Str,
/// User's hostname
host: Str,
/// Hostname of the server the user is connected to
server: Str,
nickname: Str,
/// Flags
flags: AwayStatus,
hops: u8,
realname: Str,
},
N353NamesReply {
client: Str,
chan: Chan,
members: NonEmpty<PrefixedNick>,
},
N366NamesReplyEnd {
client: Str,
chan: Chan,
},
N474BannedFromChan {
client: Str,
chan: Chan,
message: Str,
},
N502UsersDontMatch {
client: Str,
message: Str,
},
}
impl ServerMessageBody {
pub async fn write_async(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
match self {
ServerMessageBody::Notice {
first_target,
rest_targets,
text,
} => {
writer.write_all(b"NOTICE ").await?;
writer.write_all(first_target.as_bytes()).await?;
writer.write_all(b" :").await?;
writer.write_all(text.as_bytes()).await?;
}
ServerMessageBody::Ping { token } => {
writer.write_all(b"PING ").await?;
writer.write_all(token.as_bytes()).await?;
}
ServerMessageBody::Pong { from, token } => {
writer.write_all(b"PONG ").await?;
writer.write_all(from.as_bytes()).await?;
writer.write_all(b" :").await?;
writer.write_all(token.as_bytes()).await?;
}
ServerMessageBody::PrivateMessage { target, body } => {
writer.write_all(b"PRIVMSG ").await?;
target.write_async(writer).await?;
writer.write_all(b" :").await?;
writer.write_all(body.as_bytes()).await?;
}
ServerMessageBody::Join(chan) => {
writer.write_all(b"JOIN ").await?;
chan.write_async(writer).await?;
}
ServerMessageBody::Part(chan) => {
writer.write_all(b"PART ").await?;
chan.write_async(writer).await?;
}
ServerMessageBody::Error { reason } => {
writer.write_all(b"ERROR :").await?;
writer.write_all(reason.as_bytes()).await?;
}
ServerMessageBody::N001Welcome { client, text } => {
writer.write_all(b"001 ").await?;
writer.write_all(client.as_bytes()).await?;
writer.write_all(b" :").await?;
writer.write_all(text.as_bytes()).await?;
}
ServerMessageBody::N002YourHost { client, text } => {
writer.write_all(b"002 ").await?;
writer.write_all(client.as_bytes()).await?;
writer.write_all(b" :").await?;
writer.write_all(text.as_bytes()).await?;
}
ServerMessageBody::N003Created { client, text } => {
writer.write_all(b"003 ").await?;
writer.write_all(client.as_bytes()).await?;
writer.write_all(b" :").await?;
writer.write_all(text.as_bytes()).await?;
}
ServerMessageBody::N004MyInfo {
client,
hostname,
softname,
} => {
writer.write_all(b"004 ").await?;
writer.write_all(client.as_bytes()).await?;
writer.write_all(b" ").await?;
writer.write_all(hostname.as_bytes()).await?;
writer.write_all(b" ").await?;
writer.write_all(softname.as_bytes()).await?;
writer.write_all(b" r CFILPQbcefgijklmnopqrstvz").await?;
// TODO remove hardcoded modes
}
ServerMessageBody::N005ISupport { client, params } => {
writer.write_all(b"005 ").await?;
writer.write_all(client.as_bytes()).await?;
writer.write_all(b" ").await?;
writer.write_all(params.as_bytes()).await?;
writer.write_all(b" :are supported by this server").await?;
}
ServerMessageBody::N221UserModeIs { client, modes } => {
writer.write_all(b"221 ").await?;
writer.write_all(client.as_bytes()).await?;
writer.write_all(b" ").await?;
writer.write_all(modes.as_bytes()).await?;
}
ServerMessageBody::N315EndOfWho { client, mask, msg } => {
writer.write_all(b"315 ").await?;
writer.write_all(client.as_bytes()).await?;
writer.write_all(b" ").await?;
mask.write_async(writer).await?;
writer.write_all(b" :").await?;
writer.write_all(msg.as_bytes()).await?;
}
ServerMessageBody::N332Topic {
client,
chat,
topic,
} => {
writer.write_all(b"332 ").await?;
writer.write_all(client.as_bytes()).await?;
writer.write_all(b" ").await?;
chat.write_async(writer).await?;
writer.write_all(b" :").await?;
writer.write_all(topic.as_bytes()).await?;
}
ServerMessageBody::N352WhoReply {
client,
username,
host,
server,
flags,
nickname,
hops,
realname,
} => {
writer.write_all(b"352 ").await?;
writer.write_all(client.as_bytes()).await?;
writer.write_all(b" * ").await?;
writer.write_all(username.as_bytes()).await?;
writer.write_all(b" ").await?;
writer.write_all(host.as_bytes()).await?;
writer.write_all(b" ").await?;
writer.write_all(server.as_bytes()).await?;
writer.write_all(b" ").await?;
match flags {
AwayStatus::Here => writer.write_all(b"H").await?,
AwayStatus::Gone => writer.write_all(b"G").await?,
}
writer.write_all(b" ").await?;
writer.write_all(nickname.as_bytes()).await?;
writer.write_all(b" :").await?;
writer.write_all(hops.to_string().as_bytes()).await?;
writer.write_all(b" ").await?;
writer.write_all(realname.as_bytes()).await?;
}
ServerMessageBody::N353NamesReply {
client,
chan,
members,
} => {
writer.write_all(b"353 ").await?;
writer.write_all(client.as_bytes()).await?;
writer.write_all(b" = ").await?;
chan.write_async(writer).await?;
writer.write_all(b" :").await?;
for member in members {
writer
.write_all(member.prefix.to_string().as_bytes())
.await?;
writer.write_all(member.nick.as_bytes()).await?;
writer.write_all(b" ").await?;
}
}
ServerMessageBody::N366NamesReplyEnd { client, chan } => {
writer.write_all(b"366 ").await?;
writer.write_all(client.as_bytes()).await?;
writer.write_all(b" ").await?;
chan.write_async(writer).await?;
writer.write_all(b" :End of /NAMES list").await?;
}
ServerMessageBody::N474BannedFromChan {
client,
chan,
message,
} => {
writer.write_all(b"474 ").await?;
writer.write_all(client.as_bytes()).await?;
writer.write_all(b" ").await?;
chan.write_async(writer).await?;
writer.write_all(b" :").await?;
writer.write_all(message.as_bytes()).await?;
}
ServerMessageBody::N502UsersDontMatch { client, message } => {
writer.write_all(b"502 ").await?;
writer.write_all(client.as_bytes()).await?;
writer.write_all(b" ").await?;
writer.write_all(b" :").await?;
writer.write_all(message.as_bytes()).await?;
}
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AwayStatus {
Here,
Gone,
}
fn server_message_body(input: &str) -> IResult<&str, ServerMessageBody> {
alt((
server_message_body_notice,
server_message_body_ping,
server_message_body_pong,
))(input)
}
fn server_message_body_notice(input: &str) -> IResult<&str, ServerMessageBody> {
let (input, _) = tag("NOTICE ")(input)?;
let (input, first_target) = receiver(input)?;
let (input, _) = tag(" :")(input)?;
let (input, text) = token(input)?;
let first_target = first_target.into();
let text = text.into();
Ok((
input,
ServerMessageBody::Notice {
first_target,
rest_targets: vec![],
text,
},
))
}
fn server_message_body_ping(input: &str) -> IResult<&str, ServerMessageBody> {
let (input, _) = tag("PING ")(input)?;
let (input, token) = token(input)?;
Ok((
input,
ServerMessageBody::Ping {
token: token.into(),
},
))
}
fn server_message_body_pong(input: &str) -> IResult<&str, ServerMessageBody> {
let (input, _) = tag("PONG ")(input)?;
let (input, from) = receiver(input)?;
let (input, _) = tag(" :")(input)?;
let (input, token) = token(input)?;
Ok((
input,
ServerMessageBody::Pong {
from: from.into(),
token: token.into(),
},
))
}
#[cfg(test)]
mod test {
use assert_matches::*;
use super::*;
use crate::testkit::*;
#[test]
fn test_server_message_notice() {
let input = "NOTICE * :*** Looking up your hostname...\r\n";
let expected = ServerMessage {
tags: vec![],
sender: None,
body: ServerMessageBody::Notice {
first_target: "*".into(),
rest_targets: vec![],
text: "*** Looking up your hostname...".into(),
},
};
let result = server_message(input);
assert_matches!(result, Ok((_, result)) => assert_eq!(expected, result));
let mut bytes = vec![];
sync_future(expected.write_async(&mut bytes))
.unwrap()
.unwrap();
assert_eq!(bytes, input.as_bytes());
}
#[test]
fn test_server_message_pong() {
let input = "PONG server.example :LAG004911\r\n";
let expected = ServerMessage {
tags: vec![],
sender: None,
body: ServerMessageBody::Pong {
from: "server.example".into(),
token: "LAG004911".into(),
},
};
let result = server_message(input);
assert_matches!(result, Ok((_, result)) => assert_eq!(expected, result));
let mut bytes = vec![];
sync_future(expected.write_async(&mut bytes))
.unwrap()
.unwrap();
assert_eq!(bytes, input.as_bytes());
}
}

View File

@ -1,10 +1,10 @@
use std::future::Future;
use std::task::{Context, Poll};
use futures_util::task::noop_waker_ref;
use tokio::pin;
use crate::prelude::*;
pub fn sync_future<T>(future: impl Future<Output = T>) -> Result<T> {
pub fn sync_future<T>(future: impl Future<Output = T>) -> anyhow::Result<T> {
let waker = noop_waker_ref();
let mut context = Context::from_waker(waker);
pin!(future);

View File

@ -0,0 +1,30 @@
use super::*;
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Prefix {
Empty,
}
impl fmt::Display for Prefix {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Prefix::Empty => write!(f, ""),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PrefixedNick {
pub prefix: Prefix,
pub nick: Str,
}
impl PrefixedNick {
pub fn from_str(nick: Str) -> PrefixedNick {
PrefixedNick {
prefix: Prefix::Empty,
nick,
}
}
}

View File

@ -0,0 +1,16 @@
[package]
name = "proto-xmpp"
edition = "2021"
version.workspace = true
[dependencies]
quick-xml.workspace = true
lazy_static.workspace = true
regex.workspace = true
anyhow.workspace = true
tokio.workspace = true
derive_more.workspace = true
base64.workspace = true
[dev-dependencies]
assert_matches.workspace = true

View File

@ -0,0 +1,231 @@
use std::fmt::Display;
use anyhow::{anyhow, Result};
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
use quick_xml::name::{Namespace, ResolveResult};
use crate::prelude::*;
use crate::xml::*;
pub const XMLNS: &'static str = "urn:ietf:params:xml:ns:xmpp-bind";
// TODO remove `pub` in newtypes, introduce validation
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct Name(pub Str);
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct Server(pub Str);
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct Resource(pub Str);
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct Jid {
pub name: Option<Name>,
pub server: Server,
pub resource: Option<Resource>,
}
impl Display for Jid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(name) = &self.name {
write!(f, "{}@", &name.0)?;
}
write!(f, "{}", &self.server.0)?;
if let Some(resource) = &self.resource {
write!(f, "/{}", &resource.0)?;
}
Ok(())
}
}
impl Jid {
pub fn from_string(i: &str) -> Result<Jid> {
use regex::Regex;
use lazy_static::lazy_static;
lazy_static! {
static ref RE: Regex = Regex::new(r"^(([a-zA-Z]+)@)?([a-zA-Z.]+)(/([a-zA-Z\-]+))?$").unwrap();
}
let m = RE
.captures(i)
.ok_or(anyhow!("Incorrectly format jid: {i}"))?;
let name = m.get(2).map(|name| Name(name.as_str().into()));
let server = m.get(3).unwrap();
let server = Server(server.as_str().into());
let resource = m
.get(5)
.map(|resource| Resource(resource.as_str().into()));
Ok(Jid {
name,
server,
resource,
})
}
}
/// Request to bind to a resource.
///
/// Example:
/// ```xml
/// <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
/// <resource>mobile</resource>
/// </bind>
/// ```
///
#[derive(PartialEq, Eq, Debug)]
pub struct BindRequest(pub Resource);
impl FromXmlTag for BindRequest {
const NS: &'static str = XMLNS;
const NAME: &'static str = "bind";
}
impl FromXml for BindRequest {
type P = impl Parser<Output = Result<Self>>;
fn parse() -> Self::P {
|(namespace, event): (ResolveResult<'static>, &'static Event<'static>)| -> Result<Self> {
let mut resource: Option<Str> = None;
let Event::Start(bytes) = event else {
return Err(anyhow!("Unexpected XML event: {event:?}"));
};
if bytes.name().0 != BindRequest::NAME.as_bytes() {
return Err(anyhow!("Unexpected XML tag: {:?}", bytes.name()));
}
let ResolveResult::Bound(Namespace(ns)) = namespace else {
return Err(anyhow!("No namespace provided"));
};
if ns != XMLNS.as_bytes() {
return Err(anyhow!("Incorrect namespace"));
}
loop {
let (namespace, event) = yield;
match event {
Event::Start(bytes) if bytes.name().0 == b"resource" => {
let (namespace, event) = yield;
let Event::Text(text) = event else {
return Err(anyhow!("Unexpected XML event: {event:?}"));
};
resource = Some(std::str::from_utf8(&*text)?.into());
let (namespace, event) = yield;
let Event::End(bytes) = event else {
return Err(anyhow!("Unexpected XML event: {event:?}"));
};
if bytes.name().0 != b"resource" {
return Err(anyhow!("Unexpected XML tag: {:?}", bytes.name()));
}
}
Event::End(bytes) if bytes.name().0 == BindRequest::NAME.as_bytes() => {
break;
}
_ => return Err(anyhow!("Unexpected XML event: {event:?}")),
}
}
let Some(resource) = resource else {
return Err(anyhow!("No resource was provided"));
};
Ok(BindRequest(Resource(resource)))
}
}
}
pub struct BindResponse(pub Jid);
impl ToXml for BindResponse {
fn serialize(&self, events: &mut Vec<Event<'static>>) {
events.extend_from_slice(&[
Event::Start(BytesStart::new(
r#"bind xmlns="urn:ietf:params:xml:ns:xmpp-bind""#,
)),
Event::Start(BytesStart::new(r#"jid"#)),
Event::Text(BytesText::new(self.0.to_string().as_str()).into_owned()),
Event::End(BytesEnd::new("jid")),
Event::End(BytesEnd::new("bind")),
]);
}
}
#[cfg(test)]
mod tests {
use quick_xml::NsReader;
use super::*;
#[tokio::test]
async fn parse_message() {
let input =
r#"<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><resource>mobile</resource></bind>"#;
let mut reader = NsReader::from_reader(input.as_bytes());
let mut buf = vec![];
let (ns, event) = reader
.read_resolved_event_into_async(&mut buf)
.await
.unwrap();
let mut parser = BindRequest::parse().consume(ns, &event);
let result = loop {
match parser {
Continuation::Final(res) => break res,
Continuation::Continue(next) => {
let (ns, event) = reader
.read_resolved_event_into_async(&mut buf)
.await
.unwrap();
parser = next.consume(ns, &event);
}
}
}
.unwrap();
assert_eq!(result, BindRequest(Resource("mobile".into())),)
}
#[test]
fn jid_parse_full() {
let input = "chelik@server.example/kek";
let expected = Jid {
name: Some(Name("chelik".into())),
server: Server("server.example".into()),
resource: Some(Resource("kek".into())),
};
let res = Jid::from_string(input).unwrap();
assert_eq!(res, expected);
}
#[test]
fn jid_parse_user() {
let input = "chelik@server.example";
let expected = Jid {
name: Some(Name("chelik".into())),
server: Server("server.example".into()),
resource: None,
};
let res = Jid::from_string(input).unwrap();
assert_eq!(res, expected);
}
#[test]
fn jid_parse_server() {
let input = "server.example";
let expected = Jid {
name: None,
server: Server("server.example".into()),
resource: None,
};
let res = Jid::from_string(input).unwrap();
assert_eq!(res, expected);
}
#[test]
fn jid_parse_server_resource() {
let input = "server.example/kek";
let expected = Jid {
name: None,
server: Server("server.example".into()),
resource: Some(Resource("kek".into())),
};
let res = Jid::from_string(input).unwrap();
assert_eq!(res, expected);
}
}

View File

@ -0,0 +1,678 @@
use derive_more::From;
use quick_xml::events::attributes::Attribute;
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
use quick_xml::name::{QName, ResolveResult};
use anyhow::{anyhow as ffail, Result};
use crate::prelude::*;
use crate::xml::*;
use super::bind::Jid;
pub const XMLNS: &'static str = "jabber:client";
#[derive(PartialEq, Eq, Debug)]
pub struct Message<T> {
pub from: Option<Jid>,
pub id: Option<String>,
pub to: Option<Jid>,
// default is Normal
pub r#type: MessageType,
pub lang: Option<Str>,
pub subject: Option<Str>,
pub body: Str,
pub custom: Vec<T>,
}
impl<T: FromXml> FromXmlTag for Message<T> {
const NS: &'static str = XMLNS;
const NAME: &'static str = "message";
}
impl<T: FromXml> FromXml for Message<T> {
type P = impl Parser<Output = Result<Self>>;
fn parse() -> Self::P {
MessageParser(MessageParserInner::Init)
}
}
#[derive(From)]
struct MessageParser<T: FromXml>(MessageParserInner<T>);
#[derive(Default)]
enum MessageParserInner<T: FromXml> {
#[default]
Init,
Outer(MessageParserState<T>),
InSubject(MessageParserState<T>),
InBody(MessageParserState<T>),
InCustom(MessageParserState<T>, T::P),
}
#[derive(Default)]
struct MessageParserState<T> {
from: Option<Jid>,
id: Option<String>,
to: Option<Jid>,
r#type: MessageType,
lang: Option<Str>,
subject: Option<Str>,
body: Option<Str>,
custom: Vec<T>,
}
impl<T: FromXml> Parser for MessageParser<T> {
type Output = Result<Message<T>>;
fn consume<'a>(self: Self, namespace: ResolveResult, event: &Event<'a>) -> Continuation<Self, Self::Output> {
// TODO validate tag name and namespace at each stage
use MessageParserInner::*;
match self.0 {
Init => {
if let Event::Start(ref bytes) = event {
let mut state: MessageParserState<T> = MessageParserState {
from: None,
id: None,
to: None,
r#type: MessageType::Normal,
lang: None,
subject: None,
body: None,
custom: vec![],
};
for attr in bytes.attributes() {
let attr = fail_fast!(attr);
if attr.key.0 == b"from" {
let value = fail_fast!(std::str::from_utf8(&*attr.value));
let value = fail_fast!(Jid::from_string(value));
state.from = Some(value)
} else if attr.key.0 == b"id" {
let value = fail_fast!(std::str::from_utf8(&*attr.value));
state.id = Some(value.to_string())
} else if attr.key.0 == b"to" {
let value = fail_fast!(std::str::from_utf8(&*attr.value));
let value = fail_fast!(Jid::from_string(value));
state.to = Some(value)
} else if attr.key.0 == b"type" {
let value = fail_fast!(MessageType::from_str(&*attr.value));
state.r#type = value;
}
}
Continuation::Continue(Outer(state).into())
} else {
Continuation::Final(Err(ffail!("Expected start")))
}
}
Outer(mut state) => match event {
Event::Start(ref bytes) => {
if bytes.name().0 == b"subject" {
Continuation::Continue(InSubject(state).into())
} else if bytes.name().0 == b"body" {
Continuation::Continue(InBody(state).into())
} else {
let parser = T::parse();
match parser.consume(namespace, event) {
Continuation::Final(Ok(e)) => {
state.custom.push(e);
Continuation::Continue(Outer(state).into())
}
Continuation::Final(Err(e)) => Continuation::Final(Err(e)),
Continuation::Continue(p) => Continuation::Continue(InCustom(state, p).into()),
}
}
}
Event::End(_) => {
if let Some(body) = state.body {
Continuation::Final(Ok(Message {
from: state.from,
id: state.id,
to: state.to,
r#type: state.r#type,
lang: state.lang,
subject: state.subject,
body,
custom: state.custom,
}))
} else {
Continuation::Final(Err(ffail!("Body not found")))
}
}
Event::Empty(_) => {
let parser = T::parse();
match parser.consume(namespace, event) {
Continuation::Final(Ok(e)) => {
state.custom.push(e);
Continuation::Continue(Outer(state).into())
}
Continuation::Final(Err(e)) => Continuation::Final(Err(e)),
Continuation::Continue(p) => Continuation::Continue(InCustom(state, p).into()),
}
}
_ => Continuation::Final(Err(ffail!("Unexpected XML event: {event:?}"))),
},
InSubject(mut state) => match event {
Event::Text(ref bytes) => {
let subject = fail_fast!(std::str::from_utf8(&*bytes));
state.subject = Some(subject.into());
Continuation::Continue(InSubject(state).into())
}
Event::End(_) => Continuation::Continue(Outer(state).into()),
_ => Continuation::Final(Err(ffail!("Unexpected XML event: {event:?}"))),
},
InBody(mut state) => match event {
Event::Text(ref bytes) => match std::str::from_utf8(&*bytes) {
Ok(subject) => {
state.body = Some(subject.into());
Continuation::Continue(InBody(state).into())
}
Err(err) => Continuation::Final(Err(err.into())),
},
Event::End(_) => Continuation::Continue(Outer(state).into()),
_ => Continuation::Final(Err(ffail!("Unexpected XML event: {event:?}"))),
},
InCustom(mut state, custom) => match custom.consume(namespace, event) {
Continuation::Final(Ok(e)) => {
state.custom.push(e);
Continuation::Continue(Outer(state).into())
}
Continuation::Final(Err(e)) => Continuation::Final(Err(e)),
Continuation::Continue(c) => Continuation::Continue(InCustom(state, c).into()),
},
}
}
}
impl<T: ToXml> ToXml for Message<T> {
fn serialize(&self, events: &mut Vec<Event<'static>>) {
let mut bytes = BytesStart::new(format!(r#"message xmlns="{}""#, XMLNS));
if let Some(from) = &self.from {
bytes.push_attribute(Attribute {
key: QName(b"from"),
value: from.to_string().into_bytes().into(),
});
}
if let Some(to) = &self.to {
bytes.push_attribute(Attribute {
key: QName(b"to"),
value: to.to_string().into_bytes().into(),
});
}
if let Some(id) = &self.id {
bytes.push_attribute(Attribute {
key: QName(b"id"),
value: id.clone().into_bytes().into(),
});
}
bytes.push_attribute(Attribute {
key: QName(b"type"),
value: self.r#type.as_str().as_bytes().into(),
});
events.push(Event::Start(bytes));
events.push(Event::Start(BytesStart::new("body")));
events.push(Event::Text(BytesText::new(&self.body).into_owned()));
events.push(Event::End(BytesEnd::new("body")));
events.push(Event::End(BytesEnd::new("message")));
}
}
#[derive(PartialEq, Eq, Debug)]
pub enum MessageType {
Chat,
Error,
Groupchat,
Headline,
Normal,
}
impl Default for MessageType {
fn default() -> Self {
MessageType::Normal
}
}
impl MessageType {
pub fn from_str(s: &[u8]) -> Result<MessageType> {
use MessageType::*;
let s = std::str::from_utf8(s)?;
match s {
"chat" => Ok(Chat),
"error" => Ok(Error),
"groupchat" => Ok(Groupchat),
"headline" => Ok(Headline),
"normal" => Ok(Normal),
t => Err(ffail!("Unknown message type: {t}")),
}
}
pub fn as_str(&self) -> &'static str {
match self {
MessageType::Chat => "chat",
MessageType::Error => "error",
MessageType::Groupchat => "groupchat",
MessageType::Headline => "headline",
MessageType::Normal => "normal",
}
}
}
#[derive(PartialEq, Eq, Debug)]
pub struct Iq<T> {
pub from: Option<String>,
pub id: String,
pub to: Option<String>,
pub r#type: IqType,
pub body: T,
}
impl<T: FromXml> FromXmlTag for Iq<T> {
const NAME: &'static str = "iq";
const NS: &'static str = XMLNS;
}
impl<T: FromXml> FromXml for Iq<T> {
type P = IqParser<T>;
fn parse() -> Self::P {
IqParser(IqParserInner::Init)
}
}
pub struct IqParser<T: FromXml>(IqParserInner<T>);
enum IqParserInner<T: FromXml> {
Init,
ParsingBody(IqParserState<T>, T::P),
Final(IqParserState<T>),
}
struct IqParserState<T> {
pub from: Option<String>,
pub id: Option<String>,
pub to: Option<String>,
pub r#type: Option<IqType>,
pub body: Option<T>,
}
impl<T: FromXml> Parser for IqParser<T> {
type Output = Result<Iq<T>>;
fn consume<'a>(self: Self, namespace: ResolveResult, event: &Event<'a>) -> Continuation<Self, Self::Output> {
match self.0 {
IqParserInner::Init => {
if let Event::Start(ref bytes) = event {
let mut state: IqParserState<T> = IqParserState {
from: None,
id: None,
to: None,
r#type: None,
body: None,
};
for attr in bytes.attributes() {
let attr = fail_fast!(attr);
if attr.key.0 == b"from" {
let value = fail_fast!(std::str::from_utf8(&*attr.value));
state.from = Some(value.to_string())
} else if attr.key.0 == b"id" {
let value = fail_fast!(std::str::from_utf8(&*attr.value));
state.id = Some(value.to_string())
} else if attr.key.0 == b"to" {
let value = fail_fast!(std::str::from_utf8(&*attr.value));
state.to = Some(value.to_string())
} else if attr.key.0 == b"type" {
let value = fail_fast!(IqType::from_str(&*attr.value));
state.r#type = Some(value);
}
}
Continuation::Continue(IqParser(IqParserInner::ParsingBody(state, T::parse())))
} else {
Continuation::Final(Err(ffail!("Expected start")))
}
}
IqParserInner::ParsingBody(mut state, parser) => match parser.consume(namespace, event) {
Continuation::Final(f) => {
let body = fail_fast!(f);
state.body = Some(body);
Continuation::Continue(IqParser(IqParserInner::Final(state)))
}
Continuation::Continue(parser) => {
Continuation::Continue(IqParser(IqParserInner::ParsingBody(state, parser)))
}
},
IqParserInner::Final(state) => {
if let Event::End(ref bytes) = event {
let id = fail_fast!(state.id.ok_or_else(|| ffail!("No id provided")));
let r#type = fail_fast!(state.r#type.ok_or_else(|| ffail!("No type provided")));
let body = fail_fast!(state.body.ok_or_else(|| ffail!("No body provided")));
Continuation::Final(Ok(Iq {
from: state.from,
id,
to: state.to,
r#type,
body,
}))
} else {
Continuation::Final(Err(ffail!("Unexpected event: {event:?}")))
}
}
}
}
}
#[derive(PartialEq, Eq, Debug)]
pub enum IqType {
Error,
Get,
Result,
Set,
}
impl IqType {
pub fn from_str(s: &[u8]) -> Result<IqType> {
use IqType::*;
let s = std::str::from_utf8(s)?;
match s {
"error" => Ok(Error),
"get" => Ok(Get),
"result" => Ok(Result),
"set" => Ok(Set),
t => Err(ffail!("Unknown iq type: {t}")),
}
}
pub fn as_str(&self) -> &'static str {
match self {
IqType::Error => "error",
IqType::Get => "get",
IqType::Result => "result",
IqType::Set => "set",
}
}
}
impl<T: ToXml> ToXml for Iq<T> {
fn serialize(&self, events: &mut Vec<Event<'static>>) {
let start = format!(r#"iq xmlns="{}""#, XMLNS);
let mut start = BytesStart::new(start);
let mut attrs = vec![];
if let Some(ref from) = self.from {
attrs.push(Attribute {
key: QName(b"from"),
value: from.as_bytes().into(),
});
};
if let Some(ref to) = self.to {
attrs.push(Attribute {
key: QName(b"to"),
value: to.as_bytes().into(),
});
}
attrs.push(Attribute {
key: QName(b"id"),
value: self.id.as_bytes().into(),
});
attrs.push(Attribute {
key: QName(b"type"),
value: self.r#type.as_str().as_bytes().into(),
});
start.extend_attributes(attrs.into_iter());
events.push(Event::Start(start));
self.body.serialize(events);
events.push(Event::End(BytesEnd::new("iq")));
}
}
#[derive(PartialEq, Eq, Debug)]
pub struct Presence<T> {
pub to: Option<Jid>,
pub from: Option<Jid>,
pub priority: Option<PresencePriority>,
pub show: Option<PresenceShow>,
pub status: Vec<String>,
pub custom: Vec<T>,
pub r#type: Option<String>,
}
impl<T> Default for Presence<T> {
fn default() -> Self {
Self {
to: Default::default(),
from: Default::default(),
priority: Default::default(),
show: Default::default(),
status: Default::default(),
custom: Default::default(),
r#type: None,
}
}
}
#[derive(PartialEq, Eq, Debug)]
pub enum PresenceShow {
Away,
Chat,
Dnd,
Xa,
}
/// Presence priority is an integer number in range [-128; 127].
///
/// Presence priority < 0 means that the bound resource will never be chosen unless it was asked for specifically.
#[derive(PartialEq, Eq, Debug)]
pub struct PresencePriority(pub i8);
impl PresenceShow {
pub fn from_str(s: &[u8]) -> Result<Self> {
use PresenceShow::*;
let s = std::str::from_utf8(s)?;
match s {
"away" => Ok(Away),
"chat" => Ok(Chat),
"dnd" => Ok(Dnd),
"xa" => Ok(Xa),
t => Err(ffail!("Unknown presence show type: {t}")),
}
}
pub fn as_str(&self) -> &'static str {
use PresenceShow::*;
match self {
Away => "away",
Chat => "chat",
Dnd => "dnd",
Xa => "xa",
}
}
}
impl<T: FromXml> FromXml for Presence<T> {
type P = impl Parser<Output = Result<Presence<T>>>;
fn parse() -> Self::P {
|(namespace, event): (ResolveResult<'static>, &'static Event<'static>)| -> Result<Self> {
let (bytes, end) = match event {
Event::Start(bytes) => (bytes, false),
Event::Empty(bytes) => (bytes, true),
_ => return Err(ffail!("Unexpected XML event: {event:?}")),
};
let mut p = Presence::<T>::default();
for attr in bytes.attributes() {
let attr = attr?;
match attr.key.0 {
b"to" => {
let s = std::str::from_utf8(&attr.value)?;
p.to = Some(Jid::from_string(s)?);
}
b"from" => {
let s = std::str::from_utf8(&attr.value)?;
p.to = Some(Jid::from_string(s)?);
}
b"type" => {
let s = std::str::from_utf8(&attr.value)?;
p.r#type = Some(s.into());
}
_ => {}
}
}
if end {
return Ok(p);
}
loop {
let (namespace, event) = yield;
match event {
Event::Start(bytes) => match bytes.name().0 {
b"show" => {
let (_, event) = yield;
let Event::Text(bytes) = event else {
return Err(ffail!("Unexpected XML event: {event:?}"));
};
let i = PresenceShow::from_str(bytes)?;
p.show = Some(i);
let (_, event) = yield;
let Event::End(_) = event else {
return Err(ffail!("Unexpected XML event: {event:?}"));
};
}
b"status" => {
let (_, event) = yield;
let Event::Text(bytes) = event else {
return Err(ffail!("Unexpected XML event: {event:?}"));
};
let s = std::str::from_utf8(bytes)?;
p.status.push(s.to_string());
let (_, event) = yield;
let Event::End(_) = event else {
return Err(ffail!("Unexpected XML event: {event:?}"));
};
}
b"priority" => {
let (_, event) = yield;
let Event::Text(bytes) = event else {
return Err(ffail!("Unexpected XML event: {event:?}"));
};
let s = std::str::from_utf8(bytes)?;
let i = s.parse()?;
p.priority = Some(PresencePriority(i));
let (_, event) = yield;
let Event::End(_) = event else {
return Err(ffail!("Unexpected XML event: {event:?}"));
};
}
_ => {
let res = delegate_parsing!(T, namespace, event);
p.custom.push(res?);
}
},
Event::Empty(_) => {
let res = delegate_parsing!(T, namespace, event);
p.custom.push(res?);
}
Event::End(_) => return Ok(p),
_ => return Err(ffail!("Unexpected XML event: {event:?}")),
}
}
}
}
}
impl<T: FromXml> FromXmlTag for Presence<T> {
const NAME: &'static str = "presence";
const NS: &'static str = XMLNS;
}
impl<T: ToXml> ToXml for Presence<T> {
fn serialize(&self, events: &mut Vec<Event<'static>>) {
let mut start = BytesStart::new("presence");
if let Some(ref to) = self.to {
start.extend_attributes([Attribute {
key: QName(b"to"),
value: to.to_string().as_bytes().into(),
}]);
}
if let Some(ref from) = self.from {
start.extend_attributes([Attribute {
key: QName(b"from"),
value: from.to_string().as_bytes().into(),
}]);
}
events.push(Event::Start(start));
if let Some(ref priority) = self.priority {
let s = priority.0.to_string();
events.extend_from_slice(&[
Event::Start(BytesStart::new(r#"priority"#)),
Event::Text(BytesText::new(s.as_str()).into_owned()),
Event::End(BytesEnd::new("priority")),
]);
}
events.push(Event::End(BytesEnd::new("presence")));
}
}
#[cfg(test)]
mod tests {
use crate::bind::{BindRequest, Name, Resource, Server};
use super::*;
#[tokio::test]
async fn parse_message() {
let input = r#"<message id="aacea" type="chat" to="nikita@vlnv.dev"><subject>daa</subject><body>bbb</body><unknown-stuff></unknown-stuff></message>"#;
let result: Message<Ignore> = crate::xml::parse(input).unwrap();
assert_eq!(
result,
Message::<Ignore> {
from: None,
id: Some("aacea".to_string()),
to: Some(Jid {
name: Some(Name("nikita".into())),
server: Server("vlnv.dev".into()),
resource: None
}),
r#type: MessageType::Chat,
lang: None,
subject: Some("daa".into()),
body: "bbb".into(),
custom: vec![Ignore],
}
)
}
#[tokio::test]
async fn parse_message_empty_custom() {
let input = r#"<message id="aacea" type="chat" to="nikita@vlnv.dev"><subject>daa</subject><body>bbb</body><unknown-stuff/></message>"#;
let result: Message<Ignore> = crate::xml::parse(input).unwrap();
assert_eq!(
result,
Message::<Ignore> {
from: None,
id: Some("aacea".to_string()),
to: Some(Jid {
name: Some(Name("nikita".into())),
server: Server("vlnv.dev".into()),
resource: None
}),
r#type: MessageType::Chat,
lang: None,
subject: Some("daa".into()),
body: "bbb".into(),
custom: vec![Ignore],
}
)
}
#[tokio::test]
async fn parse_iq() {
let input = r#"<iq id="bind_1" type="set"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><resource>mobile</resource></bind></iq>"#;
let result: Iq<BindRequest> = crate::xml::parse(input).unwrap();
assert_eq!(
result,
Iq {
from: None,
id: "bind_1".to_string(),
to: None,
r#type: IqType::Set,
body: BindRequest(Resource("mobile".into()))
}
)
}
}

View File

@ -0,0 +1,398 @@
use quick_xml::events::attributes::Attribute;
use quick_xml::events::{BytesEnd, BytesStart, Event};
use quick_xml::name::{QName, ResolveResult};
use anyhow::{Result, anyhow as ffail};
use crate::xml::*;
use super::bind::Jid;
pub const XMLNS_INFO: &'static str = "http://jabber.org/protocol/disco#info";
pub const XMLNS_ITEM: &'static str = "http://jabber.org/protocol/disco#items";
#[derive(PartialEq, Eq, Debug)]
pub struct InfoQuery {
pub node: Option<String>,
pub identity: Vec<Identity>,
pub feature: Vec<Feature>,
}
impl FromXml for InfoQuery {
type P = impl Parser<Output = Result<Self>>;
fn parse() -> Self::P {
|(namespace, event): (ResolveResult<'static>, &'static Event<'static>)| -> Result<Self> {
let mut node = None;
let mut identity = vec![];
let mut feature = vec![];
let (bytes, end) = match event {
Event::Start(bytes) => (bytes, false),
Event::Empty(bytes) => (bytes, true),
_ => return Err(ffail!("Unexpected XML event: {event:?}")),
};
for attr in bytes.attributes() {
let attr = attr?;
match attr.key.0 {
b"node" => {
let s = std::str::from_utf8(&attr.value)?;
node = Some(s.to_owned())
}
_ => {}
}
}
if end {
return Ok(InfoQuery {
node,
identity,
feature,
});
}
loop {
let (namespace, event) = yield;
let bytes = match event {
Event::Start(bytes) => bytes,
Event::Empty(bytes) => bytes,
Event::End(_) => break,
_ => return Err(ffail!("Unexpected XML event: {event:?}")),
};
if bytes.name().0 == Identity::NAME.as_bytes() {
let res = delegate_parsing!(Identity, namespace, event)?;
identity.push(res);
} else if bytes.name().0 == Feature::NAME.as_bytes() {
let res = delegate_parsing!(Feature, namespace, event)?;
feature.push(res);
} else {
return Err(ffail!("Unexpected XML event: {event:?}"));
}
}
return Ok(InfoQuery {
node,
identity,
feature,
});
}
}
}
impl FromXmlTag for InfoQuery {
const NAME: &'static str = "query";
const NS: &'static str = XMLNS_INFO;
}
impl ToXml for InfoQuery {
fn serialize(&self, events: &mut Vec<Event<'static>>) {
let mut bytes = BytesStart::new(format!(r#"query xmlns="{}""#, XMLNS_INFO));
if let Some(node) = &self.node {
bytes.push_attribute(Attribute {
key: QName(b"node"),
value: node.as_bytes().into(),
});
}
let empty = self.feature.is_empty() && self.identity.is_empty();
if empty {
events.push(Event::Empty(bytes));
} else {
events.push(Event::Start(bytes));
}
for i in &self.identity {
let mut bytes = BytesStart::new("identity");
bytes.push_attribute(Attribute {
key: QName(b"category"),
value: i.category.as_bytes().into(),
});
if let Some(name) = &i.name {
bytes.push_attribute(Attribute {
key: QName(b"name"),
value: name.as_bytes().into(),
});
}
bytes.push_attribute(Attribute {
key: QName(b"type"),
value: i.r#type.as_bytes().into(),
});
events.push(Event::Empty(bytes));
}
for f in &self.feature {
let mut bytes = BytesStart::new("feature");
bytes.push_attribute(Attribute {
key: QName(b"var"),
value: f.var.as_bytes().into(),
});
events.push(Event::Empty(bytes));
}
if !empty {
events.push(Event::End(BytesEnd::new("query")));
}
}
}
#[derive(PartialEq, Eq, Debug)]
pub struct Identity {
pub category: String,
pub name: Option<String>,
pub r#type: String,
}
impl FromXml for Identity {
type P = impl Parser<Output = Result<Self>>;
fn parse() -> Self::P {
|(namespace, event): (ResolveResult<'static>, &'static Event<'static>)| -> Result<Self> {
let mut category = None;
let mut name = None;
let mut r#type = None;
let (bytes, end) = match event {
Event::Start(bytes) => (bytes, false),
Event::Empty(bytes) => (bytes, true),
_ => return Err(ffail!("Unexpected XML event: {event:?}")),
};
for attr in bytes.attributes() {
let attr = attr?;
match attr.key.0 {
b"category" => {
let s = std::str::from_utf8(&attr.value)?;
category = Some(s.to_owned())
}
b"name" => {
let s = std::str::from_utf8(&attr.value)?;
name = Some(s.to_owned())
}
b"type" => {
let s = std::str::from_utf8(&attr.value)?;
r#type = Some(s.to_owned())
}
_ => {}
}
}
let Some(category) = category else {
return Err(ffail!("No jid provided"));
};
let Some(r#type) = r#type else {
return Err(ffail!("No type provided"));
};
let item = Identity {
category,
name,
r#type,
};
if end {
return Ok(item);
}
let (namespace, event) = yield;
let Event::End(bytes) = event else {
return Err(ffail!("Unexpected XML event: {event:?}"));
};
Ok(item)
}
}
}
impl FromXmlTag for Identity {
const NAME: &'static str = "identity";
const NS: &'static str = XMLNS_INFO;
}
#[derive(PartialEq, Eq, Debug)]
pub struct Feature {
pub var: String,
}
impl Feature {
pub fn new(s: &str) -> Feature {
Feature { var: s.to_string() }
}
}
impl FromXml for Feature {
type P = impl Parser<Output = Result<Self>>;
fn parse() -> Self::P {
|(namespace, event): (ResolveResult<'static>, &'static Event<'static>)| -> Result<Self> {
let mut var = None;
let (bytes, end) = match event {
Event::Start(bytes) => (bytes, false),
Event::Empty(bytes) => (bytes, true),
_ => return Err(ffail!("Unexpected XML event: {event:?}")),
};
for attr in bytes.attributes() {
let attr = attr?;
match attr.key.0 {
b"var" => {
let s = std::str::from_utf8(&attr.value)?;
var = Some(s.to_owned())
}
_ => {}
}
}
let Some(var) = var else {
return Err(ffail!("No jid provided"));
};
let item = Feature { var };
if end {
return Ok(item);
}
let (namespace, event) = yield;
let Event::End(bytes) = event else {
return Err(ffail!("Unexpected XML event: {event:?}"));
};
Ok(item)
}
}
}
impl FromXmlTag for Feature {
const NAME: &'static str = "feature";
const NS: &'static str = XMLNS_INFO;
}
#[derive(PartialEq, Eq, Debug)]
pub struct ItemQuery {
pub item: Vec<Item>,
}
impl FromXml for ItemQuery {
type P = impl Parser<Output = Result<Self>>;
fn parse() -> Self::P {
|(namespace, event): (ResolveResult<'static>, &'static Event<'static>)| -> Result<Self> {
let mut item = vec![];
let (bytes, end) = match event {
Event::Start(bytes) => (bytes, false),
Event::Empty(bytes) => (bytes, true),
_ => return Err(ffail!("Unexpected XML event: {event:?}")),
};
if end {
return Ok(ItemQuery { item });
}
loop {
let (namespace, event) = yield;
let bytes = match event {
Event::Start(bytes) => bytes,
Event::Empty(bytes) => bytes,
Event::End(_) => break,
_ => return Err(ffail!("Unexpected XML event: {event:?}")),
};
if bytes.name().0 == Item::NAME.as_bytes() {
let res = delegate_parsing!(Item, namespace, event)?;
item.push(res);
} else {
return Err(ffail!("Unexpected XML event: {event:?}"));
}
}
Ok(ItemQuery { item })
}
}
}
impl FromXmlTag for ItemQuery {
const NAME: &'static str = "query";
const NS: &'static str = XMLNS_ITEM;
}
impl ToXml for ItemQuery {
fn serialize(&self, events: &mut Vec<Event<'static>>) {
let mut bytes = BytesStart::new(format!(r#"query xmlns="{}""#, XMLNS_ITEM));
let empty = self.item.is_empty();
if empty {
events.push(Event::Empty(bytes));
} else {
events.push(Event::Start(bytes));
}
for f in &self.item {
let mut bytes = BytesStart::new("item");
bytes.push_attribute(Attribute {
key: QName(b"jid"),
value: f.jid.to_string().into_bytes().into(),
});
if let Some(name) = &f.name {
bytes.push_attribute(Attribute {
key: QName(b"name"),
value: name.as_bytes().into(),
});
}
if let Some(node) = &f.node {
bytes.push_attribute(Attribute {
key: QName(b"node"),
value: node.as_bytes().into(),
});
}
events.push(Event::Empty(bytes));
}
if !empty {
events.push(Event::End(BytesEnd::new("query")));
}
}
}
#[derive(PartialEq, Eq, Debug)]
pub struct Item {
pub jid: super::bind::Jid,
pub name: Option<String>,
pub node: Option<String>,
}
impl FromXml for Item {
type P = impl Parser<Output = Result<Self>>;
fn parse() -> Self::P {
|(namespace, event): (ResolveResult<'static>, &'static Event<'static>)| -> Result<Self> {
let mut jid = None;
let mut name = None;
let mut node = None;
let (bytes, end) = match event {
Event::Start(bytes) => (bytes, false),
Event::Empty(bytes) => (bytes, true),
_ => return Err(ffail!("Unexpected XML event: {event:?}")),
};
for attr in bytes.attributes() {
let attr = attr?;
match attr.key.0 {
b"name" => {
let s = std::str::from_utf8(&attr.value)?;
name = Some(s.to_owned())
}
b"node" => {
let s = std::str::from_utf8(&attr.value)?;
node = Some(s.to_owned())
}
b"jid" => {
let s = std::str::from_utf8(&attr.value)?;
let s = Jid::from_string(s)?;
jid = Some(s)
}
_ => {}
}
}
let Some(jid) = jid else {
return Err(ffail!("No jid provided"));
};
let item = Item { jid, name, node };
if end {
return Ok(item);
}
let (namespace, event) = yield;
let Event::End(bytes) = event else {
return Err(ffail!("Unexpected XML event: {event:?}"));
};
Ok(item)
}
}
}
impl FromXmlTag for Item {
const NAME: &'static str = "item";
const NS: &'static str = XMLNS_ITEM;
}

View File

@ -0,0 +1,37 @@
#![feature(
generators,
generator_trait,
type_alias_impl_trait,
impl_trait_in_assoc_type
)]
pub mod bind;
pub mod client;
pub mod disco;
pub mod muc;
pub mod roster;
pub mod sasl;
pub mod session;
pub mod stanzaerror;
pub mod stream;
pub mod tls;
mod prelude;
pub mod xml;
// Implemented as a macro instead of a fn due to borrowck limitations
macro_rules! skip_text {
($reader: ident, $buf: ident) => {
loop {
use quick_xml::events::Event;
$buf.clear();
let res = $reader.read_event_into_async($buf).await?;
if let Event::Text(_) = res {
continue;
} else {
break res;
}
}
};
}
pub(crate) use skip_text;

View File

@ -0,0 +1,231 @@
#![allow(unused_variables)]
use quick_xml::events::Event;
use quick_xml::name::ResolveResult;
use crate::xml::*;
use anyhow::{anyhow, Result};
pub const XMLNS: &'static str = "http://jabber.org/protocol/muc";
#[derive(PartialEq, Eq, Debug, Default)]
pub struct History {
pub maxchars: Option<u32>,
pub maxstanzas: Option<u32>,
pub seconds: Option<u32>,
}
impl FromXml for History {
type P = impl Parser<Output = Result<Self>>;
fn parse() -> Self::P {
|(namespace, event): (ResolveResult<'static>, &'static Event<'static>)| -> Result<Self> {
let mut history = History::default();
let (bytes, end) = match event {
Event::Start(bytes) if bytes.name().0 == Self::NAME.as_bytes() => (bytes, false),
Event::Empty(bytes) if bytes.name().0 == Self::NAME.as_bytes() => (bytes, true),
_ => return Err(anyhow!("Unexpected XML event: {event:?}")),
};
for attr in bytes.attributes() {
let attr = attr?;
match attr.key.0 {
b"maxchars" => {
let s = std::str::from_utf8(&attr.value)?;
let a = s.parse()?;
history.maxchars = Some(a)
}
b"maxstanzas" => {
let s = std::str::from_utf8(&attr.value)?;
let a = s.parse()?;
history.maxstanzas = Some(a)
}
b"seconds" => {
let s = std::str::from_utf8(&attr.value)?;
let a = s.parse()?;
history.seconds = Some(a)
}
_ => {}
}
}
if end {
return Ok(history);
}
let (namespace, event) = yield;
let Event::End(bytes) = event else {
return Err(anyhow!("Unexpected XML event: {event:?}"));
};
Ok(history)
}
}
}
impl FromXmlTag for History {
const NAME: &'static str = "history";
const NS: &'static str = XMLNS;
}
#[derive(PartialEq, Eq, Debug)]
pub struct Password(pub String);
impl FromXml for Password {
type P = impl Parser<Output = Result<Self>>;
fn parse() -> Self::P {
|(namespace, event): (ResolveResult<'static>, &'static Event<'static>)| -> Result<Self> {
let bytes = match event {
Event::Start(bytes) if bytes.name().0 == Self::NAME.as_bytes() => bytes,
_ => return Err(anyhow!("Unexpected XML event: {event:?}")),
};
let (namespace, event) = yield;
let Event::Text(bytes) = event else {
return Err(anyhow!("Unexpected XML event: {event:?}"));
};
let s = std::str::from_utf8(bytes)?.to_string();
let (namespace, event) = yield;
let Event::End(bytes) = event else {
return Err(anyhow!("Unexpected XML event: {event:?}"));
};
Ok(Password(s))
}
}
}
impl FromXmlTag for Password {
const NAME: &'static str = "password";
const NS: &'static str = XMLNS;
}
#[derive(PartialEq, Eq, Debug, Default)]
pub struct X {
pub history: Option<History>,
pub password: Option<Password>,
}
impl FromXml for X {
type P = impl Parser<Output = Result<Self>>;
fn parse() -> Self::P {
|(namespace, event): (ResolveResult<'static>, &'static Event<'static>)| -> Result<Self> {
let mut res = X::default();
let (_, end) = match event {
Event::Start(bytes) => (bytes, false),
Event::Empty(bytes) => (bytes, true),
_ => return Err(anyhow!("Unexpected XML event: {event:?}")),
};
if end {
return Ok(res);
}
loop {
let (namespace, event) = yield;
let bytes = match event {
Event::Start(bytes) => bytes,
Event::Empty(bytes) => bytes,
Event::End(_) => break,
_ => return Err(anyhow!("Unexpected XML event: {event:?}")),
};
if bytes.name().0 == Password::NAME.as_bytes() {
let password = delegate_parsing!(Password, namespace, event)?;
res.password = Some(password);
} else if bytes.name().0 == History::NAME.as_bytes() {
let history = delegate_parsing!(History, namespace, event)?;
res.history = Some(history);
} else {
return Err(anyhow!("Unexpected XML event: {event:?}"));
}
}
Ok(res)
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_history_success_empty() {
let input = "<history/>";
let res: History = parse(input).unwrap();
let expected = History {
maxchars: None,
maxstanzas: None,
seconds: None,
};
assert_eq!(res, expected);
}
#[test]
fn test_history_success_empty_attrs() {
let input = r#"<history maxchars="1" maxstanzas="2" seconds="4"/>"#;
let res: History = parse(input).unwrap();
let expected = History {
maxchars: Some(1),
maxstanzas: Some(2),
seconds: Some(4),
};
assert_eq!(res, expected);
}
#[test]
fn test_history_success_start_end() {
let input = r#"<history></history>"#;
let res: History = parse(input).unwrap();
let expected = History {
maxchars: None,
maxstanzas: None,
seconds: None,
};
assert_eq!(res, expected);
}
#[test]
fn test_history_incorrect_empty() {
let input = r#"<iq/>"#;
parse::<History>(input).err().unwrap();
}
#[test]
fn test_password_success() {
let input = "<password>olala</password>";
let res: Password = parse(input).unwrap();
let expected = Password("olala".into());
assert_eq!(res, expected);
}
#[test]
fn test_password_incorrect() {
let input = r#"<iq>asdsd</iq>"#;
parse::<Password>(input).err().unwrap();
}
#[test]
fn test_x_success_empty() {
let input = "<x/>";
let res: X = parse(input).unwrap();
let expected = X {
history: None,
password: None,
};
assert_eq!(res, expected);
}
#[test]
fn test_x_success_full() {
let input = r#"<x><password>ololo</password><history maxchars="1"/></x>"#;
let res: X = parse(input).unwrap();
let expected = X {
history: Some(History {
maxchars: Some(1),
maxstanzas: None,
seconds: None,
}),
password: Some(Password("ololo".into())),
};
assert_eq!(res, expected);
}
}

View File

@ -0,0 +1,3 @@
use std::sync::Arc;
pub type Str = Arc<str>;

View File

@ -0,0 +1,60 @@
use quick_xml::events::{BytesStart, Event};
use crate::xml::*;
use anyhow::{anyhow as ffail, Result};
pub const XMLNS: &'static str = "jabber:iq:roster";
#[derive(PartialEq, Eq, Debug)]
pub struct RosterQuery;
pub struct QueryParser(QueryParserInner);
enum QueryParserInner {
Initial,
InQuery,
}
impl Parser for QueryParser {
type Output = Result<RosterQuery>;
fn consume<'a>(
self: Self,
namespace: quick_xml::name::ResolveResult,
event: &quick_xml::events::Event<'a>,
) -> Continuation<Self, Self::Output> {
match self.0 {
QueryParserInner::Initial => match event {
Event::Start(_) => Continuation::Continue(QueryParser(QueryParserInner::InQuery)),
Event::Empty(_) => Continuation::Final(Ok(RosterQuery)),
_ => Continuation::Final(Err(ffail!("Unexpected XML event: {event:?}"))),
},
QueryParserInner::InQuery => match event {
Event::End(_) => Continuation::Final(Ok(RosterQuery)),
_ => Continuation::Final(Err(ffail!("Unexpected XML event: {event:?}"))),
},
}
}
}
impl FromXml for RosterQuery {
type P = QueryParser;
fn parse() -> Self::P {
QueryParser(QueryParserInner::Initial)
}
}
impl FromXmlTag for RosterQuery {
const NAME: &'static str = "query";
const NS: &'static str = XMLNS;
}
impl ToXml for RosterQuery {
fn serialize(&self, events: &mut Vec<Event<'static>>) {
events.push(Event::Empty(BytesStart::new(format!(
r#"query xmlns="{}""#,
XMLNS
))));
}
}

View File

@ -0,0 +1,164 @@
use std::borrow::Borrow;
use quick_xml::{
events::{BytesStart, Event},
NsReader, Writer,
};
use tokio::io::{AsyncBufRead, AsyncWrite};
use base64::{Engine as _, engine::general_purpose};
use super::skip_text;
use anyhow::{anyhow, Result};
pub enum Mechanism {
Plain,
}
impl Mechanism {
pub fn to_str(&self) -> &'static str {
match self {
Mechanism::Plain => "PLAIN",
}
}
pub fn from_str(input: &[u8]) -> Result<Mechanism> {
match input {
b"PLAIN" => Ok(Mechanism::Plain),
_ => Err(anyhow!("unknown auth mechanism: {input:?}")),
}
}
}
#[derive(PartialEq, Debug)]
pub struct AuthBody {
pub login: String,
pub password: String,
}
impl AuthBody {
pub fn from_str(input: &[u8]) -> Result<AuthBody> {
match general_purpose::STANDARD.decode(input){
Ok(decoded_body) => {
match String::from_utf8(decoded_body) {
Ok(parsed_to_string) => {
let separated_words: Vec<&str> = parsed_to_string.split("\x00").collect::<Vec<_>>().clone();
if separated_words.len() == 3 {
// first segment ignored (might be needed in the future)
Ok(AuthBody { login: separated_words[1].to_string(), password: separated_words[2].to_string() })
} else { return Err(anyhow!("Incorrect auth format")) }
},
Err(e) => return Err(anyhow!(e))
}
},
Err(e) => return Err(anyhow!(e))
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_returning_auth_body() {
let orig = b"\x00login\x00pass";
let encoded = general_purpose::STANDARD.encode(orig);
let expected = AuthBody {login: "login".to_string(), password: "pass".to_string()};
let result = AuthBody::from_str(encoded.as_bytes()).unwrap();
assert_eq!(expected, result);
}
#[test]
fn test_ignoring_first_segment() {
let orig = b"ignored\x00login\x00pass";
let encoded = general_purpose::STANDARD.encode(orig);
let expected = AuthBody {login: "login".to_string(), password: "pass".to_string()};
let result = AuthBody::from_str(encoded.as_bytes()).unwrap();
assert_eq!(expected, result);
}
#[test]
fn test_returning_auth_body_with_empty_strings() {
let orig = b"\x00\x00";
let encoded = general_purpose::STANDARD.encode(orig);
let expected = AuthBody {login: "".to_string(), password: "".to_string()};
let result = AuthBody::from_str(encoded.as_bytes()).unwrap();
assert_eq!(expected, result);
}
#[test]
fn test_fail_if_size_less_then_3() {
let orig = b"login\x00pass";
let encoded = general_purpose::STANDARD.encode(orig);
let expected = AuthBody {login: "login".to_string(), password: "pass".to_string()};
let result = AuthBody::from_str(encoded.as_bytes());
assert!(result.is_err());
}
#[test]
fn test_fail_if_size_greater_then_3() {
let orig = b"first\x00login\x00pass\x00other";
let encoded = general_purpose::STANDARD.encode(orig);
let expected = AuthBody {login: "login".to_string(), password: "pass".to_string()};
let result = AuthBody::from_str(encoded.as_bytes());
assert!(result.is_err());
}
}
pub struct Auth {
pub mechanism: Mechanism,
pub body: Vec<u8>,
}
impl Auth {
pub async fn parse(
reader: &mut NsReader<impl AsyncBufRead + Unpin>,
buf: &mut Vec<u8>,
) -> Result<Auth> {
let event = skip_text!(reader, buf);
let mechanism = if let Event::Start(bytes) = event {
let mut mechanism = None;
for attr in bytes.attributes() {
let attr = attr?;
if attr.key.0 == b"mechanism" {
mechanism = Some(attr.value)
}
}
if let Some(mechanism) = mechanism {
Mechanism::from_str(mechanism.borrow())?
} else {
return Err(anyhow!("expected mechanism attribute in <auth>"));
}
} else {
return Err(anyhow!("expected start of <auth>"));
};
let body = if let Event::Text(text) = reader.read_event_into_async(buf).await? {
text.into_inner().into_owned()
} else {
return Err(anyhow!("expected text body in <auth>"));
};
if let Event::End(_) = reader.read_event_into_async(buf).await? {
//TODO
} else {
return Err(anyhow!("expected end of <auth>"));
};
Ok(Auth { mechanism, body })
}
}
pub struct Success;
impl Success {
pub async fn write_xml(&self, writer: &mut Writer<impl AsyncWrite + Unpin>) -> Result<()> {
let event = BytesStart::new(r#"success xmlns="urn:ietf:params:xml:ns:xmpp-sasl""#);
writer.write_event_async(Event::Empty(event)).await?;
Ok(())
}
}

View File

@ -0,0 +1,62 @@
use quick_xml::events::{BytesStart, Event};
use crate::xml::*;
use anyhow::{anyhow, Result};
pub const XMLNS: &'static str = "urn:ietf:params:xml:ns:xmpp-session";
#[derive(PartialEq, Eq, Debug)]
pub struct Session;
pub struct SessionParser(SessionParserInner);
enum SessionParserInner {
Initial,
InSession,
}
impl Parser for SessionParser {
type Output = Result<Session>;
fn consume<'a>(
self: Self,
namespace: quick_xml::name::ResolveResult,
event: &quick_xml::events::Event<'a>,
) -> Continuation<Self, Self::Output> {
match self.0 {
SessionParserInner::Initial => match event {
Event::Start(_) => {
Continuation::Continue(SessionParser(SessionParserInner::InSession))
}
Event::Empty(_) => Continuation::Final(Ok(Session)),
_ => Continuation::Final(Err(anyhow!("Unexpected XML event: {event:?}"))),
},
SessionParserInner::InSession => match event {
Event::End(_) => Continuation::Final(Ok(Session)),
_ => Continuation::Final(Err(anyhow!("Unexpected XML event: {event:?}"))),
},
}
}
}
impl FromXml for Session {
type P = SessionParser;
fn parse() -> Self::P {
SessionParser(SessionParserInner::Initial)
}
}
impl FromXmlTag for Session {
const NAME: &'static str = "session";
const NS: &'static str = XMLNS;
}
impl ToXml for Session {
fn serialize(&self, events: &mut Vec<Event<'static>>) {
events.push(Event::Empty(BytesStart::new(format!(
r#"session xmlns="{}""#,
XMLNS
))));
}
}

View File

@ -0,0 +1,25 @@
pub enum StanzaError {
BadRequest,
Conflict,
FeatureNotImplemented,
Forbidden,
Gone(String),
InternalServerError,
ItemNotFound,
JidMalformed,
NotAcceptable,
NotAllowed,
NotAuthorized,
PaymentRequired,
PolicyViolation,
RecipientUnavailable,
Redirect(String),
RegistrationRequired,
RemoteServerNotFound,
RemoteServerTimeout,
ResourceConstraint,
ServiceUnavailable,
SubscriptionRequired,
UndefinedCondition,
UnexpectedRequest,
}

View File

@ -0,0 +1,212 @@
use quick_xml::events::attributes::Attribute;
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
use quick_xml::name::{Namespace, QName, ResolveResult};
use quick_xml::{NsReader, Writer};
use tokio::io::{AsyncBufRead, AsyncWrite};
use super::skip_text;
use anyhow::{anyhow, Result};
use crate::xml::ToXml;
pub static XMLNS: &'static str = "http://etherx.jabber.org/streams";
pub static PREFIX: &'static str = "stream";
pub static XMLNS_XML: &'static str = "http://www.w3.org/XML/1998/namespace";
#[derive(Debug, PartialEq, Eq)]
pub struct ClientStreamStart {
pub to: String,
pub lang: Option<String>,
pub version: String,
}
impl ClientStreamStart {
pub async fn parse(
reader: &mut NsReader<impl AsyncBufRead + Unpin>,
buf: &mut Vec<u8>,
) -> Result<ClientStreamStart> {
let incoming = skip_text!(reader, buf);
if let Event::Start(e) = incoming {
let (ns, local) = reader.resolve_element(e.name());
if ns != ResolveResult::Bound(Namespace(XMLNS.as_bytes())) {
return Err(panic!());
}
if local.into_inner() != b"stream" {
return Err(panic!());
}
let mut to = None;
let mut lang = None;
let mut version = None;
for attr in e.attributes() {
let attr = attr?;
let (ns, name) = reader.resolve_attribute(attr.key);
match (ns, name.into_inner()) {
(ResolveResult::Unbound, b"to") => {
let value = attr.unescape_value()?;
to = Some(value.to_string());
}
(
ResolveResult::Bound(Namespace(b"http://www.w3.org/XML/1998/namespace")),
b"lang",
) => {
let value = attr.unescape_value()?;
lang = Some(value.to_string());
}
(ResolveResult::Unbound, b"version") => {
let value = attr.unescape_value()?;
version = Some(value.to_string());
}
_ => {}
}
}
Ok(ClientStreamStart {
to: to.unwrap(),
lang: lang,
version: version.unwrap(),
})
} else {
Err(panic!())
}
}
}
pub struct ServerStreamStart {
pub from: String,
pub lang: String,
pub id: String,
pub version: String,
}
impl ServerStreamStart {
pub async fn write_xml(&self, writer: &mut Writer<impl AsyncWrite + Unpin>) -> Result<()> {
let mut event = BytesStart::new("stream:stream");
let attributes = [
Attribute {
key: QName(b"from"),
value: self.from.as_bytes().into(),
},
Attribute {
key: QName(b"version"),
value: self.version.as_bytes().into(),
},
Attribute {
key: QName(b"xmlns"),
value: super::client::XMLNS.as_bytes().into(),
},
Attribute {
key: QName(b"xmlns:stream"),
value: XMLNS.as_bytes().into(),
},
Attribute {
key: QName(b"xml:lang"),
value: self.lang.as_bytes().into(),
},
Attribute {
key: QName(b"id"),
value: self.id.as_bytes().into(),
},
];
event.extend_attributes(attributes.into_iter());
writer.write_event_async(Event::Start(event)).await?;
Ok(())
}
}
pub struct ServerStreamEnd;
impl ToXml for ServerStreamEnd {
fn serialize(&self, events: &mut Vec<Event<'static>>) {
events.push(Event::End(BytesEnd::new("stream:stream")));
}
}
pub struct Features {
pub start_tls: bool,
pub mechanisms: bool,
pub bind: bool,
}
impl Features {
pub async fn write_xml(&self, writer: &mut Writer<impl AsyncWrite + Unpin>) -> Result<()> {
writer
.write_event_async(Event::Start(BytesStart::new("stream:features")))
.await?;
if self.start_tls {
writer
.write_event_async(Event::Start(BytesStart::new(
r#"starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls""#,
)))
.await?;
writer
.write_event_async(Event::Empty(BytesStart::new("required")))
.await?;
writer
.write_event_async(Event::End(BytesEnd::new("starttls")))
.await?;
}
if self.mechanisms {
writer
.write_event_async(Event::Start(BytesStart::new(
r#"mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl""#,
)))
.await?;
writer
.write_event_async(Event::Start(BytesStart::new(r#"mechanism"#)))
.await?;
writer
.write_event_async(Event::Text(BytesText::new("PLAIN")))
.await?;
writer
.write_event_async(Event::End(BytesEnd::new("mechanism")))
.await?;
writer
.write_event_async(Event::End(BytesEnd::new("mechanisms")))
.await?;
}
if self.bind {
writer
.write_event_async(Event::Empty(BytesStart::new(
r#"bind xmlns="urn:ietf:params:xml:ns:xmpp-bind""#,
)))
.await?;
}
writer
.write_event_async(Event::End(BytesEnd::new("stream:features")))
.await?;
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
#[tokio::test]
async fn client_stream_start_correct_parse() {
let input = r###"<stream:stream xmlns:stream="http://etherx.jabber.org/streams" to="vlnv.dev" version="1.0" xmlns="jabber:client" xml:lang="en" xmlns:xml="http://www.w3.org/XML/1998/namespace">"###;
let mut reader = NsReader::from_reader(input.as_bytes());
let mut buf = vec![];
let res = ClientStreamStart::parse(&mut reader, &mut buf)
.await
.unwrap();
assert_eq!(
res,
ClientStreamStart {
to: "vlnv.dev".to_owned(),
lang: Some("en".to_owned()),
version: "1.0".to_owned()
}
)
}
#[tokio::test]
async fn server_stream_start_write() {
let input = ServerStreamStart {
from: "vlnv.dev".to_owned(),
lang: "en".to_owned(),
id: "stream_id".to_owned(),
version: "1.0".to_owned(),
};
let expected = r###"<stream:stream from="vlnv.dev" version="1.0" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams" xml:lang="en" id="stream_id">"###;
let mut output: Vec<u8> = vec![];
let mut writer = Writer::new(&mut output);
input.write_xml(&mut writer).await.unwrap();
assert_eq!(std::str::from_utf8(&output).unwrap(), expected);
}
}

View File

@ -0,0 +1,41 @@
use quick_xml::events::attributes::Attribute;
use quick_xml::events::{BytesStart, Event};
use quick_xml::name::QName;
use quick_xml::{NsReader, Writer};
use tokio::io::{AsyncBufRead, AsyncWrite};
use super::skip_text;
use anyhow::{anyhow, Result};
pub static XMLNS: &'static str = "urn:ietf:params:xml:ns:xmpp-tls";
pub struct StartTLS;
impl StartTLS {
pub async fn parse(
reader: &mut NsReader<impl AsyncBufRead + Unpin>,
buf: &mut Vec<u8>,
) -> Result<StartTLS> {
let incoming = skip_text!(reader, buf);
if let Event::Empty(ref e) = incoming {
if e.name().0 == b"starttls" {
return Ok(StartTLS);
}
}
Err(anyhow!("XML tag starttls expected, received: {incoming:?}"))
}
}
pub struct ProceedTLS;
impl ProceedTLS {
pub async fn write_xml(&self, writer: &mut Writer<impl AsyncWrite + Unpin>) -> Result<()> {
let mut event = BytesStart::new("proceed");
let attributes = [Attribute {
key: QName(b"xmlns"),
value: XMLNS.as_bytes().into(),
}];
event.extend_attributes(attributes.into_iter());
writer.write_event_async(Event::Empty(event)).await?;
Ok(())
}
}

View File

@ -0,0 +1,62 @@
use super::*;
use derive_more::From;
#[derive(Default, Debug, PartialEq, Eq)]
pub struct Ignore;
#[derive(From)]
pub struct IgnoreParser(IgnoreParserInner);
enum IgnoreParserInner {
Initial,
InTag { name: Vec<u8>, depth: u8 },
}
impl Parser for IgnoreParser {
type Output = Result<Ignore>;
fn consume<'a>(
self: Self,
_: ResolveResult,
event: &Event<'a>,
) -> Continuation<Self, Self::Output> {
match self.0 {
IgnoreParserInner::Initial => match event {
Event::Start(bytes) => {
let name = bytes.name().0.to_owned();
Continuation::Continue(IgnoreParserInner::InTag { name, depth: 0 }.into())
}
Event::Empty(_) => Continuation::Final(Ok(Ignore)),
_ => Continuation::Final(Ok(Ignore)),
},
IgnoreParserInner::InTag { name, depth } => match event {
Event::End(bytes) if name == bytes.name().0 => {
if depth == 0 {
Continuation::Final(Ok(Ignore))
} else {
Continuation::Continue(
IgnoreParserInner::InTag {
name,
depth: depth - 1,
}
.into(),
)
}
}
_ => Continuation::Continue(IgnoreParserInner::InTag { name, depth }.into()),
},
}
}
}
impl FromXml for Ignore {
type P = IgnoreParser;
fn parse() -> Self::P {
IgnoreParserInner::Initial.into()
}
}
impl ToXml for () {
fn serialize(&self, _: &mut Vec<Event<'static>>) {}
}

View File

@ -0,0 +1,129 @@
use std::ops::Generator;
use std::pin::Pin;
use quick_xml::NsReader;
use quick_xml::events::Event;
use quick_xml::name::ResolveResult;
use anyhow::Result;
mod ignore;
pub use ignore::Ignore;
pub trait FromXml: Sized {
type P: Parser<Output = Result<Self>>;
fn parse() -> Self::P;
}
pub trait ToXml: Sized {
fn serialize(&self, events: &mut Vec<Event<'static>>);
}
pub trait FromXmlTag: FromXml {
const NAME: &'static str;
const NS: &'static str;
}
pub trait Parser: Sized {
type Output;
fn consume<'a>(
self: Self,
namespace: ResolveResult,
event: &Event<'a>,
) -> Continuation<Self, Self::Output>;
}
impl<T, Out> Parser for T
where
T: Generator<(ResolveResult<'static>, &'static Event<'static>), Yield = (), Return = Out>
+ Unpin,
{
type Output = Out;
fn consume<'a>(
mut self: Self,
namespace: ResolveResult,
event: &Event<'a>,
) -> Continuation<Self, Self::Output> {
let s = Pin::new(&mut self);
// this is a very rude workaround fixing the fact that rust generators
// 1. don't support higher-kinded lifetimes (i.e. no `impl for <'a> Generator<Event<'a>>)
// 2. don't track borrows across yield points and lack thereof
// implementors of Parser should manually check that inputs are not used across yields
match s.resume(unsafe { std::mem::transmute((namespace, event)) }) {
std::ops::GeneratorState::Yielded(()) => Continuation::Continue(self),
std::ops::GeneratorState::Complete(res) => Continuation::Final(res),
}
}
}
pub enum Continuation<Parser, Res> {
Final(Res),
Continue(Parser),
}
pub fn parse<T: FromXml>(input: &str) -> Result<T> {
let mut reader = NsReader::from_reader(input.as_bytes());
let mut buf = vec![];
let (ns, event) = reader.read_resolved_event_into(&mut buf)?;
let mut parser: Continuation<_, std::result::Result<T, anyhow::Error>> = T::parse().consume(ns, &event);
loop {
match parser {
Continuation::Final(res) => break res,
Continuation::Continue(next) => {
let (ns, event) = reader.read_resolved_event_into(&mut buf)?;
parser = next.consume(ns, &event);
}
}
}
}
macro_rules! fail_fast {
($errorable: expr) => {
match $errorable {
Ok(i) => i,
Err(e) => return Continuation::Final(Err(e.into())),
}
};
}
#[macro_export]
macro_rules! delegate_parsing {
($parser: ty, $namespace: expr, $event: expr) => {{
let mut parser = <$parser as FromXml>::parse().consume($namespace, $event);
loop {
match parser {
Continuation::Final(Ok(res)) => break Ok(res.into()),
Continuation::Final(Err(err)) => break Err(err),
Continuation::Continue(p) => {
let (namespace, event) = yield;
parser = p.consume(namespace, event);
}
}
}
}};
}
#[macro_export]
macro_rules! match_parser {
($name: expr, $ns: expr, $event: expr; $subtype: ty, $fin: block) => {
if $name.0 == <$subtype as FromXmlTag>::NAME.as_bytes() && $ns == ResolveResult::Bound(Namespace(<$subtype as FromXmlTag>::NS.as_bytes())) {
delegate_parsing!($subtype, $ns, $event)
} else {
$fin
}
};
($name: expr, $ns: expr, $event: expr; $subtype: ty, $($rest: ty),+, $fin: block) => {
if $name.0 == <$subtype as FromXmlTag>::NAME.as_bytes() && $ns == ResolveResult::Bound(Namespace(<$subtype as FromXmlTag>::NS.as_bytes())) {
delegate_parsing!($subtype, $ns, $event)
} else {
match_parser!($name, $ns, $event; $($rest),*, $fin)
}
};
}
pub use delegate_parsing;
pub(crate) use fail_fast;
pub use match_parser;

View File

@ -1,9 +0,0 @@
[package]
name = "lavina_proto"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.68"
async-trait = "0.1.63"
serde = { version = "1.0.152", features = ["serde_derive"] }

View File

@ -1,2 +0,0 @@
mod prelude;
pub mod well_known;

View File

@ -1,3 +0,0 @@
pub type Result<T> = std::result::Result<T, anyhow::Error>;
pub use async_trait::async_trait;
pub use serde::{Deserialize, Serialize};

View File

@ -1,13 +0,0 @@
use crate::prelude::*;
#[derive(Serialize, Deserialize)]
pub struct ServerInfo {
pub name: String,
pub user_api_base_url: String,
pub fedi_api_base_url: String,
}
#[async_trait]
pub trait WellKnown {
async fn well_known(&self) -> Result<ServerInfo>;
}

43
deny.toml Normal file
View File

@ -0,0 +1,43 @@
targets = [
{ triple = "x86_64-unknown-linux-gnu" },
{ triple = "arm64-unknown-linux-gnu" },
]
[bans]
deny = [
{ name = "openssl" },
{ name = "openssl-sys" },
{ name = "libssh2-sys" },
]
[licenses]
# See https://spdx.org/licenses/ for list of possible licenses
unlicensed = "deny"
allow = [
"MIT",
"Apache-2.0",
"ISC",
"BSD-3-Clause",
]
exceptions = [
{ allow = ["Unicode-DFS-2016"], name = "unicode-ident" },
{ allow = ["OpenSSL"], name = "ring" },
]
deny = [
"GPL-2.0",
"GPL-3.0",
"AGPL-3.0",
"MPL-2.0" # it is used by webpki-roots; we do not hardcode root certs in the binary
]
copyleft = "deny"
confidence-threshold = 0.93
private = { ignore = true }
[[licenses.clarify]]
name = "ring"
version = "*"
expression = "MIT AND ISC AND OpenSSL"
license-files = [
# Each entry is a crate relative path, and the (opaque) hash of its contents
{ path = "LICENSE", hash = 0xbd0eed23 }
]

11
dist/alpine3.18.Dockerfile vendored Normal file
View File

@ -0,0 +1,11 @@
FROM rust:1.72.0-alpine3.18@sha256:2f5592c561cef195c9fa4462633a674458dc375fc0ba4b80e7efe4c3c8e68403 as bld
RUN apk add --no-cache musl-dev
COPY . .
RUN cargo build --release
FROM alpine:3.18@sha256:7144f7bab3d4c2648d7e59409f15ec52a18006a128c733fcff20d3a4a54ba44a
COPY --from=bld target/release/lavina /usr/bin/lavina
VOLUME ["/etc/lavina/", "/var/lib/lavina/"]
ENTRYPOINT ["lavina", "--config", "/etc/lavina/config.toml"]

51
docs/cheatsheet.md Normal file
View File

@ -0,0 +1,51 @@
# Cheatsheet
Some useful commands for development and testing.
<!-- please use spaces at line start to indicate shell cmds -->
## Certificates
Following commands require `OpenSSL` to be installed. It is provided as `openssl` package in Arch Linux.
Generate self-signed TLS certificate:
openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -noenc \
-keyout certs/xmpp.key -out certs/xmpp.pem \
-subj "/CN=example.com"
Print content of a TLS certificate:
openssl x509 -in certs/xmpp.pem -text
Make sure `xmpp.key` starts and ends with:
```
-----BEGIN RSA PRIVATE KEY-----
-----END RSA PRIVATE KEY-----
```
## Protocol Specs
XMPP XSDs - [https://xmpp.org/schemas/index.shtml]
IRC modern spec - [https://modern.ircdocs.horse/]
## Initializing DB with some users
sqlite3 db.sqlite < test/init_state.sql
Same test migration could be used for integration tests in the future.
## Using irssi
irssi in a TUI-based IRC client.
Connecting:
/connect -nocap <address> [<port> [<password> [<nick>]]]
Password should be the same as in storage.
Example:
/connect -nocap 127.0.0.1 6667 parolchik1 kek

67
docs/flow.md Normal file
View File

@ -0,0 +1,67 @@
## Sending a message
```mermaid
sequenceDiagram
participant Alice PC
participant Alice Connection 1
participant Alice Phone
participant Alice Connection 2
participant Alice
participant Room
participant Bob
participant Bob Connection 1
participant Bob PC
Alice PC->>Alice Connection 1: `PRIVMSG Room :Hello!`
Alice Connection 1->>+Alice: PlayerCommand::SendMessage
Alice->Alice: Check permissions
Alice->>+Room: /send Room Hello!
Alice-->>Alice Connection 1: fulfil promise
Alice->>-Alice Connection 2: Updates::NewMessage
Alice Connection 2->>Alice Phone: `:Alice PRIVMSG Room :Hello!`
Room->>+Bob: Updates::NewMessage
deactivate Room;
Bob->>+Bob Connection 1: Updates::NewMessage
deactivate Bob;
Bob Connection 1->>-Bob PC: `:Alice PRIVMSG Room :Hello!`
```
## Context
```mermaid
C4Context
System_Ext(AlicePC, "Alice PC", "Alice's PC")
BiRel(AlicePC, AliceConn1, "IRC")
System_Ext(AlicePh, "Alice Phone", "Alice's phone")
BiRel(AlicePh, AliceConn2, "XMPP")
Boundary(Node, "Node", "Single server process") {
System(AliceConn1, "Alice Connection 1", "Projection nto an IRC socket")
System(AliceConn2, "Alice Connection 2", "Projection onto an XMPP socket")
BiRel(AliceConn1, Alice, "Cmds & updates")
BiRel(AliceConn2, Alice, "Cmds & updates")
Boundary(Core, "Core", "Core chat entities") {
System(Alice, "Alice", "Player actor")
System(Room1, "#room")
BiRel(Alice, Room1, "")
System(Bob, "Bob", "Player actor")
BiRel(Bob, Room1, "")
System(Players, "Player Registry", "")
Rel(Players, Alice, "References")
Rel(Players, Bob, "References")
Rel(Players, Rooms, "")
System(Rooms, "Room Registry", "")
Rel(Rooms, Room1, "")
}
System(BobConn1, "Bob Connection 1", "Projection onto an IRC socket")
BiRel(BobConn1, Bob, "Cmds & updates")
}
System_Ext(BobPC, "Bob PC", "Bob's PC")
BiRel(BobPC, BobConn1, "IRC")
UpdateLayoutConfig($c4ShapeInRow="3", $c4BoundaryInRow="1")
```

26
docs/mapping.md Normal file
View File

@ -0,0 +1,26 @@
## Core Concepts
| Lavina | IRC | XMPP | Matrix |
| -------- | ----------- | ------------------- | ---------- |
| Realm | Network | Server | Homeserver |
| Player | User | User | User |
| Room | Global chan | MUC/MIX group chats | Room |
| Presence | N/A | Presence | ? |
## Player
| Lavina | IRC | Description |
| --------- | -------- | --------------------------- |
| Username | Nickname | Visible name of user's |
| Username | Username | Mostly unused in modern IRC |
| Full name | Realname | User's free-form real name |
| Username | Account | IRC extension |
## Rooms
| Lavina | IRC | Description |
| ------ | ------------- | -------------------------- |
| Member | Joined | Shown in room's memberlist |
| Join | JOIN | Become a room's member |
| Leave | PART | Stop being a room's member |
| Ban | MODE #chan +b | Ban a current room member |

33
docs/xmpp.md Normal file
View File

@ -0,0 +1,33 @@
# XMPP Implementation
## Storage
[XEP-0049] Private XML Storage will not be supported, as it provides an ability to store an arbitrary XML which cannot be reused by other projections. [XEP-0048] Bookmarks also will not be supported, as it relies on storage. Instead, potentially [XEP-0402] PEP Native Bookmarks will be implemented.
[XEP-0048]: https://xmpp.org/extensions/xep-0048.html
[XEP-0049]: https://xmpp.org/extensions/xep-0049.html
[XEP-0402]: https://xmpp.org/extensions/xep-0402.html
IQ namespace to be ignored is `jabber:iq:private`.
## Blocklists
[XEP-0016] will not be supported, as it's deprecated in favor of [XEP-0191], which potentially will be implemented.
[XEP-0016]: https://xmpp.org/extensions/xep-0016.html
[XEP-0191]: https://xmpp.org/extensions/xep-0191.html
IQ namespace to be ignored is `jabber:iq:privacy`.
## Chat history
[XEP-0313] Message Archive Management will be supported. Its implementation shall use the same storage backend as chat history features in other projections.
[XEP-0313]: https://xmpp.org/extensions/xep-0313.html
## Rooms
[XEP-0045] MUC and [XEP-0369] MIX will both be implemented.
[XEP-0045]: https://xmpp.org/extensions/xep-0045.html
[XEP-0369]: https://xmpp.org/extensions/xep-0369.html

25
readme.md Normal file
View File

@ -0,0 +1,25 @@
# Lavina
Multiprotocol chat server based on open protocols.
## Goals
#### Support for multiple open protocols
Federated protocols such as XMPP and Matrix aim to solve the problem of isolation between service providers. They allow people using one service provider to easily communicate with people using different providers, which is not possible with providers based on non-federated protocols, such as Telegram and WhatsApp.
Non-federated protocols also make it harder to bring a new provider into the market, as it will have a few users at first, which will not be able to communicate with their friends who use a different provider commonly known as a **[network effect](https://en.wikipedia.org/wiki/Network_effect)**.
Using a federated protocol does not solve the problem entirely, though, as it brings the network effect onto a higher level. Previously, we had XMPP as the only open federated protocol. Matrix was introduced in 2014 as a modern alternative. This fragmented the ecosystem, since users of Matrix cannot communicate with users of XMPP straightforwardly, creating a different sort of a network effect.
Lavina is an attempt to solve that problem. It is not a new protocol, but it's a way to make other protocols be interoperable between each other. Users of a Lavina service should be able to connect to their service using a client of any supported protocols, their service should federate with other services using any supported protocols.
Products should compete with each other on basis of technical merit and not network effects.
#### Scaling down, up and wide
Federated services are being run by communities and companies of various sizes and scales this ranges from tiniest single-person servers to large communities with thousands of active users to global corporations with billions of concurrent connections.
1. Scale down we should support running the software on low-level hardware such as Raspberry Pi or single-core VPS. This is important for costs saving of smaller sized instances.
2. Scale up we should utilize available resources of more powerful machines in a fair and efficient manner. This includes multiple CPUs, increased RAM and storage, network bandwidth.
3. Scale wide the most complex property to implement, we should support running the service as a distributed cluster of instances, which efficiently schedule the load and balance connections across themselves.
Clustered setup may additionally provide support for data replicas for additional reliability and improved read scaling. It should also consider data locality so as not to introduce additional network hops, which may result in multiple cross-AZ/DC data transfers in scope of a single request.

1
rust-toolchain Normal file
View File

@ -0,0 +1 @@
nightly-2023-10-06

1
rustfmt.toml Normal file
View File

@ -0,0 +1 @@
max_width = 120

View File

@ -1,254 +0,0 @@
//! Domain of chat participants.
//!
//! Player is a single user account, which is used to participate in chats,
//! including sending messages, receiving messaged, retrieving history and running privileged actions.
//! A player corresponds to a single user account. Usually a person has only one account,
//! but it is possible to have multiple accounts for one person and therefore multiple player entities.
//!
//! A player actor is a serial handler of commands from a single player. It is preferable to run all per-player validations in the player actor,
//! so that they don't overload the room actor.
use std::{
collections::HashMap,
sync::{Arc, RwLock},
};
use prometheus::{IntGauge, Registry as MetricsRegistry};
use tokio::{
sync::mpsc::{channel, Receiver, Sender},
sync::oneshot::{channel as oneshot, Sender as OneshotSender},
task::JoinHandle,
};
use crate::{
core::room::{RoomId, RoomRegistry},
prelude::*,
util::table::{AnonTable, Key as AnonKey},
};
/// Opaque player identifier.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PlayerId(pub ByteVec);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ConnectionId(pub AnonKey);
pub struct PlayerConnection {
pub connection_id: ConnectionId,
pub receiver: Receiver<Updates>,
player_handle: PlayerHandle,
}
impl PlayerConnection {
pub async fn send_message(&mut self, room_id: RoomId, body: String) {
self.player_handle
.send_message(room_id, self.connection_id.clone(), body)
.await
}
pub async fn join_room(&mut self, room_id: RoomId) {
self.player_handle.join_room(room_id).await
}
}
/// Handle to a player actor.
#[derive(Clone)]
pub struct PlayerHandle {
tx: Sender<PlayerCommand>,
}
impl PlayerHandle {
pub async fn subscribe(&mut self) -> PlayerConnection {
let (sender, receiver) = channel(32);
let (promise, deferred) = oneshot();
self.tx
.send(PlayerCommand::AddSocket { sender, promise })
.await;
let connection_id = deferred.await.unwrap();
PlayerConnection {
connection_id,
player_handle: self.clone(),
receiver,
}
}
pub async fn send_message(
&mut self,
room_id: RoomId,
connection_id: ConnectionId,
body: String,
) {
self.tx
.send(PlayerCommand::SendMessage {
room_id,
connection_id,
body,
})
.await;
}
pub async fn join_room(&mut self, room_id: RoomId) {
self.tx.send(PlayerCommand::JoinRoom { room_id }).await;
}
pub async fn receive_message(
&mut self,
room_id: RoomId,
author: PlayerId,
connection_id: ConnectionId,
body: String,
) {
self.tx
.send(PlayerCommand::IncomingMessage {
room_id,
author,
connection_id,
body,
})
.await;
}
}
/// Player update event type which is sent to a connection handler.
pub enum Updates {
RoomJoined {
room_id: RoomId,
},
NewMessage {
author_id: PlayerId,
connection_id: ConnectionId,
room_id: RoomId,
body: String,
},
}
enum PlayerCommand {
AddSocket {
sender: Sender<Updates>,
promise: OneshotSender<ConnectionId>,
},
JoinRoom {
room_id: RoomId,
},
SendMessage {
room_id: RoomId,
connection_id: ConnectionId,
body: String,
},
IncomingMessage {
room_id: RoomId,
connection_id: ConnectionId,
author: PlayerId,
body: String,
},
}
/// Handle to a player registry — a shared data structure containing information about players.
#[derive(Clone)]
pub struct PlayerRegistry(Arc<RwLock<PlayerRegistryInner>>);
impl PlayerRegistry {
pub fn empty(
room_registry: RoomRegistry,
metrics: &mut MetricsRegistry,
) -> Result<PlayerRegistry> {
let metric_active_players =
IntGauge::new("chat_players_active", "Number of alive player actors")?;
metrics.register(Box::new(metric_active_players.clone()))?;
let inner = PlayerRegistryInner {
room_registry,
players: HashMap::new(),
metric_active_players,
};
Ok(PlayerRegistry(Arc::new(RwLock::new(inner))))
}
pub async fn get_or_create_player(&mut self, id: PlayerId) -> PlayerHandle {
let player = Player {
sockets: AnonTable::new(),
};
let mut inner = self.0.write().unwrap();
if let Some((handle, _)) = inner.players.get(&id) {
handle.clone()
} else {
let (handle, fiber) = player.launch(id.clone(), inner.room_registry.clone());
inner.players.insert(id, (handle.clone(), fiber));
inner.metric_active_players.inc();
handle
}
}
pub async fn connect_to_player(&mut self, id: PlayerId) -> PlayerConnection {
let mut player_handle = self.get_or_create_player(id).await;
player_handle.subscribe().await
}
}
/// The player registry state representation.
struct PlayerRegistryInner {
room_registry: RoomRegistry,
players: HashMap<PlayerId, (PlayerHandle, JoinHandle<Player>)>,
metric_active_players: IntGauge,
}
/// Player actor inner state representation.
struct Player {
sockets: AnonTable<Sender<Updates>>,
}
impl Player {
fn launch(
mut self,
player_id: PlayerId,
mut rooms: RoomRegistry,
) -> (PlayerHandle, JoinHandle<Player>) {
let (tx, mut rx) = channel(32);
let handle = PlayerHandle { tx };
let handle_clone = handle.clone();
let fiber = tokio::task::spawn(async move {
while let Some(cmd) = rx.recv().await {
match cmd {
PlayerCommand::AddSocket { sender, promise } => {
let connection_id = self.sockets.insert(sender);
promise.send(ConnectionId(connection_id));
}
PlayerCommand::JoinRoom { room_id } => {
let mut room = rooms.get_or_create_room(room_id);
room.subscribe(player_id.clone(), handle.clone()).await;
}
PlayerCommand::SendMessage {
room_id,
connection_id,
body,
} => {
let room = rooms.get_room(room_id);
match room {
Some(mut room) => {
room.send_message(player_id.clone(), connection_id, body)
.await;
}
None => {
tracing::info!("no room found");
}
}
}
PlayerCommand::IncomingMessage {
room_id,
author,
connection_id,
body,
} => {
tracing::info!("Handling incoming message");
for socket in &self.sockets {
log::info!("Send message to socket");
socket
.send(Updates::NewMessage {
author_id: author.clone(),
connection_id: connection_id.clone(),
room_id: room_id.clone(),
body: body.clone(),
})
.await;
}
}
}
}
self
});
(handle_clone, fiber)
}
}

View File

@ -1,148 +0,0 @@
//! Domain of rooms — chats with multiple participants.
use std::{
collections::HashMap,
hash::Hash,
sync::{Arc, RwLock},
};
use prometheus::{IntGauge, Registry as MetricRegistry};
use tokio::sync::mpsc::{channel, Sender};
use crate::{
core::player::{PlayerHandle, PlayerId},
prelude::*,
};
use super::player::ConnectionId;
/// Opaque room id
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RoomId(pub ByteVec);
/// Shared datastructure for storing metadata about rooms.
#[derive(Clone)]
pub struct RoomRegistry(Arc<RwLock<RoomRegistryInner>>);
impl RoomRegistry {
pub fn empty(metrics: &mut MetricRegistry) -> Result<RoomRegistry> {
let metric_active_rooms =
IntGauge::new("chat_rooms_active", "Number of alive room actors")?;
metrics.register(Box::new(metric_active_rooms.clone()))?;
let inner = RoomRegistryInner {
rooms: HashMap::new(),
metric_active_rooms,
};
Ok(RoomRegistry(Arc::new(RwLock::new(inner))))
}
pub fn get_or_create_room(&mut self, room_id: RoomId) -> RoomHandle {
let room = Room {
subscriptions: HashMap::new(),
};
let mut inner = self.0.write().unwrap();
if let Some((room_handle, _)) = inner.rooms.get(&room_id) {
room_handle.clone()
} else {
let (room_handle, fiber) = room.launch(room_id.clone());
inner.rooms.insert(room_id, (room_handle.clone(), fiber));
inner.metric_active_rooms.inc();
room_handle
}
}
pub fn get_room(&self, room_id: RoomId) -> Option<RoomHandle> {
let inner = self.0.read().unwrap();
let res = inner.rooms.get(&room_id);
res.map(|r| r.0.clone())
}
}
struct RoomRegistryInner {
rooms: HashMap<RoomId, (RoomHandle, JoinHandle<Room>)>,
metric_active_rooms: IntGauge,
}
#[derive(Clone)]
pub struct RoomHandle {
tx: Sender<RoomCommand>,
}
impl RoomHandle {
pub async fn subscribe(&mut self, player_id: PlayerId, player: PlayerHandle) {
match self
.tx
.send(RoomCommand::AddSubscriber { player_id, player })
.await
{
Ok(_) => {}
Err(_) => {
tracing::error!("Room mailbox is closed unexpectedly");
}
};
}
pub async fn send_message(
&mut self,
player_id: PlayerId,
connection_id: ConnectionId,
body: String,
) {
self.tx
.send(RoomCommand::SendMessage {
player_id,
connection_id,
body,
})
.await;
}
}
enum RoomCommand {
AddSubscriber {
player_id: PlayerId,
player: PlayerHandle,
},
SendMessage {
player_id: PlayerId,
connection_id: ConnectionId,
body: String,
},
}
struct Room {
subscriptions: HashMap<PlayerId, PlayerHandle>,
}
impl Room {
fn launch(mut self, room_id: RoomId) -> (RoomHandle, JoinHandle<Room>) {
let (tx, mut rx) = channel(32);
let fiber = tokio::task::spawn(async move {
tracing::info!("Starting room fiber");
while let Some(a) = rx.recv().await {
match a {
RoomCommand::AddSubscriber { player_id, player } => {
tracing::info!("Adding a subscriber to room");
self.subscriptions.insert(player_id, player);
}
RoomCommand::SendMessage {
player_id,
connection_id,
body,
} => {
tracing::info!("Adding a message to room");
for (_, sub) in &mut self.subscriptions {
log::info!("Sending a message from room to player");
sub.receive_message(
room_id.clone(),
player_id.clone(),
connection_id.clone(),
body.clone(),
)
.await;
}
}
}
}
tracing::info!("Stopping room fiber");
self
});
(RoomHandle { tx }, fiber)
}
}

197
src/http.rs Normal file
View File

@ -0,0 +1,197 @@
use std::convert::Infallible;
use std::net::SocketAddr;
use futures_util::FutureExt;
use http_body_util::{BodyExt, Full};
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, StatusCode};
use prometheus::{Encoder, Registry as MetricsRegistry, TextEncoder};
use serde::{Deserialize, Serialize};
use tokio::net::TcpListener;
use lavina_core::prelude::*;
use lavina_core::repo::Storage;
use lavina_core::room::RoomRegistry;
use lavina_core::terminator::Terminator;
use mgmt_api::*;
type HttpResult<T> = std::result::Result<T, Infallible>;
#[derive(Deserialize, Debug)]
pub struct ServerConfig {
pub listen_on: SocketAddr,
}
pub async fn launch(
config: ServerConfig,
metrics: MetricsRegistry,
rooms: RoomRegistry,
storage: Storage,
) -> Result<Terminator> {
log::info!("Starting the http service");
let listener = TcpListener::bind(config.listen_on).await?;
log::debug!("Listener started");
let terminator = Terminator::spawn(|rx| main_loop(listener, metrics, rooms, storage, rx.map(|_| ())));
Ok(terminator)
}
async fn main_loop(
listener: TcpListener,
metrics: MetricsRegistry,
rooms: RoomRegistry,
storage: Storage,
termination: impl Future<Output = ()>,
) -> Result<()> {
pin!(termination);
loop {
select! {
biased;
_ = &mut termination => break,
result = listener.accept() => {
let (stream, _) = result?;
let metrics = metrics.clone();
let rooms = rooms.clone();
let storage = storage.clone();
tokio::task::spawn(async move {
let registry = metrics.clone();
let rooms = rooms.clone();
let storage = storage.clone();
let server = http1::Builder::new().serve_connection(stream, service_fn(move |r| route(registry.clone(), rooms.clone(), storage.clone(), r)));
if let Err(err) = server.await {
tracing::error!("Error serving connection: {:?}", err);
}
});
},
}
}
log::info!("Terminating the http service");
Ok(())
}
async fn route(
registry: MetricsRegistry,
rooms: RoomRegistry,
storage: Storage,
request: Request<hyper::body::Incoming>,
) -> HttpResult<Response<Full<Bytes>>> {
let res = match (request.method(), request.uri().path()) {
(&Method::GET, "/metrics") => endpoint_metrics(registry),
(&Method::GET, "/rooms") => endpoint_rooms(rooms).await,
(&Method::POST, paths::CREATE_PLAYER) => endpoint_create_player(request, storage).await.or5xx(),
(&Method::POST, paths::SET_PASSWORD) => endpoint_set_password(request, storage).await.or5xx(),
_ => not_found(),
};
Ok(res)
}
fn endpoint_metrics(registry: MetricsRegistry) -> Response<Full<Bytes>> {
let mf = registry.gather();
let mut buffer = vec![];
TextEncoder.encode(&mf, &mut buffer).expect("write to vec cannot fail");
Response::new(Full::new(Bytes::from(buffer)))
}
async fn endpoint_rooms(rooms: RoomRegistry) -> Response<Full<Bytes>> {
// TODO introduce management API types independent from core-domain types
// TODO remove `Serialize` implementations from all core-domain types
let room_list = rooms.get_all_rooms().await.to_body();
Response::new(room_list)
}
async fn endpoint_create_player(
request: Request<hyper::body::Incoming>,
mut storage: Storage,
) -> Result<Response<Full<Bytes>>> {
let str = request.collect().await?.to_bytes();
let Ok(res) = serde_json::from_slice::<CreatePlayerRequest>(&str[..]) else {
let payload = ErrorResponse {
code: errors::MALFORMED_REQUEST,
message: "The request payload contains incorrect JSON value",
}
.to_body();
let mut response = Response::new(payload);
*response.status_mut() = StatusCode::BAD_REQUEST;
return Ok(response);
};
storage.create_user(&res.name).await?;
log::info!("Player {} created", res.name);
let mut response = Response::new(Full::<Bytes>::default());
*response.status_mut() = StatusCode::CREATED;
Ok(response)
}
async fn endpoint_set_password(
request: Request<hyper::body::Incoming>,
mut storage: Storage,
) -> Result<Response<Full<Bytes>>> {
let str = request.collect().await?.to_bytes();
let Ok(res) = serde_json::from_slice::<ChangePasswordRequest>(&str[..]) else {
let payload = ErrorResponse {
code: errors::MALFORMED_REQUEST,
message: "The request payload contains incorrect JSON value",
}
.to_body();
let mut response = Response::new(payload);
*response.status_mut() = StatusCode::BAD_REQUEST;
return Ok(response);
};
let Some(_) = storage.set_password(&res.player_name, &res.password).await? else {
let payload = ErrorResponse {
code: errors::PLAYER_NOT_FOUND,
message: "No such player exists",
}
.to_body();
let mut response = Response::new(payload);
*response.status_mut() = StatusCode::UNPROCESSABLE_ENTITY;
return Ok(response);
};
log::info!("Password changed for player {}", res.player_name);
let mut response = Response::new(Full::<Bytes>::default());
*response.status_mut() = StatusCode::NO_CONTENT;
Ok(response)
}
pub fn not_found() -> Response<Full<Bytes>> {
let payload = ErrorResponse {
code: errors::INVALID_PATH,
message: "The path does not exist",
}
.to_body();
let mut response = Response::new(payload);
*response.status_mut() = StatusCode::NOT_FOUND;
response
}
trait Or5xx {
fn or5xx(self) -> Response<Full<Bytes>>;
}
impl Or5xx for Result<Response<Full<Bytes>>> {
fn or5xx(self) -> Response<Full<Bytes>> {
match self {
Ok(e) => e,
Err(e) => {
let mut response = Response::new(Full::new(e.to_string().into()));
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
response
}
}
}
}
trait ToBody {
fn to_body(&self) -> Full<Bytes>;
}
impl<T> ToBody for T
where
T: Serialize,
{
fn to_body(&self) -> Full<Bytes> {
let mut buffer = vec![];
serde_json::to_writer(&mut buffer, self).expect("unexpected fail when writing to vec");
Full::new(Bytes::from(buffer))
}
}

View File

@ -1,28 +1,36 @@
mod core;
mod prelude;
mod projections;
mod protos;
mod util;
mod http;
use std::future::Future;
use std::path::Path;
use clap::Parser;
use figment::providers::Format;
use figment::{providers::Toml, Figment};
use prometheus::Registry as MetricsRegistry;
use serde::Deserialize;
use crate::core::player::PlayerRegistry;
use crate::core::room::RoomRegistry;
use crate::prelude::*;
use lavina_core::player::PlayerRegistry;
use lavina_core::prelude::*;
use lavina_core::repo::Storage;
use lavina_core::room::RoomRegistry;
#[derive(Deserialize, Debug)]
struct ServerConfig {
telemetry: util::telemetry::ServerConfig,
irc: projections::irc::ServerConfig,
telemetry: http::ServerConfig,
irc: projection_irc::ServerConfig,
xmpp: projection_xmpp::ServerConfig,
storage: lavina_core::repo::StorageConfig,
}
#[derive(Parser)]
struct CliArgs {
#[arg(long)]
config: Box<Path>,
}
fn load_config() -> Result<ServerConfig> {
let raw_config = Figment::new().merge(Toml::file("config.toml"));
let args = CliArgs::parse();
let raw_config = Figment::from(Toml::file(args.config));
let config: ServerConfig = raw_config.extract()?;
Ok(config)
}
@ -38,23 +46,50 @@ async fn main() -> Result<()> {
let ServerConfig {
telemetry: telemetry_config,
irc: irc_config,
xmpp: xmpp_config,
storage: storage_config,
} = config;
let mut metrics = MetricsRegistry::new();
let rooms = RoomRegistry::empty(&mut metrics)?;
let players = PlayerRegistry::empty(rooms.clone(), &mut metrics)?;
let telemetry_terminator = util::telemetry::launch(telemetry_config, metrics.clone()).await?;
let irc = projections::irc::launch(irc_config, players, metrics.clone()).await?;
let storage = Storage::open(storage_config).await?;
let rooms = RoomRegistry::new(&mut metrics, storage.clone())?;
let mut players = PlayerRegistry::empty(rooms.clone(), &mut metrics)?;
let telemetry_terminator = http::launch(telemetry_config, metrics.clone(), rooms.clone(), storage.clone()).await?;
let irc = projection_irc::launch(
irc_config,
players.clone(),
rooms.clone(),
metrics.clone(),
storage.clone(),
)
.await?;
let xmpp = projection_xmpp::launch(xmpp_config, players.clone(), rooms.clone(), metrics.clone(), storage.clone()).await?;
tracing::info!("Started");
sleep.await;
tracing::info!("Begin shutdown");
xmpp.terminate().await?;
irc.terminate().await?;
telemetry_terminator.terminate().await?;
players.shutdown_all().await?;
drop(players);
drop(rooms);
storage.close().await?;
tracing::info!("Shutdown complete");
Ok(())
}
#[cfg(windows)]
fn ctrl_c() -> Result<impl Future<Output = ()>> {
use tokio::signal::windows::*;
let chan = ctrl_c()?;
async fn recv(mut chan: CtrlC) {
let _ = chan.recv().await;
}
Ok(recv(chan))
}
#[cfg(unix)]
fn ctrl_c() -> Result<impl Future<Output = ()>> {
use tokio::signal::unix::*;
let chan = signal(SignalKind::interrupt())?;

View File

@ -1,389 +0,0 @@
use std::net::SocketAddr;
use prometheus::{IntCounter, IntGauge, Registry as MetricsRegistry};
use serde::Deserialize;
use tokio::io::{AsyncBufReadExt, AsyncWrite, AsyncWriteExt, BufReader, BufWriter};
use tokio::net::tcp::{ReadHalf, WriteHalf};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::oneshot::channel;
use crate::core::player::{
ConnectionId, PlayerConnection, PlayerHandle, PlayerId, PlayerRegistry, Updates,
};
use crate::core::room::RoomId;
use crate::prelude::*;
use crate::protos::irc::client::{client_message, ClientMessage};
use crate::protos::irc::server::{ServerMessage, ServerMessageBody};
use crate::protos::irc::{Chan, Recipient};
use crate::util::Terminator;
#[derive(Deserialize, Debug, Clone)]
pub struct ServerConfig {
pub listen_on: SocketAddr,
pub server_name: String,
}
#[derive(Debug)]
struct RegisteredUser {
nickname: Vec<u8>,
username: Vec<u8>,
realname: Vec<u8>,
}
async fn handle_socket(
config: ServerConfig,
mut stream: TcpStream,
socket_addr: SocketAddr,
mut players: PlayerRegistry,
) -> Result<()> {
let (reader, writer) = stream.split();
let mut reader: BufReader<ReadHalf> = BufReader::new(reader);
let mut writer = BufWriter::new(writer);
ServerMessage {
tags: vec![],
sender: Some(config.server_name.as_bytes().to_vec()),
body: ServerMessageBody::Notice {
first_target: b"*".to_vec(),
rest_targets: vec![],
text: b"Welcome to my server!".to_vec(),
},
}
.write_async(&mut writer)
.await?;
writer.flush().await?;
let registered_user: Result<RegisteredUser> =
handle_registration(&mut reader, &mut writer).await;
match registered_user {
Ok(user) => {
handle_registered_socket(config, socket_addr, players, &mut reader, &mut writer, user)
.await?;
}
Err(_) => {}
}
stream.shutdown().await?;
Ok(())
}
async fn handle_registration<'a>(
reader: &mut BufReader<ReadHalf<'a>>,
writer: &mut BufWriter<WriteHalf<'a>>,
) -> Result<RegisteredUser> {
let mut buffer = vec![];
let mut future_nickname: Option<Vec<u8>> = None;
let mut future_username: Option<(Vec<u8>, Vec<u8>)> = None;
loop {
let res = reader.read_until(b'\n', &mut buffer).await;
match res {
Ok(len) => {
if len == 0 {
log::info!("Terminating socket");
break Err(anyhow::Error::msg("EOF"));
}
}
Err(err) => {
log::warn!("Failed to read from socket: {err}");
break Err(err.into());
}
}
let parsed = client_message(&buffer[..]);
match parsed {
Ok((rest, msg)) => {
log::info!("Incoming IRC message: {msg:?}");
match msg {
ClientMessage::Nick { nickname } => {
if let Some((username, realname)) = future_username {
break Ok(RegisteredUser {
nickname,
username,
realname,
});
} else {
future_nickname = Some(nickname);
}
}
ClientMessage::User { username, realname } => {
if let Some(nickname) = future_nickname {
break Ok(RegisteredUser {
nickname,
username,
realname,
});
} else {
future_username = Some((username, realname));
}
}
_ => {}
}
}
Err(err) => {
log::warn!("Failed to parse IRC message: {err}");
}
}
buffer.clear();
}
}
async fn handle_registered_socket<'a>(
config: ServerConfig,
socket_addr: SocketAddr,
mut players: PlayerRegistry,
reader: &mut BufReader<ReadHalf<'a>>,
writer: &mut BufWriter<WriteHalf<'a>>,
user: RegisteredUser,
) -> Result<()> {
let mut buffer = vec![];
log::info!("Handling registered user: {user:?}");
let mut connection = players
.connect_to_player(PlayerId(user.nickname.clone()))
.await;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.as_bytes().to_vec()),
body: ServerMessageBody::N001Welcome {
client: user.nickname.clone(),
text: b"Welcome to Kek Server".to_vec(),
},
}
.write_async(writer)
.await?;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.as_bytes().to_vec()),
body: ServerMessageBody::N002YourHost {
client: user.nickname.clone(),
text: b"Welcome to Kek Server".to_vec(),
},
}
.write_async(writer)
.await?;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.as_bytes().to_vec()),
body: ServerMessageBody::N003Created {
client: user.nickname.clone(),
text: b"Welcome to Kek Server".to_vec(),
},
}
.write_async(writer)
.await?;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.as_bytes().to_vec()),
body: ServerMessageBody::N004MyInfo {
client: user.nickname.clone(),
hostname: config.server_name.as_bytes().to_vec(),
softname: b"kek-0.1.alpha.3".to_vec(),
},
}
.write_async(writer)
.await?;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.as_bytes().to_vec()),
body: ServerMessageBody::N005ISupport {
client: user.nickname.clone(),
params: b"CHANTYPES=#".to_vec(),
},
}
.write_async(writer)
.await?;
writer.flush().await?;
loop {
select! {
biased;
len = reader.read_until(b'\n', &mut buffer) => {
let len = len?;
let len = if len == 0 {
log::info!("EOF, Terminating socket");
break;
} else {
len
};
handle_incoming_message(&buffer[0..len], &config, &user, &mut connection, writer).await?;
buffer.clear();
},
update = connection.receiver.recv() => {
match update.unwrap() {
Updates::RoomJoined { room_id } => {},
Updates::NewMessage { author_id, connection_id, room_id, body } => {
if connection.connection_id != connection_id {
ServerMessage {
tags: vec![],
sender: Some(author_id.0.clone()),
body: ServerMessageBody::PrivateMessage { target: Recipient::Chan(Chan::Global(room_id.0)), body: body.as_bytes().to_vec() }
}.write_async(writer).await?;
writer.flush().await?
}
},
}
}
}
}
Ok(())
}
async fn handle_incoming_message(
buffer: &[u8],
config: &ServerConfig,
user: &RegisteredUser,
user_handle: &mut PlayerConnection,
writer: &mut (impl AsyncWrite + Unpin),
) -> Result<()> {
let parsed = client_message(buffer);
match parsed {
Ok((rest, msg)) => match msg {
ClientMessage::Ping { token } => {
ServerMessage {
tags: vec![],
sender: None,
body: ServerMessageBody::Pong {
from: config.server_name.as_bytes().to_vec(),
token,
},
}
.write_async(writer)
.await?;
writer.flush().await?;
}
ClientMessage::Join(ref chan) => {
handle_join(&config, &user, user_handle, chan, writer).await?;
}
ClientMessage::PrivateMessage { recipient, body } => match recipient {
Recipient::Chan(Chan::Global(room)) => match String::from_utf8(body) {
Ok(body) => {
user_handle
.send_message(RoomId(room.clone()), body.clone())
.await
}
Err(err) => log::warn!("failed to parse incoming message: {err}"),
},
_ => log::warn!("Unsupported target type"),
},
_ => {}
},
Err(err) => {
log::warn!("Failed to parse IRC message: {err}");
}
}
Ok(())
}
async fn handle_join(
config: &ServerConfig,
user: &RegisteredUser,
user_handle: &mut PlayerConnection,
chan: &Chan,
writer: &mut (impl AsyncWrite + Unpin),
) -> Result<()> {
match chan {
Chan::Global(ref room) => {
user_handle.join_room(RoomId(room.clone())).await;
ServerMessage {
tags: vec![],
sender: Some(user.nickname.clone()),
body: ServerMessageBody::Join(chan.clone()),
}
.write_async(writer)
.await?;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.as_bytes().to_vec()),
body: ServerMessageBody::N332Topic {
client: user.nickname.clone(),
chat: chan.clone(),
topic: b"chan topic lol".to_vec(),
},
}
.write_async(writer)
.await?;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.as_bytes().to_vec()),
body: ServerMessageBody::N353NamesReply {
client: user.nickname.clone(),
chan: chan.clone(),
members: user.nickname.clone(),
},
}
.write_async(writer)
.await?;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.as_bytes().to_vec()),
body: ServerMessageBody::N366NamesReplyEnd {
client: user.nickname.clone(),
chan: chan.clone(),
},
}
.write_async(writer)
.await?;
writer.flush().await?;
}
Chan::Local(_) => {}
};
Ok(())
}
pub async fn launch(
config: ServerConfig,
players: PlayerRegistry,
metrics: MetricsRegistry,
) -> Result<Terminator> {
log::info!("Starting IRC projection");
let (signal, mut rx) = channel();
let current_connections =
IntGauge::new("irc_current_connections", "Open and alive TCP connections")?;
let total_connections = IntCounter::new(
"irc_total_connections",
"Total number of opened connections",
)?;
metrics.register(Box::new(current_connections.clone()))?;
metrics.register(Box::new(total_connections.clone()))?;
let listener = TcpListener::bind(config.listen_on).await?;
log::debug!("Listener started");
let handle = tokio::task::spawn(async move {
loop {
select! {
biased;
_ = &mut rx => break,
new_conn = listener.accept() => {
match new_conn {
Ok((stream, socket_addr)) => {
let config = config.clone();
total_connections.inc();
current_connections.inc();
log::debug!("Incoming connection from {socket_addr}");
let players_clone = players.clone();
let current_connections_clone = current_connections.clone();
let handle = tokio::task::spawn(async move {
match handle_socket(config, stream, socket_addr, players_clone).await {
Ok(_) => log::info!("Connection terminated"),
Err(err) => log::warn!("Connection failed: {err}"),
}
current_connections_clone.dec();
});
},
Err(err) => log::warn!("Failed to accept new connection: {err}"),
}
},
}
}
log::info!("Stopping IRC projection");
Ok(())
});
let terminator = Terminator::from_raw(signal, handle);
Ok(terminator)
}

View File

@ -1,2 +0,0 @@
//! Protocol projections — implementations of public APIs.
pub mod irc;

View File

@ -1,225 +0,0 @@
use super::*;
/// Client-to-server command.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ClientMessage {
/// CAP. Capability-related commands.
Capability { subcommand: CapabilitySubcommand },
/// PING <token>
Ping { token: ByteVec },
/// PONG <token>
Pong { token: ByteVec },
/// NICK <nickname>
Nick { nickname: ByteVec },
/// USER <username> 0 * :<realname>
User {
username: ByteVec,
realname: ByteVec,
},
/// JOIN <chan>
Join(Chan),
/// PRIVMSG <target> :<msg>
PrivateMessage { recipient: Recipient, body: ByteVec },
/// QUIT :<reason>
Quit { reason: ByteVec },
}
pub fn client_message(input: &[u8]) -> IResult<&[u8], ClientMessage> {
alt((
client_message_capability,
client_message_ping,
client_message_pong,
client_message_nick,
client_message_user,
client_message_join,
client_message_privmsg,
client_message_quit,
))(input)
}
fn client_message_capability(input: &[u8]) -> IResult<&[u8], ClientMessage> {
let (input, _) = tag("CAP ")(input)?;
let (input, subcommand) = capability_subcommand(input)?;
Ok((input, ClientMessage::Capability { subcommand }))
}
fn client_message_ping(input: &[u8]) -> IResult<&[u8], ClientMessage> {
let (input, _) = tag("PING ")(input)?;
let (input, token) = token(input)?;
Ok((
input,
ClientMessage::Ping {
token: token.to_owned(),
},
))
}
fn client_message_pong(input: &[u8]) -> IResult<&[u8], ClientMessage> {
let (input, _) = tag("PONG ")(input)?;
let (input, token) = token(input)?;
Ok((
input,
ClientMessage::Pong {
token: token.to_owned(),
},
))
}
fn client_message_nick(input: &[u8]) -> IResult<&[u8], ClientMessage> {
let (input, _) = tag("NICK ")(input)?;
let (input, nickname) = receiver(input)?;
Ok((
input,
ClientMessage::Nick {
nickname: nickname.to_owned(),
},
))
}
fn client_message_user(input: &[u8]) -> IResult<&[u8], ClientMessage> {
let (input, _) = tag("USER ")(input)?;
let (input, username) = receiver(input)?;
let (input, _) = tag(" ")(input)?;
let (input, _) = take(1_usize)(input)?; // 0 in spec, but any in fact
let (input, _) = tag(" * :")(input)?;
let (input, realname) = token(input)?;
Ok((
input,
ClientMessage::User {
username: username.to_owned(),
realname: realname.to_owned(),
},
))
}
fn client_message_join(input: &[u8]) -> IResult<&[u8], ClientMessage> {
let (input, _) = tag("JOIN ")(input)?;
let (input, chan) = chan(input)?;
Ok((input, ClientMessage::Join(chan)))
}
fn client_message_privmsg(input: &[u8]) -> IResult<&[u8], ClientMessage> {
let (input, _) = tag("PRIVMSG ")(input)?;
let (input, recipient) = recipient(input)?;
let (input, _) = tag(" :")(input)?;
let (input, body) = token(input)?;
let body = body.to_vec();
Ok((input, ClientMessage::PrivateMessage { recipient, body }))
}
fn client_message_quit(input: &[u8]) -> IResult<&[u8], ClientMessage> {
let (input, _) = tag("QUIT :")(input)?;
let (input, reason) = token(input)?;
Ok((
input,
ClientMessage::Quit {
reason: reason.to_vec(),
},
))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CapabilitySubcommand {
/// CAP LS {code}
List { code: [u8; 3] },
/// CAP END
End,
}
fn capability_subcommand(input: &[u8]) -> IResult<&[u8], CapabilitySubcommand> {
alt((capability_subcommand_ls, capability_subcommand_end))(input)
}
fn capability_subcommand_ls(input: &[u8]) -> IResult<&[u8], CapabilitySubcommand> {
let (input, _) = tag("LS ")(input)?;
let (input, code) = take(3usize)(input)?;
Ok((
input,
CapabilitySubcommand::List {
code: code.try_into().unwrap(),
},
))
}
fn capability_subcommand_end(input: &[u8]) -> IResult<&[u8], CapabilitySubcommand> {
let (input, _) = tag("END")(input)?;
Ok((input, CapabilitySubcommand::End))
}
#[cfg(test)]
mod test {
use assert_matches::*;
use super::*;
#[test]
fn test_client_message_cap_ls() {
let input = b"CAP LS 302";
let expected = ClientMessage::Capability {
subcommand: CapabilitySubcommand::List { code: *b"302" },
};
let result = client_message(input);
assert_matches!(result, Ok((_, result)) => assert_eq!(expected, result));
}
#[test]
fn test_client_message_cap_end() {
let input = b"CAP END";
let expected = ClientMessage::Capability {
subcommand: CapabilitySubcommand::End,
};
let result = client_message(input);
assert_matches!(result, Ok((_, result)) => assert_eq!(expected, result));
}
#[test]
fn test_client_message_ping() {
let input = b"PING 1337";
let expected = ClientMessage::Ping {
token: b"1337".to_vec(),
};
let result = client_message(input);
assert_matches!(result, Ok((_, result)) => assert_eq!(expected, result));
}
#[test]
fn test_client_message_pong() {
let input = b"PONG 1337";
let expected = ClientMessage::Pong {
token: b"1337".to_vec(),
};
let result = client_message(input);
assert_matches!(result, Ok((_, result)) => assert_eq!(expected, result));
}
#[test]
fn test_client_message_nick() {
let input = b"NICK SomeNick";
let expected = ClientMessage::Nick {
nickname: b"SomeNick".to_vec(),
};
let result = client_message(input);
assert_matches!(result, Ok((_, result)) => assert_eq!(expected, result));
}
#[test]
fn test_client_message_user() {
let input = b"USER SomeNick 8 * :Real Name";
let expected = ClientMessage::User {
username: b"SomeNick".to_vec(),
realname: b"Real Name".to_vec(),
};
let result = client_message(input);
assert_matches!(result, Ok((_, result)) => assert_eq!(expected, result));
}
}

View File

@ -1,315 +0,0 @@
use tokio::io::AsyncWrite;
use tokio::io::AsyncWriteExt;
use super::*;
/// Server-to-client message.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ServerMessage {
/// Optional tags section, prefixed with `@`
pub tags: Vec<Tag>,
/// Optional server name, prefixed with `:`.
pub sender: Option<ByteVec>,
pub body: ServerMessageBody,
}
impl ServerMessage {
pub async fn write_async(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
match &self.sender {
Some(ref sender) => {
writer.write_all(b":").await?;
writer.write_all(sender.as_slice()).await?;
writer.write_all(b" ").await?;
}
None => {}
}
self.body.write_async(writer).await?;
writer.write_all(b"\n").await?;
Ok(())
}
}
pub fn server_message(input: &[u8]) -> IResult<&[u8], ServerMessage> {
let (input, command) = server_message_body(input)?;
let (input, _) = tag(b"\n")(input)?;
let message = ServerMessage {
tags: vec![],
sender: None,
body: command,
};
Ok((input, message))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ServerMessageBody {
Notice {
first_target: ByteVec,
rest_targets: Vec<ByteVec>,
text: ByteVec,
},
Ping {
token: ByteVec,
},
Pong {
from: ByteVec,
token: ByteVec,
},
PrivateMessage {
target: Recipient,
body: ByteVec,
},
Join(Chan),
N001Welcome {
client: ByteVec,
text: ByteVec,
},
N002YourHost {
client: ByteVec,
text: ByteVec,
},
N003Created {
client: ByteVec,
text: ByteVec,
},
N004MyInfo {
client: ByteVec,
hostname: ByteVec,
softname: ByteVec,
// TODO user modes, channel modes, channel modes with a parameter
},
N005ISupport {
client: ByteVec,
params: ByteVec, // TODO make this a datatype
},
N332Topic {
client: ByteVec,
chat: Chan,
topic: ByteVec,
},
N353NamesReply {
client: ByteVec,
chan: Chan,
members: ByteVec, // TODO make this a non-empty list with prefixes
},
N366NamesReplyEnd {
client: ByteVec,
chan: Chan,
},
}
impl ServerMessageBody {
pub async fn write_async(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
match self {
ServerMessageBody::Notice {
first_target,
rest_targets,
text,
} => {
writer.write_all(b"NOTICE ").await?;
writer.write_all(&first_target).await?;
writer.write_all(b" :").await?;
writer.write_all(&text).await?;
}
ServerMessageBody::Ping { token } => {
writer.write_all(b"PING ").await?;
writer.write_all(&token).await?;
}
ServerMessageBody::Pong { from, token } => {
writer.write_all(b"PONG ").await?;
writer.write_all(&from).await?;
writer.write_all(b" :").await?;
writer.write_all(&token).await?;
}
ServerMessageBody::PrivateMessage { target, body } => {
writer.write_all(b"PRIVMSG ").await?;
target.write_async(writer).await?;
writer.write_all(b" :").await?;
writer.write_all(&body).await?;
}
ServerMessageBody::Join(chan) => {
writer.write_all(b"JOIN ").await?;
chan.write_async(writer).await?;
}
ServerMessageBody::N001Welcome { client, text } => {
writer.write_all(b"001 ").await?;
writer.write_all(&client).await?;
writer.write_all(b" :").await?;
writer.write_all(text).await?;
}
ServerMessageBody::N002YourHost { client, text } => {
writer.write_all(b"002 ").await?;
writer.write_all(&client).await?;
writer.write_all(b" :").await?;
writer.write_all(text).await?;
}
ServerMessageBody::N003Created { client, text } => {
writer.write_all(b"003 ").await?;
writer.write_all(&client).await?;
writer.write_all(b" :").await?;
writer.write_all(text).await?;
}
ServerMessageBody::N004MyInfo {
client,
hostname,
softname,
} => {
writer.write_all(b"004 ").await?;
writer.write_all(&client).await?;
writer.write_all(b" ").await?;
writer.write_all(&hostname).await?;
writer.write_all(b" ").await?;
writer.write_all(&softname).await?;
writer
.write_all(b" DGMQRSZagiloswz CFILPQbcefgijklmnopqrstvz bkloveqjfI")
.await?;
// TODO remove hardcoded modes
}
ServerMessageBody::N005ISupport { client, params } => {
writer.write_all(b"005 ").await?;
writer.write_all(&client).await?;
writer.write_all(b" ").await?;
writer.write_all(&params).await?;
writer.write_all(b" :are supported by this server").await?;
}
ServerMessageBody::N332Topic {
client,
chat,
topic,
} => {
writer.write_all(b"332 ").await?;
writer.write_all(&client).await?;
writer.write_all(b" ").await?;
chat.write_async(writer).await?;
writer.write_all(b" :").await?;
writer.write_all(&topic).await?;
}
ServerMessageBody::N353NamesReply {
client,
chan,
members,
} => {
writer.write_all(b"353 ").await?;
writer.write_all(&client).await?;
writer.write_all(b" = ").await?;
chan.write_async(writer).await?;
writer.write_all(b" :").await?;
writer.write_all(&members).await?;
}
ServerMessageBody::N366NamesReplyEnd { client, chan } => {
writer.write_all(b"366 ").await?;
writer.write_all(&client).await?;
writer.write_all(b" ").await?;
chan.write_async(writer).await?;
writer.write_all(b" :End of /NAMES list").await?;
}
}
Ok(())
}
}
fn server_message_body(input: &[u8]) -> IResult<&[u8], ServerMessageBody> {
alt((
server_message_body_notice,
server_message_body_ping,
server_message_body_pong,
))(input)
}
fn server_message_body_notice(input: &[u8]) -> IResult<&[u8], ServerMessageBody> {
let (input, _) = tag("NOTICE ")(input)?;
let (input, first_target) = receiver(input)?;
let (input, _) = tag(" :")(input)?;
let (input, text) = token(input)?;
let first_target = first_target.to_owned();
let text = text.to_owned();
Ok((
input,
ServerMessageBody::Notice {
first_target,
rest_targets: vec![],
text,
},
))
}
fn server_message_body_ping(input: &[u8]) -> IResult<&[u8], ServerMessageBody> {
let (input, _) = tag("PING ")(input)?;
let (input, token) = token(input)?;
Ok((
input,
ServerMessageBody::Ping {
token: token.to_owned(),
},
))
}
fn server_message_body_pong(input: &[u8]) -> IResult<&[u8], ServerMessageBody> {
let (input, _) = tag("PONG ")(input)?;
let (input, from) = receiver(input)?;
let (input, _) = tag(" :")(input)?;
let (input, token) = token(input)?;
Ok((
input,
ServerMessageBody::Pong {
from: from.to_owned(),
token: token.to_owned(),
},
))
}
#[cfg(test)]
mod test {
use assert_matches::*;
use super::*;
use crate::util::testkit::*;
#[test]
fn test_server_message_notice() {
let input = b"NOTICE * :*** Looking up your hostname...\n";
let expected = ServerMessage {
tags: vec![],
sender: None,
body: ServerMessageBody::Notice {
first_target: b"*".to_vec(),
rest_targets: vec![],
text: b"*** Looking up your hostname...".to_vec(),
},
};
let result = server_message(input);
assert_matches!(result, Ok((_, result)) => assert_eq!(expected, result));
let mut bytes = vec![];
sync_future(expected.write_async(&mut bytes))
.unwrap()
.unwrap();
assert_eq!(bytes, input);
}
#[test]
fn test_server_message_pong() {
let input = b"PONG server.example :LAG004911\n";
let expected = ServerMessage {
tags: vec![],
sender: None,
body: ServerMessageBody::Pong {
from: b"server.example".to_vec(),
token: b"LAG004911".to_vec(),
},
};
let result = server_message(input);
assert_matches!(result, Ok((_, result)) => assert_eq!(expected, result));
let mut bytes = vec![];
sync_future(expected.write_async(&mut bytes))
.unwrap()
.unwrap();
assert_eq!(bytes, input);
}
}

View File

@ -1,2 +0,0 @@
//! Definitions of wire protocols to be used in implementations of projections.
pub mod irc;

View File

@ -1,10 +0,0 @@
use std::convert::Infallible;
use http_body_util::Full;
use hyper::{body::Bytes, Response, StatusCode};
pub fn not_found() -> std::result::Result<Response<Full<Bytes>>, Infallible> {
let mut response = Response::new(Full::new(Bytes::from("404")));
*response.status_mut() = StatusCode::NOT_FOUND;
Ok(response)
}

View File

@ -1,34 +0,0 @@
use futures_util::FutureExt;
use tokio::sync::oneshot::{channel, Sender};
use crate::prelude::*;
pub mod http;
pub mod table;
pub mod telemetry;
#[cfg(test)]
pub mod testkit;
pub struct Terminator {
signal: Sender<()>,
completion: JoinHandle<Result<()>>,
}
impl Terminator {
pub fn from_raw(signal: Sender<()>, completion: JoinHandle<Result<()>>) -> Terminator {
Terminator { signal, completion }
}
pub fn new(completion: JoinHandle<Result<()>>) -> (Terminator, impl Future<Output = ()>) {
let (signal, rx) = channel();
(Terminator { signal, completion }, rx.map(|_| ()))
}
pub async fn terminate(self) -> Result<()> {
match self.signal.send(()) {
Ok(()) => {}
Err(_) => log::error!("Termination channel is dropped"),
}
self.completion.await??;
Ok(())
}
}

View File

@ -1,81 +0,0 @@
use std::convert::Infallible;
use std::net::SocketAddr;
use futures_util::FutureExt;
use http_body_util::{BodyExt, Full};
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response};
use prometheus::{Encoder, Registry as MetricsRegistry, TextEncoder};
use serde::Deserialize;
use tokio::net::TcpListener;
use tokio::sync::oneshot::channel;
use crate::prelude::*;
use crate::util::http::*;
use crate::util::Terminator;
type BoxBody = http_body_util::combinators::BoxBody<Bytes, Infallible>;
#[derive(Deserialize, Debug)]
pub struct ServerConfig {
pub listen_on: SocketAddr,
}
pub async fn launch(config: ServerConfig, metrics: MetricsRegistry) -> Result<Terminator> {
log::info!("Starting the telemetry service");
let listener = TcpListener::bind(config.listen_on).await?;
log::debug!("Listener started");
let (signal, rx) = channel();
let handle = tokio::task::spawn(main_loop(listener, metrics, rx.map(|_| ())));
let terminator = Terminator::from_raw(signal, handle);
Ok(terminator)
}
async fn main_loop(
listener: TcpListener,
metrics: MetricsRegistry,
termination: impl Future<Output = ()>,
) -> Result<()> {
pin!(termination);
loop {
select! {
biased;
_ = &mut termination => break,
result = listener.accept() => {
let (stream, _) = result?;
let metrics = metrics.clone();
tokio::task::spawn(async move {
let registry = metrics.clone();
let server = http1::Builder::new().serve_connection(stream, service_fn(move |r| route(registry.clone(), r)));
if let Err(err) = server.await {
tracing::error!("Error serving connection: {:?}", err);
}
});
},
}
}
log::info!("Terminating the telemetry service");
Ok(())
}
async fn route(
registry: MetricsRegistry,
request: Request<hyper::body::Incoming>,
) -> std::result::Result<Response<BoxBody>, Infallible> {
match (request.method(), request.uri().path()) {
(&Method::GET, "/metrics") => Ok(metrics(registry)?.map(BodyExt::boxed)),
_ => Ok(not_found()?.map(BodyExt::boxed)),
}
}
fn metrics(registry: MetricsRegistry) -> std::result::Result<Response<Full<Bytes>>, Infallible> {
let mf = registry.gather();
let mut buffer = vec![];
TextEncoder
.encode(&mf, &mut buffer)
.expect("write to vec cannot fail");
Ok(Response::new(Full::new(Bytes::from(buffer))))
}