From f7844edc6da50d13dad7d2e0bb909e323e330d08 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Sat, 7 Jan 2023 01:18:07 +0000 Subject: [PATCH] Initial commit --- .gitignore | 2 ++ Cargo.lock | 835 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 22 ++++++++++++++++++++++ rustfmt.toml | 7 +++++++ src/channel.rs | 196 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/channel/response.rs | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/client.rs | 381 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/connection.rs | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/messages.rs | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/server.rs | 129 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 1917 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 rustfmt.toml create mode 100644 src/channel.rs create mode 100644 src/channel/response.rs create mode 100644 src/client.rs create mode 100644 src/connection.rs create mode 100644 src/main.rs create mode 100644 src/messages.rs create mode 100644 src/server.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c403c34 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +.idea/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2a19415 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,835 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "actix" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f728064aca1c318585bf4bb04ffcfac9e75e508ab4e8b1bd9ba5dfe04e2cbed5" +dependencies = [ + "actix-rt", + "actix_derive", + "bitflags", + "bytes", + "crossbeam-channel", + "futures-core", + "futures-sink", + "futures-task", + "futures-util", + "log", + "once_cell", + "parking_lot", + "pin-project-lite", + "smallvec", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-rt" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ea16c295198e958ef31930a6ef37d0fb64e9ca3b6116e6b93a8bdae96ee1000" +dependencies = [ + "actix-macros", + "futures-core", + "tokio", +] + +[[package]] +name = "actix_derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d44b8fee1ced9671ba043476deddef739dd0959bf77030b26b738cc591737a7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "anyhow" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bytes" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + +[[package]] +name = "futures" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" + +[[package]] +name = "futures-executor" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" + +[[package]] +name = "futures-macro" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" + +[[package]] +name = "futures-task" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" + +[[package]] +name = "futures-util" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "irc-proto" +version = "0.15.0" +dependencies = [ + "bytes", + "encoding", + "thiserror", + "tokio", + "tokio-util", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mio" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro2" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "ryu" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" + +[[package]] +name = "serde_json" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] +name = "titanircd" +version = "0.1.0" +dependencies = [ + "actix", + "actix-rt", + "anyhow", + "futures", + "irc-proto", + "itertools", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tokio" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8457750 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "titanircd" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +actix = "0.13" +actix-rt = "2.7" +anyhow = "1.0" +futures = "0.3" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } +tokio = { version = "1.23", features = ["full"] } +tokio-stream = { version = "0.1", features = ["net"] } +tokio-util = { version = "0.7", features = ["codec"] } +irc-proto = "0.15" +itertools = "0.10" + +[patch."crates-io"] +irc-proto = { path = "../irc/irc-proto" } diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..d440511 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,7 @@ +edition = "2021" +## not yet supported on stable +#imports_granularity = "Crate" +newline_style = "Unix" +## not yet supported on stable +#group_imports = "StdExternalCrate" +use_field_init_shorthand = true diff --git a/src/channel.rs b/src/channel.rs new file mode 100644 index 0000000..1adc303 --- /dev/null +++ b/src/channel.rs @@ -0,0 +1,196 @@ +mod response; + +use std::collections::HashMap; + +use actix::{Actor, Addr, AsyncContext, Context, Handler, MessageResult}; +use irc_proto::Command; +use tracing::{error, info, instrument, Span}; + +use crate::{ + channel::response::{ChannelNamesList, ChannelTopic}, + client::Client, + connection::InitiatedConnection, + messages::{ + Broadcast, ChannelJoin, ChannelList, ChannelMessage, ChannelPart, ServerDisconnect, + }, +}; +use crate::messages::UserNickChange; + +/// A channel is an IRC channel (ie. #abc) that multiple users can connect to in order +/// to chat together. +pub struct Channel { + pub name: String, + pub clients: HashMap, InitiatedConnection>, +} + +impl Actor for Channel { + type Context = Context; +} + +/// Broadcast a raw IRC message to all clients connected to this channel. +impl Handler for Channel { + type Result = (); + + #[instrument(parent = &msg.span, skip_all)] + fn handle(&mut self, msg: Broadcast, _ctx: &mut Self::Context) -> Self::Result { + for client in self.clients.keys() { + client.try_send(msg.clone()).unwrap(); + } + } +} + +/// Sends back a list of users currently connected to the client +impl Handler for Channel { + type Result = MessageResult; + + #[instrument(parent = &msg.span, skip_all)] + fn handle(&mut self, msg: ChannelList, _ctx: &mut Self::Context) -> Self::Result { + MessageResult(self.clients.values().cloned().collect()) + } +} + +/// Broadcasts a message from a user to all users in the channel +impl Handler for Channel { + type Result = (); + + #[instrument(parent = &msg.span, skip_all)] + fn handle(&mut self, msg: ChannelMessage, _ctx: &mut Self::Context) -> Self::Result { + // ensure the user is actually in the channel by their handle, and grab their + // nick & host if they are + let Some(sender) = self.clients.get(&msg.client) else { + error!("Received message from user not in channel"); + return; + }; + + // build the nick prefix for the message we're about to broadcast + let nick = sender.to_nick(); + + for client in self.clients.keys() { + if client == &msg.client { + // don't echo the message back to the sender + continue; + } + + // broadcast the message to `client` + client.do_send(Broadcast { + span: Span::current(), + message: irc_proto::Message { + tags: None, + prefix: Some(nick.clone()), + command: Command::PRIVMSG(self.name.to_string(), msg.message.clone()), + }, + }); + } + } +} + +/// Recieved when a user changes their nick. +impl Handler for Channel { + type Result = (); + + fn handle(&mut self, msg: UserNickChange, _ctx: &mut Self::Context) -> Self::Result { + // grab the user's current info + let Some(sender) = self.clients.get_mut(&msg.client) else { + return; + }; + + // update the user's info with the latest `connection` details and new nick + *sender = msg.connection; + sender.nick = msg.new_nick; + } +} + +/// Received when a user is attempting to join the channel, broadcasts a message to all clients +/// informing them of the join. +/// +/// Sends the current topic & user list, and returns a handle to the channel so the user can +/// start sending us messages. +impl Handler for Channel { + type Result = MessageResult; + + #[instrument(parent = &msg.span, skip_all)] + fn handle(&mut self, msg: ChannelJoin, ctx: &mut Self::Context) -> Self::Result { + info!(self.name, msg.connection.nick, "User is joining channel"); + + self.clients + .insert(msg.client.clone(), msg.connection.clone()); + + // broadcast the user's join to everyone in the channel, including the joining user + for client in self.clients.keys() { + client.do_send(Broadcast { + span: Span::current(), + message: irc_proto::Message { + tags: None, + prefix: Some(msg.connection.to_nick()), + command: Command::JOIN(self.name.to_string(), None, None), + }, + }); + } + + // send the channel's topic to the joining user + msg.client.do_send(Broadcast { + message: ChannelTopic::new(self).into_message(self.name.to_string()), + span: Span::current(), + }); + + // send the user list to the user + for message in ChannelNamesList::new(self).into_messages(msg.connection.nick.to_string()) { + msg.client.do_send(Broadcast { + message, + span: Span::current(), + }); + } + + MessageResult(Ok(ctx.address())) + } +} + +/// Received when a client is parting the channel and broadcasts it to all connected users. +impl Handler for Channel { + type Result = (); + + #[instrument(parent = &msg.span, skip_all)] + fn handle(&mut self, msg: ChannelPart, ctx: &mut Self::Context) -> Self::Result { + let Some(client_info) = self.clients.remove(&msg.client) else { + return; + }; + + let message = Broadcast { + message: irc_proto::Message { + tags: None, + prefix: Some(client_info.to_nick()), + command: Command::PART(self.name.to_string(), msg.message), + }, + span: Span::current(), + }; + + // send the part message to both the parting user and other clients + msg.client.do_send(message.clone()); + ctx.notify(message); + } +} + +/// Received when a client is disconnecting from the server and broadcasts it to all connected +/// users. +impl Handler for Channel { + type Result = (); + + #[instrument(parent = &msg.span, skip_all)] + fn handle(&mut self, msg: ServerDisconnect, ctx: &mut Self::Context) -> Self::Result { + let Some(client_info) = self.clients.remove(&msg.client) else { + return; + }; + + let message = Broadcast { + span: Span::current(), + message: irc_proto::Message { + tags: None, + prefix: Some(client_info.to_nick()), + command: Command::QUIT(msg.message), + }, + }; + + // send the part message to all other clients + ctx.notify(message); + } +} diff --git a/src/channel/response.rs b/src/channel/response.rs new file mode 100644 index 0000000..6163f47 --- /dev/null +++ b/src/channel/response.rs @@ -0,0 +1,72 @@ +use irc_proto::{Command, Message, Prefix, Response}; + +use crate::{channel::Channel, SERVER_NAME}; + +pub struct ChannelTopic { + channel_name: String, + topic: String, +} + +impl ChannelTopic { + pub fn new(channel: &Channel) -> Self { + Self { + channel_name: channel.name.to_string(), + topic: "hello world!".to_string(), + } + } + + pub fn into_message(self, for_user: String) -> Message { + irc_proto::Message { + tags: None, + prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), + command: Command::Response( + Response::RPL_TOPIC, + vec![for_user, self.channel_name, self.topic], + ), + } + } +} + +pub struct ChannelNamesList { + channel_name: String, + nick_list: Vec, +} + +impl ChannelNamesList { + pub fn new(channel: &Channel) -> Self { + Self { + channel_name: channel.name.to_string(), + nick_list: channel + .clients + .values() + .map(|v| v.nick.to_string()) + .collect(), + } + } + + pub fn into_messages(self, for_user: String) -> Vec { + vec![ + irc_proto::Message { + tags: None, + prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), + command: Command::Response( + Response::RPL_NAMREPLY, + vec![ + for_user.to_string(), + "=".to_string(), + self.channel_name, + self.nick_list.join(" "), + ], + ), + }, + Message { + tags: None, + prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), + command: Command::Response( + Response::RPL_ENDOFNAMES, + vec![for_user, "End of /NAMES list".to_string()], + ), + }, + ] + } +} diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..9485a6b --- /dev/null +++ b/src/client.rs @@ -0,0 +1,381 @@ +use std::{collections::HashMap, time::Duration}; + +use actix::{ + fut::wrap_future, io::WriteHandler, Actor, ActorContext, ActorFutureExt, Addr, AsyncContext, + Context, Handler, MessageResult, ResponseActFuture, Running, StreamHandler, +}; +use futures::FutureExt; +use irc_proto::{error::ProtocolError, ChannelExt, Command, Message}; +use tokio::time::Instant; +use tracing::{error, info_span, instrument, warn, Instrument, Span, debug}; + +use crate::{ + channel::Channel, + connection::{InitiatedConnection, MessageSink}, + messages::{ + Broadcast, ChannelJoin, ChannelMessage, ChannelPart, FetchClientDetails, ServerDisconnect, + UserNickChange, + }, + server::Server, + SERVER_NAME, +}; + +/// A client refers to a single connection to the server. +/// +/// This client has a handle to the server to inform it of leaves, and to request handles to +/// channels. +pub struct Client { + /// The TcpStream writer half for sending responses to the client + pub writer: MessageSink, + /// Details about the user's connection, including their nick + pub connection: InitiatedConnection, + /// A handle to the root actor for arbitration between clients and channels + pub server: Addr, + /// A list of channels the user is currently connected to + pub channels: HashMap>, + /// The time of the last ping we received from the client + pub last_active: Instant, + /// Whether the client is shutting down due to the client calling QUIT, or whether the server + /// terminated the connection + pub graceful_shutdown: bool, + /// The reason the client is leaving the server, whether this is set by the server or the user + /// is decided by graceful_shutdown + pub server_leave_reason: Option, + /// The connection span to group all logs for the same connection + pub span: Span, +} + +impl Actor for Client { + type Context = Context; + + /// Called when the actor is first started (ie. when the client connects). + /// + /// We currently just use this to schedule pings towards the client. + #[instrument(parent = &self.span, skip_all)] + fn started(&mut self, ctx: &mut Self::Context) { + // schedule pings to the client + ctx.run_interval(Duration::from_secs(30), |this, ctx| { + let _span = info_span!(parent: &this.span, "ping").entered(); + + if Instant::now().duration_since(this.last_active) > Duration::from_secs(120) { + this.server_leave_reason = Some("Ping timeout: 120 seconds".to_string()); + ctx.stop(); + } + + this.writer.write(Message { + tags: None, + prefix: None, + command: Command::PING(SERVER_NAME.to_string(), None), + }); + }); + } + + /// Called when the actor is shutting down, either gracefully by the client or forcefully + /// by the server. + #[instrument(parent = &self.span, skip_all)] + fn stopped(&mut self, ctx: &mut Self::Context) { + let message = self.server_leave_reason.take(); + + // inform the server that the user is leaving the server + self.server.do_send(ServerDisconnect { + client: ctx.address(), + message: message.clone(), + span: Span::current(), + }); + + // inform all channels the client is connected to of them leaving the server + for channel in self.channels.values() { + channel.do_send(ServerDisconnect { + client: ctx.address(), + message: message.clone(), + span: Span::current(), + }); + } + + // send the shutdown message to the client before we terminate the connection on + // return of this function + if self.graceful_shutdown { + self.writer.write(Message { + tags: None, + prefix: Some(self.connection.to_nick()), + command: Command::QUIT(message), + }); + } else { + let message = message.unwrap_or_else(|| "Ungraceful shutdown".to_string()); + + self.writer.write(Message { + tags: None, + prefix: None, + command: Command::ERROR(message), + }); + } + } +} + +/// TODO: Despite the name of the message, writes a unicast message to the client. +impl Handler for Client { + type Result = (); + + #[instrument(parent = &msg.span, skip_all)] + fn handle(&mut self, msg: Broadcast, _ctx: &mut Self::Context) -> Self::Result { + self.writer.write(msg.message); + } +} + +/// Returns the client's current nick/connection info. +impl Handler for Client { + type Result = MessageResult; + + #[instrument(parent = &msg.span, skip_all)] + fn handle(&mut self, msg: FetchClientDetails, _ctx: &mut Self::Context) -> Self::Result { + MessageResult(self.connection.clone()) + } +} + +/// A self-message from the Client's [`StreamHandler`] implementation when the user +/// sends a request command out. +/// +/// This will block the user from performing any actions until they're connected to the +/// channel due to us awaiting on the join handles. +impl Handler for Client { + type Result = ResponseActFuture; + + #[instrument(parent = &msg.span, skip_all)] + fn handle(&mut self, msg: JoinChannelRequest, ctx: &mut Self::Context) -> Self::Result { + let mut futures = Vec::with_capacity(msg.channels.len()); + + // loop over all the channels and send a channel join notification to the root + // server actor to get a handle back + for channel_name in msg.channels { + if !channel_name.is_channel_name() { + // todo: send message to client informing them of the invalid channel name + continue; + } + + futures.push( + self.server + .clone() + .send(ChannelJoin { + channel_name: channel_name.to_string(), + client: ctx.address(), + connection: self.connection.clone(), + span: Span::current(), + }) + .map(move |v| (channel_name, v.unwrap().unwrap())), + ); + } + + // await on all the `ChannelJoin` events to the server, and once we get the channel + // handles back write them to the server + let fut = wrap_future::<_, Self>( + futures::future::join_all(futures.into_iter()).instrument(Span::current()), + ) + .map(|result, this, _ctx| { + for (channel_name, handle) in result { + this.channels.insert(channel_name.clone(), handle); + } + }); + + Box::pin(fut) + } +} + +/// A message received from the root server to indicate that another known user has changed their +/// nick +impl Handler for Client { + type Result = (); + + #[instrument(parent = &msg.span, skip_all)] + fn handle(&mut self, msg: UserNickChange, _ctx: &mut Self::Context) -> Self::Result { + self.writer.write(Message { + tags: None, + prefix: Some(msg.connection.to_nick()), + command: Command::NICK(msg.new_nick), + }); + } +} + +/// Receives messages from the user's incoming TCP stream and processes them, passing them onto +/// other actors or self-notifying and calling a [`Handler`]. +impl StreamHandler> for Client { + #[instrument(parent = &self.span, skip_all)] + fn handle(&mut self, item: Result, ctx: &mut Self::Context) { + // unpack the message from the client + let item = match item { + Ok(item) => { + debug!(?item, "Received message from client"); + item + } + Err(error) => { + error!(%error, "Client sent a bad message"); + return; + } + }; + + // ensure that the message from the client is either a global message (ie. a ping) or + // has the correct nick (ie. it isn't spoofed or desynced) + if item + .source_nickname() + .map_or(false, |v| v != self.connection.nick) + { + warn!("Rejecting message from client due to incorrect nick"); + return; + } + + // https://modern.ircdocs.horse/ + #[allow(clippy::match_same_arms)] + match item.command { + Command::USER(_, _, _) | Command::PASS(_) | Command::CAP(_, _, _, _) => { + // these were already handled by `negotiate_client_connection` + } + Command::NICK(new_nick) => { + // alert the server to the nick change (we'll receive this event back so the user + // gets the notification too) + self.server.do_send(UserNickChange { + client: ctx.address(), + connection: self.connection.clone(), + new_nick: new_nick.clone(), + span: Span::current(), + }); + + for channel in self.channels.values() { + channel.do_send(UserNickChange { + client: ctx.address(), + connection: self.connection.clone(), + new_nick: new_nick.clone(), + span: Span::current(), + }); + } + + // updates our nick locally + self.connection.nick = new_nick; + } + Command::OPER(_, _) => {} + Command::UserMODE(_, _) => {} + Command::SERVICE(_, _, _, _, _, _) => {} + Command::QUIT(message) => { + // set the user's leave reason and request a shutdown of the actor to close the + // connection + self.graceful_shutdown = true; + self.server_leave_reason = message; + ctx.stop(); + } + Command::SQUIT(_, _) => {} + Command::JOIN(channel_names, _passwords, _real_name) => { + // split the list of channel names... + let channels = channel_names + .split(',') + .filter(|v| !v.is_empty()) + .map(ToString::to_string) + .collect(); + + // ...and send a self-notification to schedule those joins + ctx.notify(JoinChannelRequest { + channels, + span: Span::current(), + }); + } + Command::PART(channel, message) => { + // remove the handle from the users locally connected channels + let Some(channel) = self.channels.remove(&channel) else { + return; + }; + + // alert the channel to our leave + channel.do_send(ChannelPart { + client: ctx.address(), + message, + span: Span::current(), + }); + } + Command::ChannelMODE(_, _) => {} + Command::TOPIC(_, _) => {} + Command::NAMES(_, _) => {} + Command::LIST(_, _) => {} + Command::INVITE(_, _) => {} + Command::KICK(_, _, _) => {} + Command::PRIVMSG(target, message) => { + if !target.is_channel_name() { + // private message to another user + error!("Private messages not implemented"); + } else if let Some(channel) = self.channels.get(&target) { + channel.do_send(ChannelMessage { + client: ctx.address(), + message, + span: Span::current(), + }); + } else { + // user not connected to channel + error!("User not connected to channel"); + } + } + Command::NOTICE(_, _) => {} + Command::MOTD(_) => {} + Command::LUSERS(_, _) => {} + Command::VERSION(_) => {} + Command::STATS(_, _) => {} + Command::LINKS(_, _) => {} + Command::TIME(_) => {} + Command::CONNECT(_, _, _) => {} + Command::TRACE(_) => {} + Command::ADMIN(_) => {} + Command::INFO(_) => {} + Command::SERVLIST(_, _) => {} + Command::SQUERY(_, _) => {} + Command::WHO(_, _) => {} + Command::WHOIS(_, _) => {} + Command::WHOWAS(_, _, _) => {} + Command::KILL(_, _) => {} + Command::PING(_, _) => {} + Command::PONG(_, _) => { + self.last_active = Instant::now(); + } + Command::ERROR(_) => {} + Command::AWAY(_) => {} + Command::REHASH => {} + Command::DIE => {} + Command::RESTART => {} + Command::SUMMON(_, _, _) => {} + Command::USERS(_) => {} + Command::WALLOPS(_) => {} + Command::USERHOST(_) => {} + Command::ISON(_) => {} + Command::SAJOIN(_, _) => {} + Command::SAMODE(_, _, _) => {} + Command::SANICK(_, _) => {} + Command::SAPART(_, _) => {} + Command::SAQUIT(_, _) => {} + Command::NICKSERV(_) => {} + Command::CHANSERV(_) => {} + Command::OPERSERV(_) => {} + Command::BOTSERV(_) => {} + Command::HOSTSERV(_) => {} + Command::MEMOSERV(_) => {} + Command::AUTHENTICATE(_) => {} + Command::ACCOUNT(_) => {} + Command::METADATA(_, _, _) => {} + Command::MONITOR(_, _) => {} + Command::BATCH(_, _, _) => {} + Command::CHGHOST(_, _) => {} + Command::Response(_, _) => {} + Command::Raw(_, _) => {} + } + } +} + +/// Sent to us by actix whenever we fail to write a message to the client's outgoing tcp stream +impl WriteHandler for Client { + #[instrument(parent = &self.span, skip_all)] + fn error(&mut self, error: ProtocolError, _ctx: &mut Self::Context) -> Running { + error!(%error, "Failed to write message to client"); + Running::Continue + } +} + +/// An [`Client`] internal self-notification to schedule channel joining +#[derive(actix::Message, Debug)] +#[rtype(result = "()")] +pub struct JoinChannelRequest { + channels: Vec, + span: Span, +} diff --git a/src/connection.rs b/src/connection.rs new file mode 100644 index 0000000..6a2205d --- /dev/null +++ b/src/connection.rs @@ -0,0 +1,97 @@ +use actix::io::FramedWrite; +use futures::TryStreamExt; +use irc_proto::{error::ProtocolError, Command, Message, Prefix}; +use tokio::{ + io::{ReadHalf, WriteHalf}, + net::TcpStream, +}; +use tokio_util::codec::FramedRead; +use tracing::{instrument, warn}; + +pub type MessageStream = FramedRead, irc_proto::IrcCodec>; +pub type MessageSink = FramedWrite, irc_proto::IrcCodec>; + +#[derive(Default)] +pub struct ConnectionRequest { + nick: Option, + user: Option, + mode: Option, + real_name: Option, +} + +#[derive(Clone)] +pub struct InitiatedConnection { + pub nick: String, + pub user: String, + pub mode: String, + pub real_name: String, +} + +impl InitiatedConnection { + #[must_use] + pub fn to_nick(&self) -> Prefix { + Prefix::Nickname( + self.nick.to_string(), + self.user.to_string(), + "my-host".to_string(), + ) + } +} + +impl TryFrom for InitiatedConnection { + type Error = ConnectionRequest; + + fn try_from(value: ConnectionRequest) -> Result { + let ConnectionRequest { + nick: Some(nick), + user: Some(user), + mode: Some(mode), + real_name: Some(real_name), + } = value else { + return Err(value); + }; + + Ok(Self { + nick, + user, + mode, + real_name, + }) + } +} + +/// Currently just awaits client preamble (nick, user), but can be expanded to negotiate +/// capabilities with the client in the future. +#[instrument(skip_all)] +pub async fn negotiate_client_connection( + s: &mut MessageStream, +) -> Result, ProtocolError> { + let mut request = ConnectionRequest::default(); + + while let Some(msg) = s.try_next().await? { + #[allow(clippy::match_same_arms)] + match msg.command { + Command::PASS(_) => {} + Command::NICK(nick) => request.nick = Some(nick), + Command::USER(user, mode, real_name) => { + request.user = Some(user); + request.mode = Some(mode); + request.real_name = Some(real_name); + } + Command::CAP(_, _, _, _) => {} + _ => { + warn!(?msg, "Client sent unknown command during negotiation"); + } + } + + match InitiatedConnection::try_from(std::mem::take(&mut request)) { + Ok(v) => return Ok(Some(v)), + Err(v) => { + // connection isn't fully initiated yet... + request = v; + } + } + } + + Ok(None) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..269868c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,92 @@ +#![deny(clippy::nursery, clippy::pedantic)] +#![allow(clippy::module_name_repetitions)] + +use std::collections::HashMap; + +use actix::{io::FramedWrite, Actor, Addr, AsyncContext}; +use actix_rt::System; +use irc_proto::IrcCodec; +use tokio::{net::TcpListener, time::Instant}; +use tokio_util::codec::FramedRead; +use tracing::{error, info, info_span, Instrument}; +use tracing_subscriber::EnvFilter; + +use crate::{client::Client, messages::UserConnected, server::Server}; + +pub mod channel; +pub mod client; +pub mod connection; +pub mod messages; +pub mod server; + +pub const SERVER_NAME: &str = "my.cool.server"; + +#[actix_rt::main] +async fn main() -> anyhow::Result<()> { + let subscriber = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .pretty(); + subscriber.init(); + + let server = Server::default().start(); + let listener = TcpListener::bind("127.0.0.1:6697").await?; + + actix_rt::spawn(start_tcp_acceptor_loop(listener, server)); + + info!("Server listening on 127.0.0.1:6697"); + + tokio::signal::ctrl_c().await?; + System::current().stop(); + + Ok(()) +} + +/// Start listening for new connections from clients, and create a new client handle for +/// them. +async fn start_tcp_acceptor_loop(listener: TcpListener, server: Addr) { + while let Ok((stream, addr)) = listener.accept().await { + let span = info_span!("connection", %addr); + let _entered = span.clone().entered(); + + info!("Accepted connection"); + + let server = server.clone(); + + actix_rt::spawn(async move { + // split the stream into its read and write halves and setup codecs + let (read, writer) = tokio::io::split(stream); + let mut read = FramedRead::new(read, IrcCodec::new("utf8").unwrap()); + + // ensure we have all the details required to actually connect the client to the server + // (ie. we have a nick, user, etc) + let Some(connection) = connection::negotiate_client_connection(&mut read).await.unwrap() else { + error!("Failed to fully handshake with client, dropping connection"); + return; + }; + + // spawn the client's actor + let handle = Client::create(|ctx| { + // setup the writer codec for the user + let writer = FramedWrite::new(writer, IrcCodec::new("utf8").unwrap(), ctx); + + // add the user's incoming tcp stream to the actor, messages over the tcp stream + // will be sent to the actor over the `StreamHandler` + ctx.add_stream(read); + + Client { + writer, + connection: connection.clone(), + server: server.clone(), + channels: HashMap::new(), + last_active: Instant::now(), + graceful_shutdown: false, + server_leave_reason: None, + span: span.clone(), + } + }); + + // inform the server of the new connection + server.do_send(UserConnected { handle, connection, span }); + }.instrument(info_span!("negotiation"))); + } +} diff --git a/src/messages.rs b/src/messages.rs new file mode 100644 index 0000000..0f1e965 --- /dev/null +++ b/src/messages.rs @@ -0,0 +1,84 @@ +use actix::{Addr, Message}; +use anyhow::Result; +use tracing::Span; + +use crate::{channel::Channel, client::Client, connection::InitiatedConnection}; + +/// Sent when a user is connecting to the server. +#[derive(Message, Clone)] +#[rtype(message = "()")] +pub struct UserConnected { + pub handle: Addr, + pub connection: InitiatedConnection, + pub span: Span, +} + +/// Sent to both the `Server` and all connected `Channel`s when the user disconnects from +/// the server. +#[derive(Message, Clone)] +#[rtype(message = "()")] +pub struct ServerDisconnect { + pub client: Addr, + pub message: Option, + pub span: Span, +} + +/// Sent when the user changes their nick. +#[derive(Message, Clone)] +#[rtype(result = "()")] +pub struct UserNickChange { + pub client: Addr, + pub connection: InitiatedConnection, + pub new_nick: String, + pub span: Span, +} + +/// Sent when the user attempts to join a channel. +#[derive(Message)] +#[rtype(result = "Result>")] +pub struct ChannelJoin { + pub channel_name: String, + pub client: Addr, + pub connection: InitiatedConnection, + pub span: Span, +} + +/// Sent when the user parts a channel. +#[derive(Message)] +#[rtype(result = "()")] +pub struct ChannelPart { + pub client: Addr, + pub message: Option, + pub span: Span, +} + +/// Retrieves the list of users currently in a channel. +#[derive(Message)] +#[rtype(result = "Vec")] +pub struct ChannelList { + pub span: Span, +} + +/// Sends a raw irc message to a channel/user. +#[derive(Message, Clone)] +#[rtype(result = "()")] +pub struct Broadcast { + pub message: irc_proto::Message, + pub span: Span, +} + +/// Fetches the user's current connection info (nick, host, etc) +#[derive(Message)] +#[rtype(result = "crate::connection::InitiatedConnection")] +pub struct FetchClientDetails { + pub span: Span, +} + +/// Sends a message to a channel. +#[derive(Message)] +#[rtype(result = "()")] +pub struct ChannelMessage { + pub client: Addr, + pub message: String, + pub span: Span, +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..27df3ac --- /dev/null +++ b/src/server.rs @@ -0,0 +1,129 @@ +use std::collections::{HashMap, HashSet}; + +use actix::{Actor, Addr, Context, Handler, ResponseFuture}; +use futures::TryFutureExt; +use irc_proto::{Command, Message, Prefix, Response}; +use tracing::{instrument, Span}; + +use crate::{ + channel::Channel, + client::Client, + messages::{Broadcast, ChannelJoin, ServerDisconnect, UserConnected, UserNickChange}, + SERVER_NAME, +}; + +/// The root actor for arbitration between clients and channels. +#[derive(Default)] +pub struct Server { + channels: HashMap>, + clients: HashSet>, +} + +/// Received when a user connects to the server, and sends them the server preamble +impl Handler for Server { + type Result = (); + + #[instrument(parent = &msg.span, skip_all)] + fn handle(&mut self, msg: UserConnected, _ctx: &mut Self::Context) -> Self::Result { + // send a welcome to the user + let responses = [ + ( + Response::RPL_WELCOME, + vec!["Welcome to the network jordan!jordan@proper.sick.kid"], + ), + (Response::RPL_YOURHOST, vec!["Your host is a sick kid"]), + ( + Response::RPL_CREATED, + vec!["This server was created at some point"], + ), + ( + Response::RPL_MYINFO, + vec![ + SERVER_NAME, + "0.0.1", + "DOQRSZaghilopsuwz", + "CFILMPQSbcefgijklmnopqrstuvz", + "bkloveqjfI", + ], + ), + ( + Response::RPL_ISUPPORT, + vec!["D", "are supported by this server"], + ), + ]; + + for (response, arguments) in responses { + // fixme: bad perf here with inserting at the front of a vec + let arguments = std::iter::once(msg.connection.nick.clone()) + .chain(arguments.into_iter().map(ToString::to_string)) + .collect(); + + msg.handle.do_send(Broadcast { + span: Span::current(), + message: Message { + tags: None, + prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), + command: Command::Response(response, arguments), + }, + }); + } + + self.clients.insert(msg.handle); + } +} + +/// Received when a client disconnects from the server +impl Handler for Server { + type Result = (); + + #[instrument(parent = &msg.span, skip_all)] + fn handle(&mut self, msg: ServerDisconnect, _ctx: &mut Self::Context) -> Self::Result { + self.clients.remove(&msg.client); + } +} + +/// Received when a client is attempting to join a channel, and forwards it onto the requested +/// channel for it to handle -- creating it if it doesn't already exist. +impl Handler for Server { + type Result = ResponseFuture<::Result>; + + #[instrument(parent = &msg.span, skip_all)] + fn handle(&mut self, msg: ChannelJoin, _ctx: &mut Self::Context) -> Self::Result { + let channel = self + .channels + .entry(msg.channel_name.clone()) + .or_insert_with(|| { + Channel { + name: msg.channel_name.clone(), + clients: HashMap::new(), + } + .start() + }) + .clone(); + + Box::pin( + channel + .send(msg) + .map_err(anyhow::Error::new) + .and_then(futures::future::ready), + ) + } +} + +/// Received when a client changes their nick and forwards it on to all other users connected to +/// the server. +impl Handler for Server { + type Result = (); + + #[instrument(parent = &msg.span, skip_all)] + fn handle(&mut self, msg: UserNickChange, _ctx: &mut Self::Context) -> Self::Result { + // inform all clients of the nick change + for client in &self.clients { + client.do_send(msg.clone()); + } + } +} + +impl Actor for Server { + type Context = Context; +} -- libgit2 1.7.2