Scaffold the federated services daemon
Protocol-agnostic core: a Protocol trait maps raw server-to-server lines to a normalized event/action model, so the engine never touches a raw line and a new ircd is one new module. Adds event-log state (state is a fold over the log) and a service framework. First protocol impl links to an InspIRCd (insp4) uplink — completes the CAPAB/SERVER handshake, bursts, and introduces NickServ.
This commit is contained in:
commit
f82ab0c70a
15 changed files with 890 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
config.toml
|
||||
418
Cargo.lock
generated
Normal file
418
Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "fedserv"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||
dependencies = [
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_write",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex-automata",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "fedserv"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
description = "Federated IRC services daemon (protocol-agnostic core, InspIRCd link first)"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1", features = ["net", "io-util", "rt-multi-thread", "macros", "time"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
toml = "0.8"
|
||||
anyhow = "1"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
28
README.md
Normal file
28
README.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# fedserv
|
||||
|
||||
A federated IRC services daemon in Rust. Protocol-agnostic core, InspIRCd link first.
|
||||
|
||||
Clean-room — inspired by the ideas behind modern event-log/gossip services, not
|
||||
derived from any project's source.
|
||||
|
||||
## Design
|
||||
|
||||
- **`proto/`** — the ircd link layer. A `Protocol` trait maps raw server-to-server
|
||||
lines to/from a normalized `NetEvent`/`NetAction` model. The engine never touches
|
||||
a raw line, so a new ircd = one new module. InspIRCd is the first.
|
||||
- **`engine/`** — the services engine: live network `state`, an `EventLog` (every
|
||||
persistent change is an `Event`; state is a fold over the log — this is what makes
|
||||
replicating across nodes a later addition, not a rewrite), and the `Service` trait.
|
||||
- **`services/`** — the pseudo-clients: NickServ first, then ChanServ / OperServ / …
|
||||
|
||||
## Run
|
||||
|
||||
```sh
|
||||
cp config.example.toml config.toml # edit uplink + link password
|
||||
cargo run -- config.toml
|
||||
```
|
||||
|
||||
## Status
|
||||
|
||||
Early scaffold: links to an InspIRCd uplink and introduces NickServ. Burst/mode
|
||||
handling and account persistence are next.
|
||||
12
config.example.toml
Normal file
12
config.example.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Copy to config.toml and edit. config.toml is gitignored (holds the link password).
|
||||
|
||||
[uplink]
|
||||
host = "127.0.0.1"
|
||||
port = 7000
|
||||
password = "changeme" # must match the <link> block on the uplink IRCd
|
||||
|
||||
[server]
|
||||
name = "services.example.net"
|
||||
sid = "42S" # 3 chars, unique on the network
|
||||
description = "Federated Services"
|
||||
protocol = 1206 # InspIRCd link protocol version (1206 = insp4, 1205 = insp3)
|
||||
34
src/config.rs
Normal file
34
src/config.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
pub uplink: Uplink,
|
||||
pub server: Server,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Uplink {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Server {
|
||||
pub name: String,
|
||||
pub sid: String,
|
||||
pub description: String,
|
||||
#[serde(default = "default_protocol")]
|
||||
pub protocol: u32,
|
||||
}
|
||||
|
||||
fn default_protocol() -> u32 {
|
||||
1206 // InspIRCd 4 spanning-tree protocol (1205 = insp3)
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load(path: &str) -> anyhow::Result<Self> {
|
||||
let raw = std::fs::read_to_string(path)?;
|
||||
Ok(toml::from_str(&raw)?)
|
||||
}
|
||||
}
|
||||
61
src/engine/mod.rs
Normal file
61
src/engine/mod.rs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
pub mod service;
|
||||
pub mod state;
|
||||
|
||||
use crate::proto::{NetAction, NetEvent};
|
||||
use service::{Service, ServiceCtx};
|
||||
use state::{EventLog, Network};
|
||||
|
||||
pub struct Engine {
|
||||
services: Vec<Box<dyn Service>>,
|
||||
#[allow(dead_code)]
|
||||
network: Network,
|
||||
#[allow(dead_code)]
|
||||
log: EventLog,
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
pub fn new(services: Vec<Box<dyn Service>>) -> Self {
|
||||
Self {
|
||||
services,
|
||||
network: Network::default(),
|
||||
log: EventLog::default(),
|
||||
}
|
||||
}
|
||||
|
||||
// Sent right after the SERVER line: burst, introduce every service, endburst.
|
||||
pub fn startup_actions(&self) -> Vec<NetAction> {
|
||||
let mut out = vec![NetAction::Burst];
|
||||
for svc in &self.services {
|
||||
out.push(NetAction::IntroduceUser {
|
||||
uid: svc.uid().to_string(),
|
||||
nick: svc.nick().to_string(),
|
||||
ident: "services".to_string(),
|
||||
host: svc.host().to_string(),
|
||||
gecos: svc.gecos().to_string(),
|
||||
});
|
||||
}
|
||||
out.push(NetAction::EndBurst);
|
||||
out
|
||||
}
|
||||
|
||||
pub fn handle(&mut self, event: NetEvent) -> Vec<NetAction> {
|
||||
match event {
|
||||
NetEvent::Ping { token, from } => vec![NetAction::Pong { token, from }],
|
||||
NetEvent::Privmsg { from, to, text } => self.dispatch(&from, &to, &text),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// Route a PRIVMSG addressed to a service (by uid or nick) into that service.
|
||||
fn dispatch(&mut self, from: &str, to: &str, text: &str) -> Vec<NetAction> {
|
||||
let mut ctx = ServiceCtx::default();
|
||||
for svc in self.services.iter_mut() {
|
||||
if to.eq_ignore_ascii_case(svc.uid()) || to.eq_ignore_ascii_case(svc.nick()) {
|
||||
let args: Vec<&str> = text.split_whitespace().collect();
|
||||
svc.on_command(from, &args, &mut ctx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
ctx.actions
|
||||
}
|
||||
}
|
||||
29
src/engine/service.rs
Normal file
29
src/engine/service.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
use crate::proto::NetAction;
|
||||
|
||||
// A pseudo-client (NickServ, ChanServ, OperServ, ...). Introduced at burst,
|
||||
// receives the commands users message it, and pushes actions onto the ctx.
|
||||
pub trait Service: Send {
|
||||
fn nick(&self) -> &str;
|
||||
fn uid(&self) -> &str;
|
||||
fn host(&self) -> &str {
|
||||
"services.local"
|
||||
}
|
||||
fn gecos(&self) -> &str;
|
||||
fn on_command(&mut self, from: &str, args: &[&str], ctx: &mut ServiceCtx);
|
||||
}
|
||||
|
||||
// What a service can do in response to a command, collected for the link to flush.
|
||||
#[derive(Default)]
|
||||
pub struct ServiceCtx {
|
||||
pub actions: Vec<NetAction>,
|
||||
}
|
||||
|
||||
impl ServiceCtx {
|
||||
pub fn notice(&mut self, from: &str, to: &str, text: impl Into<String>) {
|
||||
self.actions.push(NetAction::Notice {
|
||||
from: from.to_string(),
|
||||
to: to.to_string(),
|
||||
text: text.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
39
src/engine/state.rs
Normal file
39
src/engine/state.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
// Live network view the services reason about.
|
||||
#[derive(Default)]
|
||||
pub struct Network {
|
||||
pub users: HashMap<String, User>, // keyed by UID
|
||||
pub channels: HashMap<String, Channel>,
|
||||
}
|
||||
|
||||
pub struct User {
|
||||
pub uid: String,
|
||||
pub nick: String,
|
||||
pub account: Option<String>,
|
||||
}
|
||||
|
||||
pub struct Channel {
|
||||
pub name: String,
|
||||
pub ts: u64,
|
||||
}
|
||||
|
||||
// The Sable-inspired core: every persistent change is an Event, and state is a
|
||||
// fold over the log. Single-node today; replicating this log across service
|
||||
// nodes is what turns it federated later, without rewriting the services.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Event {
|
||||
AccountRegistered { account: String, uid: String },
|
||||
ChannelRegistered { channel: String, founder: String },
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EventLog {
|
||||
pub events: Vec<Event>,
|
||||
}
|
||||
|
||||
impl EventLog {
|
||||
pub fn append(&mut self, event: Event) {
|
||||
self.events.push(event);
|
||||
}
|
||||
}
|
||||
43
src/link.rs
Normal file
43
src/link.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
use anyhow::Result;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::engine::Engine;
|
||||
use crate::proto::Protocol;
|
||||
|
||||
// One uplink session: connect, handshake + burst, then translate lines forever.
|
||||
pub async fn run(mut proto: Box<dyn Protocol>, mut engine: Engine, addr: &str) -> Result<()> {
|
||||
let stream = TcpStream::connect(addr).await?;
|
||||
let (read, mut write) = stream.into_split();
|
||||
let mut lines = BufReader::new(read).lines();
|
||||
|
||||
for line in proto.handshake() {
|
||||
send(&mut write, &line).await?;
|
||||
}
|
||||
for action in engine.startup_actions() {
|
||||
for line in proto.serialize(&action) {
|
||||
send(&mut write, &line).await?;
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(line) = lines.next_line().await? {
|
||||
tracing::debug!(dir = "<<", %line);
|
||||
for event in proto.parse(&line) {
|
||||
for action in engine.handle(event) {
|
||||
for out in proto.serialize(&action) {
|
||||
send(&mut write, &out).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::warn!("uplink closed the connection");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send(write: &mut (impl AsyncWriteExt + Unpin), line: &str) -> Result<()> {
|
||||
tracing::debug!(dir = ">>", %line);
|
||||
write.write_all(line.as_bytes()).await?;
|
||||
write.write_all(b"\r\n").await?;
|
||||
Ok(())
|
||||
}
|
||||
43
src/main.rs
Normal file
43
src/main.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
mod config;
|
||||
mod engine;
|
||||
mod link;
|
||||
mod proto;
|
||||
mod services;
|
||||
|
||||
use anyhow::Result;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use engine::Engine;
|
||||
use proto::inspircd::InspIrcd;
|
||||
use services::nickserv::NickServ;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| "fedserv=debug".into()),
|
||||
)
|
||||
.init();
|
||||
|
||||
let path = std::env::args().nth(1).unwrap_or_else(|| "config.toml".to_string());
|
||||
let cfg = config::Config::load(&path)?;
|
||||
let ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
|
||||
|
||||
let proto = Box::new(InspIrcd::new(
|
||||
cfg.server.name.clone(),
|
||||
cfg.server.sid.clone(),
|
||||
cfg.uplink.password.clone(),
|
||||
cfg.server.protocol,
|
||||
ts,
|
||||
));
|
||||
|
||||
let services: Vec<Box<dyn engine::service::Service>> = vec![Box::new(NickServ {
|
||||
uid: format!("{}AAAAAA", cfg.server.sid),
|
||||
})];
|
||||
let engine = Engine::new(services);
|
||||
|
||||
let addr = format!("{}:{}", cfg.uplink.host, cfg.uplink.port);
|
||||
tracing::info!(server = %cfg.server.name, %addr, "linking to uplink");
|
||||
link::run(proto, engine, &addr).await
|
||||
}
|
||||
104
src/proto/inspircd.rs
Normal file
104
src/proto/inspircd.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// InspIRCd spanning-tree link protocol. Handshake + UID + PING mirror the
|
||||
// sequence in Network-Links (protocols/inspircd.py); mode/burst details get
|
||||
// firmed up against a live insp4 uplink.
|
||||
use super::{NetAction, NetEvent, Protocol};
|
||||
|
||||
pub struct InspIrcd {
|
||||
sid: String,
|
||||
name: String,
|
||||
password: String,
|
||||
protocol: u32,
|
||||
ts: u64,
|
||||
}
|
||||
|
||||
impl InspIrcd {
|
||||
pub fn new(name: String, sid: String, password: String, protocol: u32, ts: u64) -> Self {
|
||||
Self { sid, name, password, protocol, ts }
|
||||
}
|
||||
|
||||
fn from_us(&self, cmd: String) -> String {
|
||||
format!(":{} {}", self.sid, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
impl Protocol for InspIrcd {
|
||||
fn handshake(&mut self) -> Vec<String> {
|
||||
vec![
|
||||
format!("CAPAB START {}", self.protocol),
|
||||
format!("CAPAB CAPABILITIES :PROTOCOL={}", self.protocol),
|
||||
"CAPAB END".to_string(),
|
||||
format!("SERVER {} {} {} :{}", self.name, self.password, self.sid, "Federated Services"),
|
||||
]
|
||||
}
|
||||
|
||||
fn parse(&mut self, line: &str) -> Vec<NetEvent> {
|
||||
let (source, rest) = match line.strip_prefix(':') {
|
||||
Some(s) => {
|
||||
let mut it = s.splitn(2, ' ');
|
||||
(Some(it.next().unwrap_or("").to_string()), it.next().unwrap_or(""))
|
||||
}
|
||||
None => (None, line),
|
||||
};
|
||||
let mut tokens = rest.split(' ');
|
||||
let cmd = match tokens.next() {
|
||||
Some(c) => c,
|
||||
None => return vec![],
|
||||
};
|
||||
match cmd.to_ascii_uppercase().as_str() {
|
||||
"SERVER" => vec![NetEvent::Registered],
|
||||
"ENDBURST" => vec![NetEvent::EndBurst],
|
||||
"PING" => {
|
||||
let token = tokens
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.trim_start_matches(':')
|
||||
.to_string();
|
||||
vec![NetEvent::Ping { token, from: source }]
|
||||
}
|
||||
"PRIVMSG" => {
|
||||
let to = tokens.next().unwrap_or("").to_string();
|
||||
vec![NetEvent::Privmsg {
|
||||
from: source.unwrap_or_default(),
|
||||
to,
|
||||
text: trailing(rest),
|
||||
}]
|
||||
}
|
||||
"QUIT" => vec![NetEvent::Quit { uid: source.unwrap_or_default() }],
|
||||
_ => vec![NetEvent::Unknown { line: line.to_string() }],
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize(&mut self, action: &NetAction) -> Vec<String> {
|
||||
match action {
|
||||
NetAction::Burst => vec![self.from_us(format!("BURST {}", self.ts))],
|
||||
NetAction::EndBurst => vec![self.from_us("ENDBURST".to_string())],
|
||||
NetAction::Pong { token, from } => {
|
||||
let dest = from.clone().unwrap_or_else(|| self.sid.clone());
|
||||
vec![self.from_us(format!("PONG {} {}", dest, token))]
|
||||
}
|
||||
NetAction::IntroduceUser { uid, nick, ident, host, gecos } => vec![self.from_us(format!(
|
||||
"UID {uid} {ts} {nick} {host} {host} {ident} 0.0.0.0 {ts} +ioS :{gecos}",
|
||||
uid = uid, ts = self.ts, nick = nick, host = host, ident = ident, gecos = gecos
|
||||
))],
|
||||
NetAction::Privmsg { from, to, text } => {
|
||||
vec![format!(":{} PRIVMSG {} :{}", from, to, text)]
|
||||
}
|
||||
NetAction::Notice { from, to, text } => {
|
||||
vec![format!(":{} NOTICE {} :{}", from, to, text)]
|
||||
}
|
||||
NetAction::Raw(s) => vec![s.clone()],
|
||||
}
|
||||
}
|
||||
|
||||
fn sid(&self) -> &str {
|
||||
&self.sid
|
||||
}
|
||||
}
|
||||
|
||||
// The IRC "trailing" argument: everything after the first " :", else the last word.
|
||||
fn trailing(rest: &str) -> String {
|
||||
match rest.find(" :") {
|
||||
Some(i) => rest[i + 2..].to_string(),
|
||||
None => rest.rsplit(' ').next().unwrap_or("").to_string(),
|
||||
}
|
||||
}
|
||||
38
src/proto/mod.rs
Normal file
38
src/proto/mod.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Protocol abstraction. The engine only ever sees NetEvent / NetAction; raw
|
||||
// server-to-server lines live entirely behind a Protocol impl. Adding another
|
||||
// ircd later means one new module, engine untouched.
|
||||
pub mod inspircd;
|
||||
|
||||
// Normalized inbound facts, translated from the uplink's raw lines.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum NetEvent {
|
||||
Registered,
|
||||
EndBurst,
|
||||
Ping { token: String, from: Option<String> },
|
||||
Privmsg { from: String, to: String, text: String },
|
||||
Quit { uid: String },
|
||||
Unknown { line: String },
|
||||
}
|
||||
|
||||
// Normalized outbound intents the engine wants performed on the network.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum NetAction {
|
||||
Burst,
|
||||
EndBurst,
|
||||
Pong { token: String, from: Option<String> },
|
||||
IntroduceUser { uid: String, nick: String, ident: String, host: String, gecos: String },
|
||||
Privmsg { from: String, to: String, text: String },
|
||||
Notice { from: String, to: String, text: String },
|
||||
Raw(String),
|
||||
}
|
||||
|
||||
pub trait Protocol: Send {
|
||||
/// Lines to send immediately on connect (auth / capability negotiation).
|
||||
fn handshake(&mut self) -> Vec<String>;
|
||||
/// One raw inbound line -> zero or more normalized events.
|
||||
fn parse(&mut self, line: &str) -> Vec<NetEvent>;
|
||||
/// One normalized action -> the raw line(s) that realise it.
|
||||
fn serialize(&mut self, action: &NetAction) -> Vec<String>;
|
||||
/// Our own server id, used as the source prefix for server-sourced lines.
|
||||
fn sid(&self) -> &str;
|
||||
}
|
||||
1
src/services/mod.rs
Normal file
1
src/services/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod nickserv;
|
||||
25
src/services/nickserv.rs
Normal file
25
src/services/nickserv.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
use crate::engine::service::{Service, ServiceCtx};
|
||||
|
||||
pub struct NickServ {
|
||||
pub uid: String,
|
||||
}
|
||||
|
||||
impl Service for NickServ {
|
||||
fn nick(&self) -> &str {
|
||||
"NickServ"
|
||||
}
|
||||
fn uid(&self) -> &str {
|
||||
&self.uid
|
||||
}
|
||||
fn gecos(&self) -> &str {
|
||||
"Nickname Services"
|
||||
}
|
||||
|
||||
fn on_command(&mut self, from: &str, args: &[&str], ctx: &mut ServiceCtx) {
|
||||
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||
Some("HELP") => ctx.notice(self.uid(), from, "Commands: REGISTER, IDENTIFY (coming soon)."),
|
||||
Some(other) => ctx.notice(self.uid(), from, format!("Unknown command: {}. Try HELP.", other)),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue