[WIP!] feat: ts6 server support

this is really unfinished and *WILL* be rebased!
pushing just to let the test run on github's servers
(my laptop is too sh*tty)
This commit is contained in:
user0-07161 2025-12-28 15:36:48 +01:00
parent 86b9c8d3a7
commit a3a19c610d
34 changed files with 4423 additions and 395 deletions

View file

@ -19,4 +19,4 @@ jobs:
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
run: cargo test --verbose -- --no-capture

344
Cargo.lock generated
View file

@ -2,15 +2,6 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "addr2line"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.1"
@ -82,28 +73,6 @@ version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "async-stream"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
dependencies = [
"async-stream-impl",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-stream-impl"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "async-trait"
version = "0.1.89"
@ -129,11 +98,10 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "axum"
version = "0.7.9"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
dependencies = [
"async-trait",
"axum-core",
"bytes",
"futures-util",
@ -146,49 +114,31 @@ dependencies = [
"mime",
"percent-encoding",
"pin-project-lite",
"rustversion",
"serde",
"serde_core",
"sync_wrapper",
"tower 0.5.2",
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum-core"
version = "0.4.5"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
dependencies = [
"async-trait",
"bytes",
"futures-util",
"futures-core",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"sync_wrapper",
"tower-layer",
"tower-service",
]
[[package]]
name = "backtrace"
version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-link",
]
[[package]]
name = "base64"
version = "0.21.7"
@ -227,9 +177,9 @@ checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "clap"
version = "4.5.48"
version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae"
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
dependencies = [
"clap_builder",
"clap_derive",
@ -237,9 +187,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.48"
version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9"
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
dependencies = [
"anstream",
"anstyle",
@ -249,9 +199,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.47"
version = "4.5.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c"
checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
dependencies = [
"heck",
"proc-macro2",
@ -273,22 +223,23 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "console-api"
version = "0.8.1"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8030735ecb0d128428b64cd379809817e620a40e5001c54465b99ec5feec2857"
checksum = "e8599749b6667e2f0c910c1d0dff6901163ff698a52d5a39720f61b5be4b20d3"
dependencies = [
"futures-core",
"prost",
"prost-types",
"tonic",
"tonic-prost",
"tracing-core",
]
[[package]]
name = "console-subscriber"
version = "0.4.1"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6539aa9c6a4cd31f4b1c040f860a1eac9aa80e7df6b05d506a6e7179936d6a01"
checksum = "fb4915b7d8dd960457a1b6c380114c2944f728e7c65294ab247ae6b6f1f37592"
dependencies = [
"console-api",
"crossbeam-channel",
@ -401,23 +352,6 @@ dependencies = [
"pin-utils",
]
[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "gimli"
version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
[[package]]
name = "h2"
version = "0.4.12"
@ -430,19 +364,13 @@ dependencies = [
"futures-core",
"futures-sink",
"http",
"indexmap 2.11.4",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.16.0"
@ -571,22 +499,12 @@ dependencies = [
"hyper",
"libc",
"pin-project-lite",
"socket2 0.6.0",
"socket2",
"tokio",
"tower-service",
"tracing",
]
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
]
[[package]]
name = "indexmap"
version = "2.11.4"
@ -594,18 +512,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
dependencies = [
"equivalent",
"hashbrown 0.16.0",
]
[[package]]
name = "io-uring"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
dependencies = [
"bitflags",
"cfg-if",
"libc",
"hashbrown",
]
[[package]]
@ -685,9 +592,9 @@ dependencies = [
[[package]]
name = "matchit"
version = "0.7.3"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "memchr"
@ -755,15 +662,6 @@ dependencies = [
"autocfg",
]
[[package]]
name = "object"
version = "0.37.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.21.3"
@ -837,15 +735,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.101"
@ -857,9 +746,9 @@ dependencies = [
[[package]]
name = "prost"
version = "0.13.5"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d"
dependencies = [
"bytes",
"prost-derive",
@ -867,9 +756,9 @@ dependencies = [
[[package]]
name = "prost-derive"
version = "0.13.5"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425"
dependencies = [
"anyhow",
"itertools",
@ -880,9 +769,9 @@ dependencies = [
[[package]]
name = "prost-types"
version = "0.13.5"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72"
dependencies = [
"prost",
]
@ -896,36 +785,6 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.5.17"
@ -952,18 +811,6 @@ version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
[[package]]
name = "rustc-demangle"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.20"
@ -1021,9 +868,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "1.0.2"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee"
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
dependencies = [
"serde_core",
]
@ -1058,16 +905,6 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "socket2"
version = "0.6.0"
@ -1132,30 +969,27 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.47.1"
version = "1.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
dependencies = [
"backtrace",
"bytes",
"io-uring",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"slab",
"socket2 0.6.0",
"socket2",
"tokio-macros",
"tracing",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.5.0"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
@ -1188,11 +1022,11 @@ dependencies = [
[[package]]
name = "toml"
version = "0.9.7"
version = "0.9.10+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0"
checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48"
dependencies = [
"indexmap 2.11.4",
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime",
@ -1203,35 +1037,34 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.7.2"
version = "0.7.5+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1"
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_parser"
version = "1.0.3"
version = "1.0.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627"
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.0.3"
version = "1.0.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109"
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
[[package]]
name = "tonic"
version = "0.12.3"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203"
dependencies = [
"async-stream",
"async-trait",
"axum",
"base64 0.22.1",
@ -1245,34 +1078,25 @@ dependencies = [
"hyper-util",
"percent-encoding",
"pin-project",
"prost",
"socket2 0.5.10",
"socket2",
"sync_wrapper",
"tokio",
"tokio-stream",
"tower 0.4.13",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower"
version = "0.4.13"
name = "tonic-prost"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67"
dependencies = [
"futures-core",
"futures-util",
"indexmap 1.9.3",
"pin-project",
"pin-project-lite",
"rand",
"slab",
"tokio",
"tokio-util",
"tower-layer",
"tower-service",
"tracing",
"bytes",
"prost",
"tonic",
]
[[package]]
@ -1283,10 +1107,15 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [
"futures-core",
"futures-util",
"indexmap",
"pin-project-lite",
"slab",
"sync_wrapper",
"tokio",
"tokio-util",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
@ -1303,9 +1132,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.41"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
"tracing-attributes",
@ -1314,9 +1143,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.30"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
@ -1325,9 +1154,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.34"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
@ -1346,9 +1175,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.20"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [
"matchers",
"nu-ansi-term",
@ -1403,9 +1232,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "windows-link"
version = "0.2.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
@ -1434,6 +1263,15 @@ dependencies = [
"windows-targets 0.53.4",
]
[[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 = "windows-targets"
version = "0.52.6"
@ -1568,23 +1406,3 @@ name = "winnow"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
[[package]]
name = "zerocopy"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View file

@ -5,16 +5,16 @@ edition = "2024"
[dependencies]
async-trait = "0.1.89"
clap = { version = "4.5.48", features = ["derive"] }
once_cell = "1.21.3"
tokio = { version = "1.47.1", features = ["full"] }
console-subscriber = { version = "0.4.1", optional = true }
tracing = "0.1.41"
tracing-subscriber = "0.3.20"
clap = { version = "4.5.53", features = ["derive"] }
tokio = { version = "1.48.0", features = ["full"] }
console-subscriber = { version = "0.5.0", optional = true }
tracing = "0.1.44"
tracing-subscriber = "0.3.22"
thiserror = "2.0.17"
anyhow = "1.0.100"
toml = "0.9.7"
toml = "0.9.10"
serde = { version = "1.0.228", features = ["derive"] }
once_cell = "1.21.3"
[features]
tokio-console = ["tokio/tracing", "console-subscriber"]

View file

@ -0,0 +1,172 @@
Client Capab Proposal (v5)
Authors: fl and Beeth
--------------------------
This document aims to define a protocol for capabilities between a server
and a client. These capabs help servers introduce changes to the protocol
that could cause problems with clients that do not support the changes.
This specification defines the "CAP" command, and the available subcommands.
This is sent in the form:
CAP [subcommand [parameters]]
CAP
---
A client that supports this specification must send one of three things at
the earliest stage of registration. To enter capab negotiation it can either
send the command "CAP" without parameters to request a list of capabs, or it
can send the subcommand "REQ" to request capabs without first seeing the
list of available capabs. A client which does not wish to enter capab
negotiation but supports this spec must send the subcommand "END" to notify
support of the specification.
The "CAP" command may be used at any time during the connection to request a
list of capabs. If the user enters capab negotiation the server must
postpone registration until the client issues the subcommand "END".
A server receiving the parameterless "CAP" command from a client must reply
to this command with a list of capabs supported by the server using the "LS"
and "LSL" subcommands. The "LS" subcommand is used when the capab list must
be spread across multiple lines and is used for all except the last line.
The subcommand "LSL" is used to finish the list of capabs.
The subcommands "LS" and "LSL" are sent in the form:
CAP <subcommand> :<capab> [capab]*
CAP: REQ
--------
The "REQ" subcommand is used by a client to give a list of capabs it
wishes to use.
The subcommand "REQ" is sent in the form:
CAP REQ :<capab> [capab]*
If any of the requested capabs are prefixed with '-' then the client is
notifying the server that it wishes the specified capab to be disabled for
the client.
The responsibility is placed on the client to request a valid list of capabs.
The server must either accept the request as issued or reject it. A server
must not partially accept parameters of the subcommand "REQ".
CAP: ACK
--------
The "ACK" subcommand is used by a server to acknowledge that the
given capabilities are now being used. The "ACK" subcommand is used by a
client to acknowledge the given capabilities are now being used.
The server places the capabilities it is acknowledging as parameters. The
parameters of the subcommand "ACK" will be identical to the parameters issued
in the subcommand "REQ" from the client if the request is valid. If any of
the requested capabs modify the protocol stream, after sending the subcommand
"ACK" the server must then transmit any following messages in the modified
protocol stream form.
The client must issue the subcommand "ACK" for any capabilities that modify
the protocol stream, using the capabs that modify the protocol stream as the
subcommand "ACK" parameters. A client must know which capabs modify the
protocol stream and send the subcommand "ACK" before transmitting any
messages in the modified protocol stream. A client must not issue the
subcommand "ACK" for a capab without first receiving the subcommand "ACK"
containing that capab from the server.
A client must not issue the subcommand "ACK" for capabs that do not modify
the protocol stream.
The subcommand "ACK" is sent in the following form:
CAP ACK :<capab> [capab]*
CAP: NAK
--------
The "NAK" subcommand is used by a server to notify a client that the
requested list of capabs are invalid.
A client receiving the subcommand "NAK" must request again the capabs it
wishes to use. The parameters of the subcommand "NAK" will be identical
to the parameters issued in the subcommand "REQ" from the client if the
request is invalid.
The subcommand "NAK" is sent in the following form:
CAP NAK :<capab> [capab]*
CAP: CLEAR
----------
The "CLEAR" subcommand is used by a server to notify a client that all
capabs have been cleared. The "CLEAR" subcommand is used by a client to
request that a server clears its current capab list.
A server receiving the subcommand "CLEAR" must issue back the subcommand
"CLEAR" using the current protocol stream. It must then clear the stored
list of capabs and revert to receiving/sending a 'normal' protocol stream
from/to the client.
A client receiving the subcommand "CLEAR" must then revert to expecting to
receive a 'normal' protocol stream.
The subcommand "CLEAR" is sent in the form:
CAP CLEAR
CAP: END
--------
The "END" subcommand is used by a client to notify the server it has
finished capab negotiation and the server can proceed with registration.
The subcommand "END" is sent in the form:
CAP END
Examples
--------
CLIENT: indicates what client sends
SERVER: indicates what server sends
This example shows a client which doesnt support this specification:
CLIENT: NICK FOO
CLIENT: USER FOO FOO FOO FOO
SERVER: :SERVERNAME 001 ...
This example shows a client supporting this specification but not
wishing to use capabs for the connection:
CLIENT: CAP END
CLIENT: PASS FOO
CLIENT: NICK FOO
CLIENT: USER FOO FOO FOO FOO
SERVER: :SERVERNAME 001 ....
This example shows a client requesting a list of capabs then successfully
requesting the capabs.
CLIENT: CAP
CLIENT: NICK FOO
CLIENT: USER FOO FOO FOO FOO
SERVER: CAP LS :A B C D E F G H
SERVER: CAP LSL :I J
CLIENT: CAP REQ :A B C D E F
SERVER: CAP ACK :A B C D E F
CLIENT: CAP END
SERVER: :SERVERNAME 001 ...
This example shows a client requesting an invalid list of capabs.
CLIENT: CAP
CLIENT: NICK FOO
CLIENT: USER FOO FOO FOO FOO
SERVER: CAP LSL :A B C D E F G H
CLIENT: CAP REQ :A B
SERVER: CAP NAK :A B
CLIENT: CAP REQ :A
SERVER: CAP ACK :A
CLIENT: CAP REQ :C
SERVER: CAP ACK :C
CLIENT: CAP END
SERVER: :SERVERNAME 001 ...

32
docs/technical/encap.txt Normal file
View file

@ -0,0 +1,32 @@
ENCAP DEFINITION
----------------
Preamble
--------
This document defines the specification for the ENCAP command.
ENCAP is designed to help fix the situation where new commands do
not propagate over hub servers running older code.
Definition
----------
Support for the ENCAP command is given by the CAPAB token "ENCAP".
The format of ENCAP is:
:<source> ENCAP <destination> <subcommand> <parameters>
<source> - The entity generating the command.
<destination> - The entity the command is destined for. This may
include wildcards for servers, but not clients.
If the wildcard does not match the current server, the
command should be propagated and ignored.
<subcommand> - The subcommand we're propagating over ENCAP. If the
subcommand is not recognised by the current server, the
command should be propagated and ignored.
<parameters> - The parameters that the subcommand have.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,296 @@
TS6 Proposal (v8)
Written by Lee H <lee@leeh.co.uk>
Ideas borrowed heavily from ircnet (Beeth, jv, Q)
- Changes between v7 and v8 -
-----------------------------
In the v7 specification, the JOIN command included the channel modes of a
channel, and acted on them following TS rules. In the v8 specification,
JOIN will never send modes.
Desyncs can occur both when they are sent and when they are not. If they
are sent, then you can have a situation where a user on one side of the
network issues "MODE #channel -l", and a user on another side of the network
issues "JOIN #channel" whilst the +l still exists. As the JOIN string sent
server<->server includes the full modes at the time of the user joining,
this will propagate the +l, but there is a -l crossing in the other
direction. Desync will occur beyond where they intersect.
If the modes are not sent, then a lower TS JOIN command, or a JOIN command
that creates a channel will cause a desync.
It is judged that the desync with sending the modes is worse than the desync
by not sending them, as such the v8 specification dictates modes are not
sent with a JOIN command server<->server.
The v8 specification also clarifies that servers may issue TMODE.
- Introduction -
----------------
This document aims to fix some of the flaws that are still present in the
current TS system.
Whilst only one person may use a nickname at any one time, they are not
a reliable method of directing commands between servers. Clients can change
their nicknames, which can create desyncs. A reliable method of directing
messages between servers is required so that a message will always reach the
intended destination, even if the client changes nicks in between.
UID solves this problem by ensuring that a client has a unique ID for the
duration of his connection.
This document also aims to solve the lack of TS rules to channel 'bans' on
a netburst. Bans from both sides of a TS war (losing/winning) are kept.
Bursting the bans with a TS solves this problem.
There is also a race condition in the current TS system, where a user can
issue a mode during a netburst and the mode will be set on the server
we are bursting to.
- Definitions -
---------------
Throughout this document, the following terms are used:
SID - A servers unique ID. This is three characters long and must be in
the form [0-9][A-Z0-9][A-Z0-9]
ID - A clients unique ID. This is six characters long and must be in
the form [A-Z][A-Z0-9][A-Z0-9][A-Z0-9][A-Z0-9][A-Z0-9]. The
numbers [0-9] at the beginning of an ID are legal characters, but
reserved for future use.
UID - An ID concateneted to a SID. This forms the clients UID.
TS6 - The TS version 6.
- Support -
-----------
Support for this document is given by the TS version 6.
Wherever a destination parameter or source parameter is used, it must use
the SID or UID if the server/client has one. A TS6 capable server must
translate any SIDs/UIDs back into the server/clients name when communicating
with a server that does not support TS6.
A TS6 server must also support the QS (quitstorm) system, and the encap
specification found here:
http://www.leeh.co.uk/ircd/encap.txt
The TS6 protocol does not supports masked entities.
- Nick TS rules -
-----------------
A server receiving a command that requires nick TS rules must check for a
collision between an existing user, and the nick in the received message.
(the "new user"). The collisions must obey the rules specified in Nick TS
collisions.
If the TS received is lower than the TS of the existing user the server will
collide the existing user if the clients user@host are different, if the
clients user@hosts are identical it will collide the new user.
If the TS received is equal to the TS of the existing user both clients are
collided.
If the TS received is higher than the TS of the existing user, the server
will collide the existing user if the user@hosts are identical, if the
clients user@host are different it will collide the new user and drop the
message.
- Nick TS collisions -
----------------------
If both users are to be collided, we must issue a KILL for the existing
user to all servers. If the new user has a UID then we must also issue a
KILL for that UID back to the server sending us data causing the collision.
If only the existing user is being collided, we must issue a KILL for the
existing user to all servers except the server sending us data. If the
existing user has a UID and the server sending us data supports TS6 then
we must also issue a KILL for the existing users UID to the server sending
us data.
If only the new user is being collided, we must issue a KILL for the new user
back to the server sending us data if the new user has a UID.
- Channel TS rules -
--------------------
A server receiving a command that requires normal channel TS rules must
apply the following rules to the command.
If the TS received is lower than our TS of the channel a TS6 server must
remove status modes (+ov etc) and channel modes (+nt etc). If the
originating server is TS6 capable (ie, it has a SID), the server must
also remove any ban modes (+b etc). The new modes and statuses are then
accepted.
If any bans are removed, the server must send to non-TS6, directly connected
servers mode changes removing the bans after the command is propagated.
This prevents desync with banlists, and has to be sent after as clients are
still able to send mode changes before the triggering command arrives.
If the TS received is equal to our TS of the channel the server should keep
its current modes and accept the received modes and statuses.
If the TS received is higher than our TS of the channel the server should keep
its current modes and ignore the received modes and statuses. Any statuses
given in the received message will be removed. A server must mark clients
losing their op (+o) status who do not have a UID as 'deopped'. A server must
ignore any "MODE" commands from a user marked as 'deopped'.
- Simple channel TS rules -
---------------------------
A server receiving a command that requires simple channel TS rules must
apply the following rules to the command.
If the TS received is lower, or equal to our TS of the channel the modes are
accepted. If the TS received is higher than our TS of the channel the modes
are ignored and dropped.
Simple channel TS rules do not affect current modes in the channel except
for the modes we are accepting.
- The following commands are defined here as the TS6 protocol -
---------------------------------------------------------------
- PASS -
PASS <PASSWORD> TS <TS_CURRENT> :<SID>
This command is used for password verification with the server we are
connecting to.
Due to the burst being sent on verification of the "SERVER" command, and
"SVINFO" being sent after "SERVER", we need to be aware of the TS version
earlier to decide whether to send a TS6 burst or not.
The <PASSWORD> field is the password we have stored for this server,
<TS_CURRENT> is our current TS version. If this field is not present then
the server does not support TS6. <SID> is the SID of the server.
- UID -
:<SID> UID <NICK> <HOPS> <TS> +<UMODE> <USERNAME> <HOSTNAME> <IP> <UID> :<GECOS>
This command is used for introducing clients to the network.
The <SID> field is the SID of the server the client is connected to.
The <NICK> field is the nick of the client being introduced. The <HOPS>
field is the amount of server hops between the server being burst to and
the server the client is on. The <TS> field is the TS of the client, either
the time they connected or the time they last changed nick. The <UMODE>
field contains the clients usermodes that need to be transmitted between
servers. The <USERNAME> field contains the clients username/ident. The
<HOSTNAME> field contains the clients host.
The <IP> field contains the clients IP. If the IP is not to be sent
(due to a spoof etc), the field must be sent as "0". The <UID> field is the
clients UID. The <GECOS> field is the clients gecos.
A server receiving a UID command must apply nick TS rules to the nick.
- SID -
:<SID> SID <SERVERNAME> <HOPS> <SID> :<GECOS>
This command is used for introducing servers to the network.
The first <SID> field is the SID of the new servers uplink. The
<SERVERNAME> field is the new servers name. The <HOPS> field is the hops
between the server being introduced nd the server being burst to.
The second <SID> field is the SID of the new server. The <GECOS> field i
is the new servers gecos.
Upon receiving the SID command servers must check for a SID collision.
Two servers must not be allowed to link to the network with the same SID.
If a server detects a SID collision it must drop the link to the directly
connected server through which the command was received.
Client and servers which do not have a UID/SID must be introduced by old
methods.
- SJOIN -
:<SID> SJOIN <TS> <CHANNAME> +<CHANMODES> :<UIDS>
This command is used for introducing users to channels.
The <SID> field is the SID of the server introducing users to the channel.
The <TS> field is the channels current TS, <CHANNAME> is the channels
current name, <CHANMODES> are the channels current modes. <UIDS> is a
space delimited list of clients UIDs to join to the channel. Each clients
UID is prefixed with their status on the channel, ie "@UID" for an opped
user. Multiple prefixes are allowed, "peons" (clients without a status) are
not prefixed.
A server receiving an SJOIN must apply normal channel TS rules to the SJOIN.
A TS6 server must not use the SJOIN command outside of a netburst
to introduce a single user to an existing channel. It must instead
use the "JOIN" command defined in this specification. A TS6 server must
still use SJOIN for creating channels.
- JOIN -
:<UID> JOIN <TS> <CHANNAME> +
This command is used for introducing one user unopped to an existing channel.
The <UID> field is the UID of the client joining the channel. The
<TS> field is the channels current TS, <CHANNAME> is the channels
current name.
A server receiving a JOIN must apply normal channel TS rules to the JOIN.
No channel modes are sent with the JOIN command. In previous versions of
this specification, the "+" parameter contained the channels current modes.
A server following this version of the specification must not interpret this
argument and must not propagate any value other than "+" for this parameter.
It should be noted that whilst JOIN would not normally create a
channel or lower the timestamp, during specific conditions it can. This
can create a desync that this specification does not rectify.
- BMASK -
:<SID> BMASK <TS> <CHANNAME> <TYPE> :<MASKS>
This command is used for bursting channel bans to a network.
The <SID> field is the SID of the server bursting the bans. The
<TS> field is the channels current TS, <CHANNAME> is the channels
name. <TYPE> is a single character identifying the mode type (ie,
for a ban 'b'). <MASKS> is a space delimited list of masks of the
given mode,limited only in length to the size of the buffer as defined
by RFC1459.
A server receiving a BMASK must apply simple channel TS rules to the BMASK.
A TS6 server must translate BMASKs into raw modes for non-TS6
capable servers. This command must be used only after SJOIN has
been sent for the given channel.
It should be noted however, that a BMASK with a lower TS should
not be possible without a desync, due to it being sent after
SJOIN.
- TMODE -
:<SID|UID> TMODE <TS> <CHANNAME> <MODESTRING>
This command is used for clients issuing modes on a channel.
<SID|UID> is either the UID of the client setting the mode, or the SID of
the server setting the mode. <TS> is the current TS of the channel,
<CHANNAME> is the channels name. <MODESTRING> is the raw mode the client is
setting.
A server receiving a TMODE must apply simple channel TS rules to the TMODE.
A TS6 server must translate MODEs issued by a local client, or received from
a server into TMODE to send to other TS6 capable servers.

View file

@ -3,3 +3,5 @@ port = 6667
server_hostname = "irc.foo.bar"
network_name = "MyCoolFooNet" # this SHOULDN'T HAVE SPACES!
operators = []
server_incoming_passwords = ["unimpl"]
server_outgoing_password = "root"

View file

@ -14,7 +14,10 @@ impl IrcHandler for Cap {
_arguments: Vec<String>,
_authenticated: bool,
_user_state: &mut User,
) -> super::IrcAction {
IrcAction::DoNothing
_server_outgoing_password: String,
_server_incoming_passwords: Vec<String>,
_user_passwords: Vec<String>,
) -> Vec<super::IrcAction> {
vec![IrcAction::DoNothing]
}
}

View file

@ -16,7 +16,10 @@ impl IrcHandler for Join {
arguments: Vec<String>,
authenticated: bool,
user_state: &mut User,
) -> super::IrcAction {
_server_outgoing_password: String,
_server_incoming_passwords: Vec<String>,
_user_passwords: Vec<String>,
) -> Vec<super::IrcAction> {
let mut joined_channels = JOINED_CHANNELS.lock().await;
let mut channels = Vec::new();
@ -28,7 +31,7 @@ impl IrcHandler for Join {
}
if !authenticated {
return IrcAction::ErrorAuthenticateFirst;
return vec![IrcAction::ErrorAuthenticateFirst];
}
for existing_channel in joined_channels.clone() {
@ -53,6 +56,6 @@ impl IrcHandler for Join {
}
}
IrcAction::JoinChannels(channels)
vec![IrcAction::JoinChannels(channels)]
}
}

View file

@ -8,11 +8,12 @@ use crate::{
SENDER,
channels::Channel,
commands::{
cap::Cap, join::Join, nick::Nick, ping::Ping, privmsg::PrivMsg, user::User as UserHandler,
who::Who,
cap::Cap, join::Join, nick::Nick, pass::Pass, ping::Ping, privmsg::PrivMsg,
user::User as UserHandler, who::Who,
},
config::ServerInfo,
error_structs::CommandExecError,
messages::{JoinMessage, Message},
messages::{ChanJoinMessage, Message},
sender::IrcResponse,
user::User,
};
@ -20,6 +21,7 @@ use crate::{
mod cap;
mod join;
mod nick;
mod pass;
mod ping;
mod privmsg;
mod user;
@ -40,10 +42,17 @@ pub enum IrcAction {
SendText(IrcResponse),
SendMessage(Message),
JoinChannels(Vec<Channel>),
UpgradeToServerConn,
ErrorAuthenticateFirst,
DoNothing,
}
pub enum ReturnAction {
Nothing,
ServerConn,
CloseConn,
}
#[async_trait]
pub trait IrcHandler: Send + Sync {
async fn handle(
@ -51,17 +60,25 @@ pub trait IrcHandler: Send + Sync {
command: Vec<String>,
authenticated: bool,
user_state: &mut User,
) -> IrcAction;
server_outgoing_password: String,
server_incoming_passwords: Vec<String>,
user_passwords: Vec<String>,
) -> Vec<IrcAction>;
}
pub struct SendMessage(Option<String>);
impl IrcCommand {
pub fn new(command_with_arguments: String) -> Self {
let split_command: Vec<&str> = command_with_arguments
pub async fn new(command_with_arguments: String) -> Self {
let mut split_command: Vec<&str> = command_with_arguments
.split_whitespace()
.into_iter()
.collect();
if split_command[0].starts_with(":") {
split_command.remove(0);
}
let command = split_command[0].to_owned();
let mut arguments = Vec::new();
let mut buffer: Option<String> = None;
@ -94,7 +111,8 @@ impl IrcCommand {
writer: &mut BufWriter<TcpStream>,
hostname: &str,
user_state: &mut User,
) -> Result<(), CommandExecError> {
config: &ServerInfo,
) -> Result<Vec<ReturnAction>, CommandExecError> {
let mut command_map: HashMap<String, &dyn IrcHandler> = HashMap::new();
let broadcast_sender = SENDER.lock().await.clone().unwrap();
@ -106,6 +124,7 @@ impl IrcCommand {
command_map.insert("PING".to_owned(), &Ping);
command_map.insert("JOIN".to_owned(), &Join);
command_map.insert("WHO".to_owned(), &Who);
command_map.insert("PASS".to_owned(), &Pass);
println!("{self:#?}");
@ -114,18 +133,28 @@ impl IrcCommand {
.map(|v| *v)
.ok_or(CommandExecError::NonexistantCommand)?;
let action = command_to_execute
let actions = command_to_execute
.handle(
self.arguments.clone(),
user_state.is_populated(),
user_state,
config.server_outgoing_password.clone(),
config.server_incoming_passwords.clone(),
vec![], // TODO
)
.await;
action
.execute(writer, hostname, &user_state, broadcast_sender)
.await;
Ok(())
let mut return_actions = Vec::new();
for action in actions {
let return_action = action
.execute(writer, hostname, &user_state, broadcast_sender.clone())
.await;
return_actions.push(return_action);
}
Ok(return_actions)
}
}
@ -136,7 +165,7 @@ impl IrcAction {
hostname: &str,
user_state: &User,
sender: Sender<Message>,
) {
) -> ReturnAction {
match self {
IrcAction::SendText(msg) => {
msg.send(hostname, writer, false).await.unwrap();
@ -144,11 +173,11 @@ impl IrcAction {
IrcAction::JoinChannels(channels) => {
for channel in channels {
let join_message = JoinMessage {
let join_message = ChanJoinMessage {
sender: user_state.clone().unwrap_all(),
channel: channel.clone(),
};
sender.send(Message::JoinMessage(join_message)).unwrap();
sender.send(Message::ChanJoinMessage(join_message)).unwrap();
}
}
@ -156,7 +185,13 @@ impl IrcAction {
sender.send(msg.clone()).unwrap();
}
IrcAction::UpgradeToServerConn => {
return ReturnAction::ServerConn;
}
_ => {}
}
return ReturnAction::Nothing;
}
}

View file

@ -14,9 +14,26 @@ impl IrcHandler for Nick {
command: Vec<String>,
_authenticated: bool,
user_state: &mut User,
) -> IrcAction {
user_state.nickname = Some(command[0].clone());
_server_outgoing_password: String,
_server_incoming_passwords: Vec<String>,
_user_passwords: Vec<String>,
) -> Vec<IrcAction> {
user_state.nickname = Some({
if command[0].len() > 9 {
String::from_utf8(
command[0]
.clone()
.chars()
.map(|x| x.clone() as u8)
.collect::<Vec<u8>>()[0..8]
.to_vec(),
)
.unwrap()
} else {
command[0].clone()
}
});
IrcAction::DoNothing
vec![IrcAction::DoNothing]
}
}

36
src/commands/pass.rs Normal file
View file

@ -0,0 +1,36 @@
use async_trait::async_trait;
use crate::{
commands::{IrcAction, IrcHandler},
user::User,
};
pub struct Pass;
#[async_trait]
impl IrcHandler for Pass {
async fn handle(
&self,
command: Vec<String>,
_authenticated: bool,
_user_state: &mut User,
server_outgoing_password: String,
server_incoming_passwords: Vec<String>,
_user_passwords: Vec<String>,
) -> Vec<IrcAction> {
if server_incoming_passwords.contains(&command[0]) {
vec![
IrcAction::SendText(crate::sender::IrcResponse {
sender: None,
command: "PASS".to_owned(),
receiver: None,
arguments: Vec::new(),
message: server_outgoing_password.clone(),
}),
IrcAction::UpgradeToServerConn,
]
} else {
vec![IrcAction::DoNothing]
}
}
}

View file

@ -15,17 +15,20 @@ impl IrcHandler for Ping {
command: Vec<String>,
authenticated: bool,
user_state: &mut User,
) -> IrcAction {
_server_outgoing_password: String,
_server_incoming_passwords: Vec<String>,
_user_passwords: Vec<String>,
) -> Vec<IrcAction> {
if authenticated {
IrcAction::SendText(IrcResponse {
vec![IrcAction::SendText(IrcResponse {
sender: None,
command: "PONG".into(),
arguments: Vec::new(),
receiver: Some(user_state.username.clone().unwrap()),
message: format!(":{}", command[0].clone()),
})
})]
} else {
IrcAction::DoNothing
vec![IrcAction::DoNothing]
}
}
}

View file

@ -1,9 +1,8 @@
use async_trait::async_trait;
use crate::{
CONNECTED_USERS,
commands::{IrcAction, IrcHandler},
messages::{Message, PrivMessage},
messages::{Message, PrivMessage, Receiver},
user::User,
};
@ -16,21 +15,26 @@ impl IrcHandler for PrivMsg {
command: Vec<String>,
authenticated: bool,
user_state: &mut User,
) -> IrcAction {
_server_outgoing_password: String,
_server_incoming_passwords: Vec<String>,
_user_passwords: Vec<String>,
) -> Vec<IrcAction> {
if !authenticated {
return IrcAction::ErrorAuthenticateFirst;
return vec![IrcAction::ErrorAuthenticateFirst];
}
let connected_users = CONNECTED_USERS.lock().await;
println!("{connected_users:#?}");
drop(connected_users);
let receiver = if command[0].clone().starts_with("#") {
Receiver::ChannelName(command[0].clone())
} else {
Receiver::Username(command[0].clone())
};
let message = PrivMessage {
sender: user_state.clone().unwrap_all(),
receiver: command[0].clone(),
receiver,
text: command[1].clone(),
};
IrcAction::SendMessage(Message::PrivMessage(message))
vec![IrcAction::SendMessage(Message::PrivMessage(message))]
}
}

View file

@ -14,13 +14,32 @@ impl IrcHandler for User {
command: Vec<String>,
_authenticated: bool,
user_state: &mut UserState,
) -> IrcAction {
_server_outgoing_password: String,
_server_incoming_passwords: Vec<String>,
_user_passwords: Vec<String>,
) -> Vec<IrcAction> {
if command.len() < 4 {
return IrcAction::DoNothing; // XXX: return an error
return vec![IrcAction::DoNothing]; // XXX: return an error
}
user_state.username = Some(command[0].clone());
// oh my god this is a mess
user_state.username = Some({
if command[0].len() > 9 {
String::from_utf8(
command[0]
.clone()
.chars()
.map(|x| x.clone() as u8)
.collect::<Vec<u8>>()[0..8]
.to_vec(),
)
.unwrap()
} else {
command[0].clone()
}
});
user_state.realname = Some(command[3].clone());
IrcAction::DoNothing
vec![IrcAction::DoNothing]
}
}

View file

@ -14,7 +14,10 @@ impl IrcHandler for Who {
_arguments: Vec<String>,
_authenticated: bool,
_user_state: &mut User,
) -> super::IrcAction {
IrcAction::DoNothing // TODO
_server_outgoing_password: String,
_server_incoming_passwords: Vec<String>,
_user_passwords: Vec<String>,
) -> Vec<super::IrcAction> {
vec![IrcAction::DoNothing] // TODO
}
}

View file

@ -11,6 +11,8 @@ pub struct ServerInfo {
pub server_hostname: String,
pub network_name: String,
pub operators: Vec<String>,
pub server_incoming_passwords: Vec<String>,
pub server_outgoing_password: String,
}
fn get_config_path() -> Result<PathBuf, ConfigReadError> {

View file

@ -1,8 +1,9 @@
use std::{
clone,
collections::HashSet,
net::{SocketAddr, TcpListener, TcpStream},
str::FromStr,
time::Duration,
time::{Duration, SystemTime},
};
use anyhow::Error as AnyhowError;
@ -25,8 +26,13 @@ use crate::{
config::ServerInfo,
error_structs::{HandlerError, ListenerError},
login::send_motd,
messages::Message,
messages::Receiver as MsgReceiver,
messages::{Message, NetJoinMessage},
sender::{IrcResponse, IrcResponseCodes},
ts6::{
Ts6,
structs::{ServerId, UserId},
},
user::{User, UserUnwrapped},
};
@ -37,10 +43,15 @@ mod error_structs;
mod login;
mod messages;
mod sender;
mod ts6;
mod user;
mod userid_gen;
mod usermodes;
pub static CONNECTED_USERS: Lazy<Mutex<HashSet<UserUnwrapped>>> =
Lazy::new(|| Mutex::new(HashSet::new()));
pub static FOREIGN_CONNECTED_USERS: Lazy<Mutex<HashSet<UserUnwrapped>>> =
Lazy::new(|| Mutex::new(HashSet::new()));
pub static JOINED_CHANNELS: Lazy<Mutex<HashSet<Channel>>> =
Lazy::new(|| Mutex::new(HashSet::new()));
pub static SENDER: Lazy<Mutex<Option<Sender<Message>>>> = Lazy::new(|| Mutex::new(None));
@ -53,6 +64,11 @@ struct Args {
pub config_path: Option<String>,
}
enum TcpListenerResult {
UpdatedUser(User),
ServerConnectionInit,
}
#[tokio::main]
async fn main() -> Result<(), AnyhowError> {
#[cfg(feature = "tokio-console")]
@ -92,37 +108,79 @@ async fn handle_connection(
let mut message_receiver = tx.clone().subscribe();
let mut tcp_reader = TokioBufReader::new(TokioTcpStream::from_std(stream.try_clone()?)?);
let mut tcp_writer = TokioBufWriter::new(TokioTcpStream::from_std(stream)?);
let mut state = User::default();
let hostname = info.server_hostname.clone();
'connection_handler: {
let mut state = User::default();
loop {
tokio::select! {
result = tcp_listener(&stream_tcp, state.clone(), &info, &mut tcp_reader) => {
match result {
Ok(modified_user) => {
state = modified_user;
}
let hostname = info.server_hostname.clone();
Err(_) => {
break;
}
}
},
result = message_listener(&state, &mut message_receiver, &mut tcp_writer, &hostname) => {
match result {
Ok(_) => {},
Err(err) => {
match err {
ListenerError::ConnectionError => {
break;
// TODO: generate randomally and allow overriding from config
let my_server_id = ServerId::try_from("000".to_owned()).unwrap();
loop {
tokio::select! {
result = tcp_listener(&stream_tcp, state.clone(), &info, &mut tcp_reader, my_server_id.clone()) => {
match result {
Ok(tcp_listener_result) => {
match tcp_listener_result {
TcpListenerResult::UpdatedUser(user) => {
state = user;
}
TcpListenerResult::ServerConnectionInit => {
break;
}
}
}
_ => {}
};
Err(_) => {
break 'connection_handler;
}
}
}
},
},
result = message_listener(&state, &mut message_receiver, &mut tcp_writer, &hostname) => {
match result {
Ok(_) => {},
Err(err) => {
match err {
ListenerError::ConnectionError => {
break 'connection_handler;
}
_ => {}
};
}
}
},
}
}
println!("upgrade to server connection");
let mut ts6_server_status = Ts6::default();
loop {
tokio::select! {
result = ts6_server_status.tcp_listener(&stream_tcp, &info, &mut tcp_reader, &my_server_id) => {
match result {
Ok(new_status) => {
println!("{new_status:#?}");
ts6_server_status = new_status;
},
Err(_) => {
break;
}
}
},
result = ts6_server_status.message_listener(&mut message_receiver, &mut tcp_writer, &my_server_id, &hostname) => {
match result {
Ok(_) => {},
Err(_) => {
break;
}
}
},
}
}
}
@ -133,56 +191,89 @@ async fn handle_connection(
async fn tcp_listener(
stream: &TcpStream,
mut state: User,
mut user_state: User,
info: &ServerInfo,
reader: &mut TokioBufReader<TokioTcpStream>,
) -> Result<User, ListenerError> {
our_sid: ServerId,
) -> Result<TcpListenerResult, ListenerError> {
let mut buffer = String::new();
let mut writer = TokioBufWriter::new(TokioTcpStream::from_std(stream.try_clone()?)?);
buffer.clear();
match reader.read_line(&mut buffer).await {
Ok(0) => return Err(ListenerError::ConnectionError),
Ok(_) => {}
Err(_) => {
let mut conneted_users = CONNECTED_USERS.lock().await;
let _ = conneted_users.remove(&state.clone().unwrap_all());
let _ = conneted_users.remove(&user_state.clone().unwrap_all());
return Err(ListenerError::ConnectionError);
}
}
let command = commands::IrcCommand::new(buffer.clone());
let command = commands::IrcCommand::new(buffer.clone()).await;
match command
.execute(&mut writer, &info.server_hostname, &mut state)
.execute(&mut writer, &info.server_hostname, &mut user_state, info)
.await
{
Ok(_) => {}
Err(error) => {
let error_string = format!("error processing your command: {error:#?}\n");
let error = IrcResponseCodes::UnknownCommand;
Ok(return_actions) => {
for return_action in return_actions {
match return_action {
commands::ReturnAction::ServerConn => {
return Ok(TcpListenerResult::ServerConnectionInit);
}
error
.into_irc_response("*".into(), error_string.into())
.send(&info.server_hostname, &mut writer, true)
.await
.unwrap();
_ => {}
}
}
}
Err(error) => match error {
error_structs::CommandExecError::NonexistantCommand => {
let error_string = format!("error processing your command: {error:#?}\n");
let error = IrcResponseCodes::UnknownCommand;
error
.into_irc_response("*".into(), error_string.into())
.send(&info.server_hostname, &mut writer, true)
.await
.unwrap();
}
},
}
if !state.identified && state.is_populated() {
send_motd(info.clone(), state.clone(), &mut writer).await?;
if !user_state.identified && user_state.is_populated_without_uid() {
let id = userid_gen::increase_user_id()
.await
.unwrap()
.iter()
.map(|x| x.to_string())
.collect::<Vec<String>>()
.join("");
let user_id = format!("{our_sid}{id}");
user_state.identified = true;
user_state.user_id = Some(UserId::try_from(user_id).unwrap()); // XXX: error handling
user_state.timestamp = Some(SystemTime::now());
send_motd(info.clone(), user_state.clone(), &mut writer).await?;
let broadcast_sender = SENDER.lock().await.clone().unwrap();
broadcast_sender
.send(Message::NetJoinMessage(NetJoinMessage {
user: user_state.clone().unwrap_all(),
server_id: our_sid.clone(),
}))
.unwrap();
state.identified = true;
CONNECTED_USERS
.lock()
.await
.insert(state.clone().unwrap_all());
.insert(user_state.clone().unwrap_all());
}
Ok(state)
Ok(TcpListenerResult::UpdatedUser(user_state))
}
async fn message_listener(
@ -208,12 +299,32 @@ async fn message_listener(
match message {
Message::PrivMessage(message) => {
for channel in joined_channels.clone() {
if channel.joined_users.contains(user_wrapped) && channel.name == message.receiver {
if let MsgReceiver::ChannelName(channelname) = message.clone().receiver
&& channelname == channel.name
&& channel.joined_users.contains(user_wrapped)
{
channel_name = Some(channel.name.clone());
}
}
if user.nickname.clone().to_ascii_lowercase() == message.receiver.to_ascii_lowercase() {
dbg!(&message);
if match message.clone().receiver {
MsgReceiver::UserId(userid) => {
println!("{userid} ?= {}", user.user_id);
if userid == user.user_id { true } else { false }
}
MsgReceiver::Username(username) => {
if username.to_lowercase() == user.username.to_lowercase() {
true
} else {
false
}
}
_ => false,
} {
IrcResponse {
sender: Some(message.sender.hostmask()),
command: "PRIVMSG".into(),
@ -238,7 +349,7 @@ async fn message_listener(
}
}
Message::JoinMessage(message) => {
Message::ChanJoinMessage(message) => {
if message.channel.joined_users.contains(user_wrapped) || message.sender == user {
let channel = message.channel.clone();
@ -263,7 +374,35 @@ async fn message_listener(
.unwrap();
}
}
Message::NetJoinMessage(_) => {} // we don't care about these here :)
}
Ok(())
}
#[cfg(test)]
mod tests {
use crate::userid_gen;
#[tokio::test]
async fn test_user_id_generator() {
while let Ok(userid) = userid_gen::increase_user_id().await {
if userid == ['A', 'B', 'C', 'D', 'E', 'F'] {
userid_gen::manually_set_user_id(['Z', 'Z', 'Z', 'Z', 'Z', 'Y'].to_vec()).await;
break;
}
dbg!(userid);
}
while let Ok(userid) = userid_gen::increase_user_id().await {
if userid == ['A', '1', '2', '3', '4', '5'] {
// ff a bit
userid_gen::manually_set_user_id(['Z', '1', '2', '3', '4', '5'].to_vec()).await;
}
dbg!(userid);
}
}
}

View file

@ -1,22 +1,42 @@
use crate::{channels::Channel, user::UserUnwrapped};
use crate::{
channels::Channel,
ts6::structs::{ServerId, UserId},
user::UserUnwrapped,
};
#[derive(Debug, Clone)]
pub enum Message {
PrivMessage(PrivMessage),
JoinMessage(JoinMessage),
ChanJoinMessage(ChanJoinMessage),
NetJoinMessage(NetJoinMessage),
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct JoinMessage {
pub struct ChanJoinMessage {
pub sender: UserUnwrapped,
pub channel: Channel,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct NetJoinMessage {
pub user: UserUnwrapped,
pub server_id: ServerId,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct PrivMessage {
pub sender: UserUnwrapped,
pub receiver: String,
pub receiver: Receiver,
pub text: String,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub enum Receiver {
Username(String),
UserId(UserId),
ChannelName(String),
}

View file

@ -5,7 +5,7 @@ use tokio::{
use crate::error_structs::SenderError;
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct IrcResponse {
pub sender: Option<String>,
pub command: String,
@ -15,16 +15,17 @@ pub struct IrcResponse {
}
#[derive(Clone, Copy)]
#[repr(u16)]
pub enum IrcResponseCodes {
UnknownCommand,
Welcome,
YourHost,
MyInfo,
ISupport,
NoMotd,
NoTopic,
NameReply,
EndOfNames,
UnknownCommand = 421,
Welcome = 001,
YourHost = 002,
MyInfo = 004,
ISupport = 005,
NoMotd = 422,
NoTopic = 331,
NameReply = 353,
EndOfNames = 366,
}
impl IrcResponse {
@ -38,8 +39,8 @@ impl IrcResponse {
let mut full_response = Vec::new();
full_response.push(sender);
full_response.extend_from_slice(&self.arguments);
full_response.push(self.command.clone());
full_response.extend_from_slice(&self.arguments);
if let Some(receiver) = self.receiver.clone() {
full_response.push(receiver);
}
@ -52,29 +53,17 @@ impl IrcResponse {
writer.write_all(full_response.join(" ").as_bytes()).await?;
writer.flush().await?;
Ok(())
}
}
println!("sending: {full_response:#?}");
impl From<IrcResponseCodes> for &str {
fn from(value: IrcResponseCodes) -> Self {
match value {
IrcResponseCodes::UnknownCommand => "421",
IrcResponseCodes::Welcome => "001",
IrcResponseCodes::YourHost => "002",
IrcResponseCodes::MyInfo => "004",
IrcResponseCodes::ISupport => "005",
IrcResponseCodes::NoMotd => "422",
IrcResponseCodes::NoTopic => "331",
IrcResponseCodes::NameReply => "353",
IrcResponseCodes::EndOfNames => "366",
}
Ok(())
}
}
impl From<IrcResponseCodes> for String {
fn from(value: IrcResponseCodes) -> Self {
Into::<&str>::into(value).to_string()
let value = value as u16;
value.to_string()
}
}

38
src/ts6/commands/capab.rs Normal file
View file

@ -0,0 +1,38 @@
use crate::ts6::{
ServerId, Ts6,
commands::{CommandSender, Ts6Action, Ts6Handler},
};
use async_trait::async_trait;
pub struct Capab;
// TODO: handle capabilities
#[async_trait]
impl Ts6Handler for Capab {
async fn handle(
&self,
command: Vec<String>,
_server_status: Ts6,
_my_sid: ServerId,
sender: Option<CommandSender>,
_hostname: &str,
) -> Vec<Ts6Action> {
let args = {
let args_without_command = command[1..].to_vec();
if args_without_command.len() == 1 {
args_without_command[0]
.split_whitespace()
.map(|x| x.to_owned())
.collect::<Vec<String>>()
} else {
args_without_command
}
};
println!("{args:#?}");
vec![Ts6Action::DoNothing]
}
}

201
src/ts6/commands/mod.rs Normal file
View file

@ -0,0 +1,201 @@
use std::collections::HashMap;
use crate::{
SENDER,
commands::IrcMessage,
messages::Message,
sender::IrcResponse,
ts6::{
ServerId, Ts6,
commands::{
capab::Capab, ping::Ping, privmsg::Privmsg, server::Server, svinfo::Svinfo, uid::Uid,
},
structs::UserId,
},
};
use anyhow::anyhow;
use async_trait::async_trait;
use tokio::{io::BufWriter, net::TcpStream};
mod capab;
mod ping;
mod privmsg;
mod server;
mod svinfo;
mod uid;
#[derive(Clone, Debug)]
pub struct Ts6Info {
pub sid: Option<ServerId>,
pub hopcount: Option<u16>,
pub description: Option<String>,
pub name: Option<String>,
pub identified: Option<bool>,
}
#[derive(Clone, Debug)]
pub enum Ts6Action {
SetInfo(Ts6Info),
SendText(IrcResponse),
SendMessage(Message),
DoNothing,
}
#[async_trait]
pub trait Ts6Handler: Send + Sync {
async fn handle(
&self,
command: Vec<String>,
server_status: Ts6,
my_sid: ServerId,
sender: Option<CommandSender>,
hostname: &str,
) -> Vec<Ts6Action>;
}
#[derive(Debug)]
pub struct Ts6Command {
command: String,
arguments: Vec<String>,
sender: Option<CommandSender>,
}
#[derive(Clone, Debug)]
pub enum CommandSender {
User(UserId),
Server(ServerId),
}
impl Ts6Command {
pub async fn new(command_with_arguments: String) -> Self {
let mut command_sender = None;
let mut split_command: Vec<&str> = command_with_arguments
.split_whitespace()
.into_iter()
.collect();
if split_command[0].starts_with(":") {
let sender = split_command.remove(0).to_string().replace(":", "");
dbg!(&sender);
match sender.len() {
3 => {
if let Ok(sid) = ServerId::try_from(sender) {
command_sender = Some(CommandSender::Server(sid));
}
}
9 => {
if let Ok(uid) = UserId::try_from(sender) {
command_sender = Some(CommandSender::User(uid));
}
}
_ => {}
}
}
let command = split_command[0].to_owned();
let mut arguments = Vec::new();
let mut buffer: Option<String> = None;
split_command[1..]
.iter()
.for_each(|e| match (buffer.as_mut(), e.starts_with(":")) {
(None, false) => arguments.push(e.to_string()),
(None, true) => {
buffer = Some(e[1..].to_string());
}
(Some(buf), starts_with_colon) => {
buf.push(' ');
buf.push_str(if starts_with_colon { &e[1..] } else { &e });
}
});
if let Some(buf) = buffer {
arguments.push(buf.to_string());
}
Self {
command: command,
arguments: arguments,
sender: command_sender,
}
}
pub async fn execute(
&self,
ts6_status: &mut Ts6,
hostname: &str,
my_sid: &ServerId,
writer: &mut BufWriter<TcpStream>,
) -> Result<(), anyhow::Error> {
let mut command_map: HashMap<String, &dyn Ts6Handler> = HashMap::new();
let message_sender = SENDER.lock().await.clone().unwrap();
command_map.insert("CAPAB".to_owned(), &Capab);
command_map.insert("SERVER".to_owned(), &Server);
command_map.insert("PING".to_owned(), &Ping);
command_map.insert("SVINFO".to_owned(), &Svinfo);
command_map.insert("UID".to_owned(), &Uid);
command_map.insert("PRIVMSG".to_owned(), &Privmsg);
let command_to_execute = command_map
.get(&self.command.to_uppercase())
.map(|v| *v)
.ok_or(anyhow!("error"))?; // TODO: error handling!!!
let actions = command_to_execute
.handle(
self.arguments.clone(),
ts6_status.clone(),
ServerId::try_from(my_sid.clone().to_owned()).unwrap(),
self.sender.clone(),
hostname,
)
.await;
println!("{actions:#?}");
for action in actions {
match action {
Ts6Action::DoNothing => {}
Ts6Action::SetInfo(new_info) => {
if let Some(sid) = new_info.sid {
(*ts6_status).server_id = sid;
};
if let Some(hopcount) = new_info.hopcount {
(*ts6_status).hopcount = hopcount;
};
if let Some(name) = new_info.name {
(*ts6_status).hostname = name;
};
if let Some(description) = new_info.description {
(*ts6_status).description = description;
};
if let Some(identified) = new_info.identified {
(*ts6_status).identified = identified;
}
}
Ts6Action::SendText(response) => {
response
.send(&my_sid.to_string(), writer, false)
.await
.unwrap();
// TODO: error handling
}
Ts6Action::SendMessage(message) => {
message_sender.send(message.clone()).unwrap();
}
}
}
Ok(())
}
}

31
src/ts6/commands/ping.rs Normal file
View file

@ -0,0 +1,31 @@
use async_trait::async_trait;
use crate::{
sender::IrcResponse,
ts6::{
ServerId, Ts6,
commands::{CommandSender, Ts6Action, Ts6Handler},
},
};
pub struct Ping;
#[async_trait]
impl Ts6Handler for Ping {
async fn handle(
&self,
command: Vec<String>,
_server_status: Ts6,
my_sid: ServerId,
sender: Option<CommandSender>,
_hostname: &str,
) -> Vec<Ts6Action> {
vec![Ts6Action::SendText(IrcResponse {
sender: None,
command: "PONG".into(),
arguments: Vec::new(),
receiver: None,
message: format!("{my_sid} {}", command[0].clone()),
})]
}
}

View file

@ -0,0 +1,58 @@
use async_trait::async_trait;
use crate::{
FOREIGN_CONNECTED_USERS,
messages::{PrivMessage, Receiver},
ts6::{
Ts6,
commands::{CommandSender, Ts6Action, Ts6Handler},
structs::{ServerId, UserId},
},
user::UserUnwrapped,
};
pub struct Privmsg;
#[async_trait]
impl Ts6Handler for Privmsg {
async fn handle(
&self,
command: Vec<String>,
server_status: Ts6,
my_sid: ServerId,
sender: Option<CommandSender>,
hostname: &str,
) -> Vec<Ts6Action> {
'priv_msg: {
let mut sending_user: Option<UserUnwrapped> = None;
dbg!(&sender);
if let Ok(user_id) = UserId::try_from(command[0].clone()) {
if let Some(CommandSender::User(command_sender)) = sender {
let foreign_users = FOREIGN_CONNECTED_USERS.lock().await;
for user in foreign_users.iter() {
if user.user_id == command_sender {
sending_user = Some(user.clone())
}
}
} else {
dbg!("sender");
break 'priv_msg vec![];
}
vec![Ts6Action::SendMessage(
crate::messages::Message::PrivMessage(PrivMessage {
sender: sending_user.unwrap(),
receiver: Receiver::UserId(user_id),
text: command[1].clone(),
}),
)]
} else {
dbg!("userid");
vec![]
}
}
}
}

View file

@ -0,0 +1,58 @@
use crate::ts6::{
ServerId, Ts6,
commands::{CommandSender, Ts6Action, Ts6Handler},
};
use async_trait::async_trait;
pub struct Server;
// TODO: handle *ALL* params
#[async_trait]
impl Ts6Handler for Server {
async fn handle(
&self,
command: Vec<String>,
_server_status: Ts6,
my_sid: ServerId,
sender: Option<CommandSender>,
hostname: &str,
) -> Vec<Ts6Action> {
let name = Some(command[0].clone());
let hopcount = Some(command[1].parse::<u16>().unwrap());
let sid = {
if let Some(sid) = ServerId::try_from(command[2].clone()).ok() {
Some(sid)
} else {
None
}
};
let _flags = Some(command[3].clone());
let description = Some(command[4].clone());
println!("server cmd");
vec![
Ts6Action::SetInfo(super::Ts6Info {
sid,
hopcount,
description,
name,
identified: Some(true),
}),
Ts6Action::SendText(crate::sender::IrcResponse {
sender: Some(hostname.to_owned().clone()),
command: "SERVER".to_owned(),
receiver: None,
arguments: vec![
hostname.to_owned().clone(),
"1".to_owned(),
my_sid.clone().to_string(),
"+".to_owned(),
],
message: String::from(":TODO"),
}),
]
}
}

View file

@ -0,0 +1,47 @@
use std::time::SystemTime;
use crate::{
sender::IrcResponse,
ts6::{
ServerId, Ts6,
commands::{CommandSender, Ts6Action, Ts6Handler},
},
};
use async_trait::async_trait;
pub struct Svinfo;
const TS_CURRENT: u8 = 6;
const TS_MINIMUM: u8 = 6;
#[async_trait]
impl Ts6Handler for Svinfo {
async fn handle(
&self,
command: Vec<String>,
_server_status: Ts6,
_my_sid: ServerId,
sender: Option<CommandSender>,
_hostname: &str,
) -> Vec<Ts6Action> {
let ts_current = command[0].parse::<u8>().unwrap();
let ts_minimum = command[1].parse::<u8>().unwrap();
let current_time = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
// XXX: we need to properly disconnect with a QUIT message but we currently don't handle
// that.. we probably need a Ts6Action for that. same goes for regular irc commands
assert_eq!(ts_current, TS_CURRENT);
assert_eq!(ts_minimum, TS_MINIMUM);
vec![Ts6Action::SendText(IrcResponse {
sender: None,
command: "SVINFO".to_owned(),
receiver: None,
arguments: vec!["6".to_owned(), "6".to_owned(), "0".to_owned()],
message: format!(":{}", current_time),
})]
}
}

58
src/ts6/commands/uid.rs Normal file
View file

@ -0,0 +1,58 @@
use std::{
net::{IpAddr, Ipv4Addr},
str::FromStr,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use crate::{
FOREIGN_CONNECTED_USERS,
ts6::{
ServerId, Ts6,
commands::{CommandSender, Ts6Action, Ts6Handler},
structs::UserId,
},
user::UserUnwrapped,
usermodes::Usermodes,
};
use async_trait::async_trait;
pub struct Uid;
#[async_trait]
impl Ts6Handler for Uid {
async fn handle(
&self,
command: Vec<String>,
server_status: Ts6,
my_sid: ServerId,
sender: Option<CommandSender>,
hostname: &str,
) -> Vec<Ts6Action> {
let username = command[0].clone();
let hops = command[1].clone().parse::<u16>().unwrap();
let timestamp = UNIX_EPOCH + Duration::new(command[2].parse::<u64>().unwrap(), 0);
let usermodes = Usermodes::default(); // XXX
let ip = IpAddr::from_str(&command[7]).unwrap_or(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
let user_id = UserId::try_from(command[8].clone()).unwrap();
// TODO: error handling
let user = UserUnwrapped {
username: username.clone(),
nickname: username.clone(),
realname: username.clone(),
hopcount: hops,
identified: true,
user_id,
usermodes,
timestamp,
ip,
};
dbg!(&user);
let mut foreign_users = FOREIGN_CONNECTED_USERS.lock().await;
foreign_users.insert(user.clone());
vec![]
}
}

148
src/ts6/mod.rs Normal file
View file

@ -0,0 +1,148 @@
// TODO: better error handling
use std::{
net::TcpStream,
time::{Duration, UNIX_EPOCH},
};
use tokio::{
io::{AsyncBufReadExt, BufReader as TokioBufReader, BufWriter as TokioBufWriter},
net::TcpStream as TokioTcpStream,
sync::broadcast::Receiver,
time::sleep,
};
use crate::{
config::ServerInfo,
messages::Message,
sender::IrcResponse,
ts6::{commands::Ts6Command, structs::ServerId},
};
#[derive(Clone, Debug, Default)]
pub struct Ts6 {
pub server_id: ServerId,
pub hopcount: u16,
pub description: String,
pub hostname: String,
identified: bool,
}
mod commands;
pub mod structs;
impl Ts6 {
pub async fn handle_command(
&mut self,
_my_server_id: &ServerId,
args: String,
hostname: &str,
my_sid: &ServerId,
writer: &mut TokioBufWriter<TokioTcpStream>,
) {
println!("server command: {}", self.server_id);
let args = Ts6Command::new(args).await;
println!("args: {args:#?}");
// XXX
let result = args.execute(self, hostname, my_sid, writer).await;
if result.is_err() {
println!("{result:#?}");
}
}
pub async fn tcp_listener(
&self,
stream: &TcpStream,
info: &ServerInfo,
reader: &mut TokioBufReader<TokioTcpStream>,
my_server_id: &ServerId,
) -> Result<Ts6, anyhow::Error> {
let mut buffer = String::new();
let mut self_clone = self.clone();
let mut writer = TokioBufWriter::new(TokioTcpStream::from_std(stream.try_clone()?)?);
match reader.read_line(&mut buffer).await {
Ok(0) => anyhow::bail!(""),
Ok(_) => {}
Err(_) => {
anyhow::bail!("");
}
}
println!("ts6: {buffer}");
let args = buffer
.split_whitespace()
.map(|x| x.to_string())
.collect::<Vec<String>>();
self_clone
.handle_command(
my_server_id,
args.join(" "),
&info.server_hostname,
my_server_id,
&mut writer,
)
.await;
Ok(self_clone)
}
pub async fn message_listener(
&self,
receiver: &mut Receiver<Message>,
writer: &mut TokioBufWriter<TokioTcpStream>,
my_sid: &ServerId,
hostname: &str,
) -> Result<(), anyhow::Error> {
if !self.identified {
sleep(Duration::from_millis(250)).await; // avoid immediate returns b'cuz they result in high
// cpu usage
return Ok(()); // TODO: error handling
}
let message: Message = receiver.recv().await.unwrap();
match message {
Message::NetJoinMessage(net_join_message) => {
let user = net_join_message.user.clone();
// TODO: refactor this entire thing. we need hostmask and ip and such fully working
IrcResponse {
sender: Some(my_sid.clone().to_string()),
command: "UID".to_string(),
receiver: None,
arguments: vec![
user.nickname.clone(),
(user.hopcount + 1).to_string(),
user.timestamp
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
.to_string(),
user.usermodes.into(),
format!("~{}", user.username.clone()),
user.ip.to_string(),
user.ip.to_string(),
user.ip.to_string(),
user.user_id.to_string().clone(),
"*".to_owned(),
format!(":{}", user.username.clone()),
],
message: String::new(),
}
.send(hostname, writer, false)
.await
.unwrap();
}
_ => {}
}
Ok(())
}
}

215
src/ts6/structs.rs Normal file
View file

@ -0,0 +1,215 @@
const A_TO_Z: &'static [u8] = b"ABCDEFGHIJKLMNOPQRSTUVW";
const ZERO_TO_9: &'static [u8] = b"0123456789";
#[derive(Clone, Default, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct ServerId([char; 3]);
#[derive(Clone, Default, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct UserId([char; 9]);
impl UserId {
pub fn to_vec(&self) -> Vec<char> {
self.0.to_vec()
}
pub fn get_server_id(&self) -> ServerId {
let vector = self.to_vec();
let server_id_chars = vector[..3].to_vec();
let server_id = ServerId::try_from(server_id_chars).unwrap();
server_id
}
pub fn get_id(&self) -> Vec<char> {
let vector = self.to_vec();
let id_chars = vector[3..].to_vec();
id_chars
}
}
impl Into<String> for UserId {
fn into(self) -> String {
String::from_utf8_lossy(
self.to_vec()
.iter()
.map(|x| x.clone() as u8)
.collect::<Vec<u8>>()
.as_slice(),
)
.to_string()
}
}
impl TryFrom<String> for UserId {
type Error = &'static str;
fn try_from(value: String) -> Result<Self, Self::Error> {
dbg!(&value);
let chars = value.chars().into_iter().collect::<Vec<char>>();
if chars.len() != 9 || !ServerId::is_server_id(&value[..3]) {
return Err("string isn't a user id");
}
Ok(Self([
chars[0].clone(),
chars[1].clone(),
chars[2].clone(),
chars[3].clone(),
chars[4].clone(),
chars[5].clone(),
chars[6].clone(),
chars[7].clone(),
chars[8].clone(),
]))
}
}
impl TryFrom<Vec<char>> for UserId {
type Error = &'static str;
fn try_from(chars: Vec<char>) -> Result<Self, Self::Error> {
if chars.len() != 9
|| !ServerId::is_server_id(
&String::from_utf8_lossy(
&chars.iter().map(|x| x.clone() as u8).collect::<Vec<u8>>()[..3],
)
.to_string(),
)
{
return Err("string isn't a user id");
}
Ok(Self([
chars[0].clone(),
chars[1].clone(),
chars[2].clone(),
chars[3].clone(),
chars[4].clone(),
chars[5].clone(),
chars[6].clone(),
chars[7].clone(),
chars[8].clone(),
]))
}
}
impl std::fmt::Display for UserId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// We could just call our implementation of Into<String>, but as we can return an error
// here, this seems a better option
if let Some(string) = String::from_utf8(
self.to_vec()
.iter()
.map(|x| x.clone() as u8)
.collect::<Vec<u8>>(),
)
.ok()
{
f.write_str(&string)?;
} else {
return Err(std::fmt::Error);
}
Ok(())
}
}
impl ServerId {
pub fn to_vec(&self) -> Vec<char> {
self.0.to_vec()
}
// there might be a cleaner way to do this?
pub fn is_server_id(id: &str) -> bool {
let chars = id.chars().collect::<Vec<char>>();
if chars.len() != 3 {
return false;
}
if !ZERO_TO_9.contains(&(chars[0] as u8)) {
return false;
}
if !(A_TO_Z.contains(&(chars[1] as u8)) || ZERO_TO_9.contains(&(chars[1] as u8))) {
return false;
}
if !(A_TO_Z.contains(&(chars[2] as u8)) || ZERO_TO_9.contains(&(chars[2] as u8))) {
return false;
}
true
}
}
impl Into<String> for ServerId {
fn into(self) -> String {
String::from_utf8_lossy(
self.to_vec()
.iter()
.map(|x| x.clone() as u8)
.collect::<Vec<u8>>()
.as_slice(),
)
.to_string()
}
}
impl TryFrom<String> for ServerId {
type Error = &'static str;
fn try_from(value: String) -> Result<Self, Self::Error> {
let chars = value.chars().into_iter().collect::<Vec<char>>();
if chars.len() != 3 || !Self::is_server_id(&value) {
return Err("string isn't a server id");
}
Ok(Self([chars[0].clone(), chars[1].clone(), chars[2].clone()]))
}
}
impl TryFrom<Vec<char>> for ServerId {
type Error = &'static str;
fn try_from(chars: Vec<char>) -> Result<Self, Self::Error> {
if chars.len() != 3
|| !Self::is_server_id(
&String::from_utf8_lossy(
&chars.iter().map(|x| x.clone() as u8).collect::<Vec<u8>>(),
)
.to_string(),
)
{
return Err("string isn't a server id");
}
Ok(Self([chars[0].clone(), chars[1].clone(), chars[2].clone()]))
}
}
impl std::fmt::Display for ServerId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// We could just call our implementation of Into<String>, but as we can return an error
// here, this seems a better option
if let Some(string) = String::from_utf8(
self.to_vec()
.iter()
.map(|x| x.clone() as u8)
.collect::<Vec<u8>>(),
)
.ok()
{
f.write_str(&string)?;
} else {
return Err(std::fmt::Error);
}
Ok(())
}
}

View file

@ -1,11 +1,24 @@
#![allow(dead_code)]
use std::{
net::{IpAddr, Ipv4Addr},
time::SystemTime,
};
use crate::{ts6::structs::UserId, usermodes::Usermodes};
#[derive(Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
pub struct User {
pub nickname: Option<String>,
pub username: Option<String>,
pub realname: Option<String>,
pub identified: bool,
pub hopcount: Option<u16>,
pub user_id: Option<UserId>,
pub usermodes: Usermodes,
pub timestamp: Option<SystemTime>,
pub ip: Option<IpAddr>,
// pub hostname: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
@ -14,9 +27,19 @@ pub struct UserUnwrapped {
pub username: String,
pub realname: String,
pub identified: bool,
pub hopcount: u16,
pub user_id: UserId,
pub usermodes: Usermodes,
pub timestamp: SystemTime,
pub ip: IpAddr,
// pub hostname: Option<String>,
}
impl User {
pub fn is_populated_without_uid(&self) -> bool {
self.realname.is_some() && self.username.is_some() && self.nickname.is_some()
}
pub fn is_populated(&self) -> bool {
self.realname.is_some() && self.username.is_some() && self.nickname.is_some()
}
@ -27,6 +50,11 @@ impl User {
username: self.username.clone().unwrap(),
realname: self.realname.clone().unwrap(),
identified: self.identified,
hopcount: self.hopcount.clone().unwrap(),
user_id: self.user_id.clone().unwrap(),
usermodes: self.usermodes.clone(),
timestamp: self.timestamp.clone().unwrap(),
ip: self.ip.unwrap_or(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
}
}
@ -36,6 +64,11 @@ impl User {
username: None,
realname: None,
identified: false,
hopcount: Some(0),
user_id: None,
usermodes: Usermodes::default(),
timestamp: None,
ip: None,
}
}
}

77
src/userid_gen.rs Normal file
View file

@ -0,0 +1,77 @@
use once_cell::sync::Lazy;
use tokio::sync::Mutex;
use thiserror::Error;
static CURRENT_ID: Lazy<Mutex<Vec<char>>> =
Lazy::new(|| Mutex::new(vec!['A', 'A', 'A', 'A', 'A', 'A']));
static ZZZZZZ_REACHED: Lazy<Mutex<bool>> = Lazy::new(|| Mutex::new(false));
#[derive(Debug, Error)]
pub enum UidIncreaseError {
#[error("cap reached")]
UserCapReached,
}
pub async fn increase_user_id() -> Result<Vec<char>, UidIncreaseError> {
let mut current_id = CURRENT_ID.lock().await;
let mut zzzzzz_reached = ZZZZZZ_REACHED.lock().await;
let mut idx = 5;
'id_increaser: {
if !zzzzzz_reached.clone() {
loop {
if current_id[idx] != 'Z' {
current_id[idx] = (current_id[idx] as u8 + 1) as char;
break 'id_increaser;
} else {
if idx == 0 {
*zzzzzz_reached = true;
break;
}
current_id[idx] = 'A';
idx -= 1;
}
}
// if we get here, our id is ZZZZZZ and we need to start using numbers
idx = 5;
(*current_id) = vec!['A', '0', '0', '0', '0', '0'];
}
loop {
if idx != 0 {
if current_id[idx] != '9' {
current_id[idx] = (current_id[idx] as u8 + 1) as char;
break 'id_increaser;
} else {
current_id[idx] = '0';
idx -= 1;
}
} else {
if current_id[idx] != 'Z' {
current_id[idx] = (current_id[idx] as u8 + 1) as char;
idx = 5;
} else {
return Err(UidIncreaseError::UserCapReached);
}
}
}
}
Ok(current_id.to_vec())
}
// THIS SHOULD BE USED *ONLY* FOR TESTING PURPOSES! DO NOT USE IT IN PRODUCTION CODE!
#[allow(dead_code)]
pub async fn manually_set_user_id(user_id: Vec<char>) {
assert_eq!(user_id.len(), 6);
let mut lock = CURRENT_ID.lock().await;
(*lock) = user_id;
}

45
src/usermodes.rs Normal file
View file

@ -0,0 +1,45 @@
#[repr(u8)]
#[derive(Clone, Hash, PartialEq, Eq, Debug, Ord, PartialOrd)]
pub enum Usermode {
Invisible = b'i',
HostHiding = b'x',
}
#[derive(Clone, Hash, PartialEq, Eq, Debug, Ord, PartialOrd)]
pub struct Usermodes(Vec<Usermode>);
impl Into<Vec<String>> for Usermodes {
fn into(self) -> Vec<String> {
let mut vector: Vec<String> = vec![];
for i in self.0 {
vector.push(Into::<String>::into(i));
}
vector
}
}
impl Into<String> for Usermodes {
fn into(self) -> String {
format!("+{}", Into::<Vec<String>>::into(self).join(""))
}
}
impl Default for Usermodes {
fn default() -> Self {
Self(vec![Usermode::Invisible, Usermode::HostHiding])
}
}
impl Into<char> for Usermode {
fn into(self) -> char {
self as u8 as char
}
}
impl Into<String> for Usermode {
fn into(self) -> String {
Into::<char>::into(self).to_string()
}
}