forked from lavina/lavina
Compare commits
123 Commits
7f5fa955ec
...
9582160d2c
Author | SHA1 | Date |
---|---|---|
JustTestingV | 9582160d2c | |
Nikita Vilunov | 3ff8c98f14 | |
Nikita Vilunov | 1a1136187e | |
Nikita Vilunov | 0b98102580 | |
Nikita Vilunov | 1373767d7f | |
Nikita Vilunov | 887fd95194 | |
Nikita Vilunov | 4621470bde | |
Nikita Vilunov | 9fca913430 | |
Nikita Vilunov | 8047a97baa | |
Nikita Vilunov | 47195f5eee | |
Nikita Vilunov | 2f034284cf | |
Nikita Vilunov | dc0a101fe6 | |
Nikita Vilunov | 854a244dbc | |
Nikita Vilunov | a1db17c779 | |
Nikita Vilunov | 1b3551f108 | |
Nikita Vilunov | 444b608e96 | |
Nikita Vilunov | 563811cbca | |
Nikita Vilunov | 58f6a5d90a | |
Nikita Vilunov | df6cdd4861 | |
Nikita Vilunov | 3d59f6aae5 | |
Nikita Vilunov | ad49703714 | |
JustTestingV | 87d73af811 | |
Nikita Vilunov | 298245f3f5 | |
Nikita Vilunov | 3de7a131f0 | |
Nikita Vilunov | c662b64f11 | |
JustTestingV | 53f218c58f | |
Nikita Vilunov | 377d9c32d2 | |
Nikita Vilunov | 43ea27b655 | |
JustTestingV | e9fc74b46b | |
Nikita Vilunov | 6ed1f6be40 | |
Nikita Vilunov | b9724cd995 | |
Nikita Vilunov | 5c07c8368d | |
Nikita Vilunov | f401aa786e | |
Nikita Vilunov | 1b5ac1491a | |
Nikita Vilunov | ef5c0dbbf6 | |
Nikita Vilunov | f8151699db | |
Nikita Vilunov | c39928799d | |
Nikita Vilunov | 70b12c9a0d | |
Nikita Vilunov | 9f0bcb9279 | |
Nikita Vilunov | 1a43a3c2d7 | |
Nikita Vilunov | 4b04696a4f | |
Nikita Vilunov | c1a461a09e | |
Nikita Vilunov | b80daf9648 | |
Nikita Vilunov | 50915afcf6 | |
Nikita Vilunov | 51d7278617 | |
Nikita Vilunov | 1895084ded | |
Nikita Vilunov | 8efbacc4d0 | |
Nikita Vilunov | daf05869a3 | |
Nikita Vilunov | fc9c45b627 | |
Nikita Vilunov | f2ab963f7b | |
Nikita Vilunov | 4057b4a910 | |
Nikita Vilunov | 55b69f4c8a | |
Nikita Vilunov | d0c579863e | |
Nikita Vilunov | c44101d5d0 | |
Nikita Vilunov | 58582f4e51 | |
Nikita Vilunov | 2b54260f0b | |
Nikita Vilunov | f71d098420 | |
Nikita Vilunov | fb8329a187 | |
Nikita Vilunov | 99435a020e | |
Nikita Vilunov | 65471a6c7f | |
Nikita Vilunov | 123781d145 | |
Nikita Vilunov | fbb7349585 | |
Nikita Vilunov | a2a0a8914d | |
Nikita Vilunov | fb2cbf8a8c | |
Nikita Vilunov | 4ce97f8e13 | |
Nikita Vilunov | 63704d6010 | |
Nikita Vilunov | 7b2bfae147 | |
Nikita Vilunov | 0e78f24fbd | |
Nikita Vilunov | a73bbdb5f1 | |
Nikita Vilunov | c449f18f97 | |
Nikita Vilunov | 9110ab9beb | |
Nikita Vilunov | d0f807841c | |
Nikita Vilunov | bba1ea107d | |
Nikita Vilunov | 71d7323534 | |
Nikita Vilunov | 3e1bb53c1b | |
Nikita Vilunov | a65ea89ce1 | |
Nikita Vilunov | 1cc4761aeb | |
Nikita Vilunov | 6add6db371 | |
Nikita Vilunov | 33dbfba116 | |
Nikita Vilunov | 4107c5b663 | |
Nikita Vilunov | 4730526fee | |
Nikita Vilunov | 443f6a2c90 | |
Nikita Vilunov | f131454cb2 | |
Nikita Vilunov | d444fc407b | |
Nikita Vilunov | 27bbabbbbd | |
Nikita Vilunov | d1dad72c08 | |
Nikita Vilunov | 25c4d02ed2 | |
Nikita Vilunov | dc788a89c4 | |
Nikita Vilunov | f1eff730a2 | |
Nikita Vilunov | 42c22d045f | |
Nikita Vilunov | 435da6663a | |
Nikita Vilunov | 494ddc7ee1 | |
Nikita Vilunov | 0adc19558d | |
Nikita Vilunov | bbd68853ae | |
Nikita Vilunov | 49a975e66e | |
Nikita Vilunov | 266eafe6e6 | |
Nikita Vilunov | e813fb7c69 | |
Nikita Vilunov | 204126b9fb | |
Nikita Vilunov | 63f31aa42f | |
Nikita Vilunov | 69bccef3bf | |
Nikita Vilunov | 81ee1c1044 | |
Nikita Vilunov | 30db029390 | |
Nikita Vilunov | 1e17e017cf | |
Nikita Vilunov | 8b4e963d39 | |
Nikita Vilunov | 203db3b207 | |
Nikita Vilunov | 1bc305962e | |
Nikita Vilunov | a03a3a11a3 | |
Nikita Vilunov | 23898038e1 | |
Nikita Vilunov | 7dfe6e0295 | |
Nikita Vilunov | 3950ee1d7a | |
Nikita Vilunov | 05f8c5e502 | |
Nikita Vilunov | 39fed80106 | |
Nikita Vilunov | 4e5ccd604c | |
Nikita Vilunov | 57ea2dd2d7 | |
Nikita Vilunov | 265b78dc51 | |
Nikita Vilunov | c845f5d4ca | |
Nikita Vilunov | d10cddec61 | |
Nikita Vilunov | a8d6a98a5b | |
Nikita Vilunov | 7d6ae661c4 | |
Nikita Vilunov | cef0269828 | |
Nikita Vilunov | ec819d37ea | |
Nikita Vilunov | 315b7e638b | |
Nikita Vilunov | b1b8ec800e |
|
@ -0,0 +1,6 @@
|
|||
*
|
||||
!/src/
|
||||
!/migrations/
|
||||
!Cargo.lock
|
||||
!Cargo.toml
|
||||
!rust-toolchain
|
|
@ -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
|
|
@ -1 +1,3 @@
|
|||
/target
|
||||
/db.sqlite
|
||||
.idea/
|
||||
|
|
File diff suppressed because it is too large
Load Diff
60
Cargo.toml
60
Cargo.toml
|
@ -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 }
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
*.pem
|
||||
*.key
|
10
config.toml
10
config.toml
|
@ -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"
|
||||
|
|
|
@ -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.
|
|
@ -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
|
|
@ -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)
|
||||
);
|
|
@ -0,0 +1 @@
|
|||
alter table messages add author_id integer null references users(id);
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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>;
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
name = "mgmt-api"
|
||||
edition = "2021"
|
||||
version.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
|
@ -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";
|
||||
}
|
|
@ -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" }
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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"))
|
||||
}
|
||||
}
|
|
@ -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:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
pub type Str = Arc<str>;
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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()))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
pub type Str = Arc<str>;
|
|
@ -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
|
||||
))));
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
))));
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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>>) {}
|
||||
}
|
|
@ -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;
|
|
@ -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"] }
|
|
@ -1,2 +0,0 @@
|
|||
mod prelude;
|
||||
pub mod well_known;
|
|
@ -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};
|
|
@ -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>;
|
||||
}
|
|
@ -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 }
|
||||
]
|
|
@ -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"]
|
|
@ -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
|
|
@ -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")
|
||||
```
|
|
@ -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 |
|
|
@ -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
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
|||
nightly-2023-10-06
|
|
@ -0,0 +1 @@
|
|||
max_width = 120
|
|
@ -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)
|
||||
}
|
||||
}
|
148
src/core/room.rs
148
src/core/room.rs
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
65
src/main.rs
65
src/main.rs
|
@ -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())?;
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
//! Protocol projections — implementations of public APIs.
|
||||
pub mod irc;
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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(¶ms).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);
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
//! Definitions of wire protocols to be used in implementations of projections.
|
||||
pub mod irc;
|
|
@ -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)
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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))))
|
||||
}
|
Loading…
Reference in New Issue