From 8b285671492300a6535cc0997727dbf118d829b0 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Sun, 8 Jan 2023 15:22:29 +0000 Subject: [PATCH] Implement SASL authentication for usernames --- .gitignore | 1 + Cargo.lock | 672 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 5 +++++ build.rs | 3 +++ config.toml | 1 + migrations/2023010814480_initial-schema.sql | 6 ++++++ src/config.rs | 1 + src/connection.rs | 334 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------- src/database/mod.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------- src/server.rs | 1 + 11 files changed, 1136 insertions(+), 24 deletions(-) create mode 100644 build.rs create mode 100644 migrations/2023010814480_initial-schema.sql create mode 100644 src/database/mod.rs diff --git a/.gitignore b/.gitignore index d3e5a9a..62d4854 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target .idea/ /result +/titanircd.db diff --git a/Cargo.lock b/Cargo.lock index 0c5ea6f..738938e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ dependencies = [ "futures-util", "log", "once_cell", - "parking_lot", + "parking_lot 0.12.1", "pin-project-lite", "smallvec", "tokio", @@ -59,6 +59,17 @@ dependencies = [ ] [[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -74,24 +85,86 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" [[package]] +name = "argon2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4ce4441f99dbd377ca8a8f57b698c44d0d6e712d8329b5040da5a64aa1ce73" +dependencies = [ + "base64ct", + "blake2", + "password-hash", +] + +[[package]] +name = "atoi" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e" +dependencies = [ + "num-traits", +] + +[[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339eb223c8f495cef5b03f1727754538a468edaeba28bab0886ecadca774a3b7" + +[[package]] +name = "base64ct" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" + +[[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + +[[package]] name = "bumpalo" version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" [[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] name = "bytes" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -172,12 +245,56 @@ dependencies = [ ] [[package]] +name = "const_format" +version = "0.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7309d9b4d3d2c0641e018d449232f2e28f1b22933c137f157d3dbc14228b8c0e" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f47bf7270cf70d370f8f98c1abb6d2d4cf60a6845d30e05bfb90c6568650" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] name = "core-foundation-sys" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53757d12b596c16c78b83458d732a5d1a17ab3f53f2f7412f6fb57cc8a140ab3" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff" + +[[package]] name = "crossbeam-channel" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -188,6 +305,16 @@ dependencies = [ ] [[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] name = "crossbeam-utils" version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -197,6 +324,16 @@ dependencies = [ ] [[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] name = "cxx" version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -241,6 +378,23 @@ dependencies = [ ] [[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dotenvy" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0" + +[[package]] name = "either" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -332,6 +486,33 @@ dependencies = [ ] [[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "pin-project", + "spin 0.9.4", +] + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] name = "futures" version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -374,6 +555,17 @@ dependencies = [ ] [[package]] +name = "futures-intrusive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot 0.11.2", +] + +[[package]] name = "futures-io" version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -421,6 +613,16 @@ dependencies = [ ] [[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] name = "getrandom" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -432,10 +634,31 @@ dependencies = [ ] [[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" +dependencies = [ + "hashbrown", +] + +[[package]] name = "heck" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "hermit-abi" @@ -447,6 +670,12 @@ dependencies = [ ] [[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] name = "iana-time-zone" version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -471,6 +700,35 @@ dependencies = [ ] [[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] name = "io-lifetimes" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -541,6 +799,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] +name = "libsqlite3-sys" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] name = "link-cplusplus" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -590,6 +859,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] name = "mio" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -602,6 +877,16 @@ dependencies = [ ] [[package]] +name = "nom" +version = "7.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5507769c4919c998e69e49c839d9dc6e693ede4cc4290d6ad8b41d4f09c548c" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] name = "nu-ansi-term" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -660,12 +945,37 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[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", + "parking_lot_core 0.9.5", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", ] [[package]] @@ -682,6 +992,49 @@ dependencies = [ ] [[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba" + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "pin-project-lite" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -694,6 +1047,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -805,6 +1164,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] name = "rustix" version = "0.36.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -819,6 +1193,27 @@ dependencies = [ ] [[package]] +name = "rustls" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" +dependencies = [ + "base64 0.13.1", +] + +[[package]] name = "ryu" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -837,6 +1232,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" [[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] name = "serde" version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -868,6 +1273,17 @@ dependencies = [ ] [[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] name = "sharded-slab" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -911,12 +1327,140 @@ dependencies = [ ] [[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlformat" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87e292b4291f154971a43c3774364e2cbcaec599d3f5bf6fa9d122885dbc38a" +dependencies = [ + "itertools", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9249290c05928352f71c077cc44a464d880c63f26f7534728cca008e135c0428" +dependencies = [ + "sqlx-core", + "sqlx-macros", +] + +[[package]] +name = "sqlx-core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbc16ddba161afc99e14d1713a453747a2b07fc097d2009f4c300ec99286105" +dependencies = [ + "ahash", + "atoi", + "bitflags", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "dotenvy", + "either", + "event-listener", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "hashlink", + "hex", + "indexmap", + "itoa", + "libc", + "libsqlite3-sys", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rustls", + "rustls-pemfile", + "sha2", + "smallvec", + "sqlformat", + "sqlx-rt", + "stringprep", + "thiserror", + "tokio-stream", + "url", + "webpki-roots", +] + +[[package]] +name = "sqlx-macros" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b850fa514dc11f2ee85be9d055c512aa866746adfacd1cb42d867d68e6a5b0d9" +dependencies = [ + "dotenvy", + "either", + "heck", + "once_cell", + "proc-macro2", + "quote", + "sha2", + "sqlx-core", + "sqlx-rt", + "syn", + "url", +] + +[[package]] +name = "sqlx-rt" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24c5b2d25fa654cc5f841750b8e1cdedbe21189bf9a9382ee90bfa9dd3562396" +dependencies = [ + "once_cell", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "stringprep" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] name = "syn" version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -977,19 +1521,39 @@ dependencies = [ ] [[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] name = "titanircd" version = "0.1.0" dependencies = [ "actix", "actix-rt", "anyhow", + "argon2", + "base64 0.21.0-rc.1", + "bytes", "chrono", "clap", + "const_format", "futures", "irc-proto", "itertools", "rand", "serde", + "sqlx", "tokio", "tokio-stream", "tokio-util", @@ -1010,7 +1574,7 @@ dependencies = [ "memchr", "mio", "num_cpus", - "parking_lot", + "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", "socket2", @@ -1030,6 +1594,17 @@ dependencies = [ ] [[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + +[[package]] name = "tokio-stream" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1139,24 +1714,86 @@ dependencies = [ ] [[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] name = "unicode-ident" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" [[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" + +[[package]] name = "unicode-width" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] name = "valuable" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1229,6 +1866,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" [[package]] +name = "web-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + +[[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml index 2ac2898..16acba7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,11 +9,16 @@ edition = "2021" actix = "0.13" actix-rt = "2.7" anyhow = "1.0" +base64 = "0.21.0-rc.1" +bytes = "1.3" +const_format = "0.2" chrono = "0.4" clap = { version = "4.0", features = ["cargo", "derive", "std", "suggestions", "color"] } futures = "0.3" +argon2 = "0.4" rand = "0.8" serde = { version = "1.0", features = ["derive"] } +sqlx = { version = "0.6", features = ["runtime-actix-rustls", "sqlite", "any"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } toml = "0.5" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..3a8149e --- /dev/null +++ b/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rerun-if-changed=migrations"); +} diff --git a/config.toml b/config.toml index 3c33392..15850ef 100644 --- a/config.toml +++ b/config.toml @@ -1,4 +1,5 @@ listen-address = "[::]:6667" +database-uri = "sqlite://titanircd.db" client-threads = 1 channel-threads = 1 diff --git a/migrations/2023010814480_initial-schema.sql b/migrations/2023010814480_initial-schema.sql new file mode 100644 index 0000000..9f0ab38 --- /dev/null +++ b/migrations/2023010814480_initial-schema.sql @@ -0,0 +1,6 @@ +CREATE TABLE users ( + username VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL +); + +CREATE UNIQUE INDEX users_username ON users(username); diff --git a/src/config.rs b/src/config.rs index 35f6dd0..daf3e7d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -17,6 +17,7 @@ pub struct Args { #[serde(rename_all = "kebab-case")] pub struct Config { pub listen_address: SocketAddr, + pub database_uri: String, pub motd: Option, /// Amount of threads to spawn for processing client commands, set to 0 to spawn clients on the /// main server thread. diff --git a/src/connection.rs b/src/connection.rs index df7d9d9..1c3293c 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -1,6 +1,16 @@ +use std::{ + io::{Error, ErrorKind}, + str::FromStr, +}; + use actix::io::FramedWrite; -use futures::TryStreamExt; -use irc_proto::{error::ProtocolError, Command, Message, Prefix}; +use argon2::PasswordHash; +use base64::{prelude::BASE64_STANDARD, Engine}; +use const_format::concatcp; +use futures::{SinkExt, TryStreamExt}; +use irc_proto::{ + error::ProtocolError, CapSubCommand, Command, IrcCodec, Message, Prefix, Response, +}; use tokio::{ io::{ReadHalf, WriteHalf}, net::TcpStream, @@ -8,9 +18,13 @@ use tokio::{ use tokio_util::codec::FramedRead; use tracing::{instrument, warn}; +use crate::database::verify_password; + pub type MessageStream = FramedRead, irc_proto::IrcCodec>; pub type MessageSink = FramedWrite, irc_proto::IrcCodec>; +pub const SUPPORTED_CAPABILITIES: &[&str] = &[concatcp!("sasl=", AuthStrategy::SUPPORTED)]; + #[derive(Default)] pub struct ConnectionRequest { nick: Option, @@ -65,10 +79,21 @@ impl TryFrom for InitiatedConnection { #[instrument(skip_all)] pub async fn negotiate_client_connection( s: &mut MessageStream, + write: &mut tokio_util::codec::FramedWrite, IrcCodec>, + database: sqlx::Pool, ) -> Result, ProtocolError> { let mut request = ConnectionRequest::default(); - while let Some(msg) = s.try_next().await? { + let mut capabilities_requested = false; + + // wait for the initiating commands from the user, giving us their NICK & USER and the user + // requesting the server's capabilities - any clients not requesting capabilities are not + // supported, as SASL auth is required + let initiated = loop { + let Some(msg) = s.try_next().await? else { + break None; + }; + #[allow(clippy::match_same_arms)] match msg.command { Command::PASS(_) => {} @@ -78,21 +103,314 @@ pub async fn negotiate_client_connection( request.mode = Some(mode); request.real_name = Some(real_name); } - Command::AUTHENTICATE(_) => {} - Command::CAP(_, _, _, _) => {} + Command::CAP(_, CapSubCommand::LIST | CapSubCommand::LS, _, _) => { + capabilities_requested = true; + + write + .send(Message { + tags: None, + prefix: None, + command: Command::CAP( + Some("*".to_string()), + CapSubCommand::LS, + None, + Some(SUPPORTED_CAPABILITIES.join(" ")), + ), + }) + .await + .unwrap(); + } _ => { warn!(?msg, "Client sent unknown command during negotiation"); } - } + }; match InitiatedConnection::try_from(std::mem::take(&mut request)) { - Ok(v) => return Ok(Some(v)), + Ok(v) => break Some(v), Err(v) => { // connection isn't fully initiated yet... request = v; } } + }; + + // if the user closed the connection before the connection was fully established, + // return back early + let Some(initiated) = initiated else { + return Ok(None); + }; + + if !capabilities_requested { + return Err(ProtocolError::Io(Error::new( + ErrorKind::InvalidData, + "capabilities not requested by client, so SASL authentication can not be performed", + ))); + } + + let mut has_authenticated = false; + + // start negotiating capabilities with the client + while let Some(msg) = s.try_next().await? { + match msg.command { + Command::CAP(_, CapSubCommand::REQ, Some(arguments), None) => { + write + .send(AcknowledgedCapabilities(arguments).into_message()) + .await?; + } + Command::CAP(_, CapSubCommand::END, _, _) => { + break; + } + Command::AUTHENTICATE(strategy) => { + has_authenticated = + start_authenticate_flow(s, write, &initiated, strategy, &database).await?; + } + _ => { + return Err(ProtocolError::Io(Error::new( + ErrorKind::InvalidData, + format!("client sent non-cap message during negotiation {msg:?}"), + ))) + } + } + } + + if has_authenticated { + Ok(Some(initiated)) + } else { + Err(ProtocolError::Io(Error::new( + ErrorKind::InvalidData, + "user has not authenticated", + ))) + } +} + +/// When the client has given us a strategy to use, we can start the authentication flow. +/// +/// This function will return true or false, depending on whether authentication was successful, +/// or an `Err` if an internal error occurs. +async fn start_authenticate_flow( + s: &mut MessageStream, + write: &mut tokio_util::codec::FramedWrite, IrcCodec>, + connection: &InitiatedConnection, + strategy: String, + database: &sqlx::Pool, +) -> Result { + let Ok(auth_strategy) = AuthStrategy::from_str(&strategy) else { + write.send(SaslStrategyUnsupported(connection.nick.to_string()).into_message()) + .await?; + return Ok(false); + }; + + // tell the client to go ahead with their authentication + write + .send(Message { + tags: None, + prefix: None, + command: Command::AUTHENTICATE("+".to_string()), + }) + .await?; + + // consume all AUTHENTICATE messages from the client + while let Some(msg) = s.try_next().await? { + let Command::AUTHENTICATE(arguments) = msg.command else { + return Err(ProtocolError::Io(Error::new( + ErrorKind::InvalidData, + format!("client sent invalid message during authentication {msg:?}"), + ))); + }; + + // user has cancelled authentication + if arguments == "*" { + write + .send(SaslAborted(connection.nick.to_string()).into_message()) + .await?; + break; + } + + let authenticated = match auth_strategy { + AuthStrategy::Plain => { + // TODO: this needs to deal with the case where the full arguments can be split over + // multiple messages + handle_plain_authentication(&arguments, connection, database).await? + } + }; + + if authenticated { + write + .send(SaslSuccess(connection.nick.to_string()).into_message()) + .await?; + + return Ok(true); + } + + write + .send(SaslFail(connection.nick.to_string()).into_message()) + .await?; + } + + Ok(false) +} + +/// Attempts to handle an `AUTHENTICATE` command for the `PLAIN` authentication method. +/// +/// This will parse the full message, ensure that the identity is correct and compare the hashes +/// to what we have stored in the database. +pub async fn handle_plain_authentication( + arguments: &str, + connection: &InitiatedConnection, + database: &sqlx::Pool, +) -> Result { + let arguments = BASE64_STANDARD + .decode(arguments) + .map_err(|e| Error::new(ErrorKind::InvalidData, e))?; + + // split the PLAIN message into its respective parts + let mut message = arguments.splitn(3, |f| *f == b'\0'); + let (Some(authorization_identity), Some(authentication_identity), Some(password)) = (message.next(), message.next(), message.next()) else { + return Err(Error::new(ErrorKind::InvalidData, "bad plain message")); + }; + + // we don't want any ambiguity here, so we only identities matching the `USER` command + if authorization_identity != connection.user.as_bytes() + || authentication_identity != connection.user.as_bytes() + { + return Err(Error::new(ErrorKind::InvalidData, "username mismatch")); + } + + // lookup the user's password based on the USER command they sent earlier + let password_hash = crate::database::fetch_password_hash(database, &connection.user) + .await + .unwrap(); + let password_hash = password_hash + .as_deref() + .map(PasswordHash::new) + .transpose() + .unwrap(); + let Some(password_hash) = password_hash else { + // this is a new user, so we'll create an account for them + // TODO: we need to deal with races here, right now we'll just error out on dup + crate::database::create_user(database, &connection.user, password).await.unwrap(); + + return Ok(true); + }; + + // check the user's password + match verify_password(password, &password_hash) { + Ok(()) => Ok(true), + Err(argon2::password_hash::Error::Password) => Ok(false), + Err(e) => Err(Error::new(ErrorKind::InvalidData, e.to_string())), + } +} + +/// Return an acknowledgement to the client for their requested capabilities. +pub struct AcknowledgedCapabilities(String); + +impl AcknowledgedCapabilities { + #[must_use] + pub fn into_message(self) -> Message { + Message { + tags: None, + prefix: None, + command: Command::CAP( + Some("*".to_string()), + CapSubCommand::ACK, + None, + Some(self.0), + ), + } } +} + +/// A requested SASL strategy. +#[derive(Copy, Clone, Debug)] +pub enum AuthStrategy { + Plain, +} - Ok(None) +impl AuthStrategy { + /// A list of all supported SASL strategies. + pub const SUPPORTED: &'static str = "PLAIN"; +} + +/// Parse a SASL strategy from the wire. +impl FromStr for AuthStrategy { + type Err = std::io::Error; + + fn from_str(s: &str) -> Result { + match s { + "PLAIN" => Ok(Self::Plain), + _ => Err(Error::new(ErrorKind::InvalidData, "unknown auth strategy")), + } + } +} + +/// Returned to the client when an invalid SASL strategy is attempted. +pub struct SaslStrategyUnsupported(String); + +impl SaslStrategyUnsupported { + #[must_use] + pub fn into_message(self) -> Message { + Message { + tags: None, + prefix: None, + command: Command::Response( + Response::RPL_SASLMECHS, + vec![ + self.0, + AuthStrategy::SUPPORTED.to_string(), + "are available SASL mechanisms".to_string(), + ], + ), + } + } +} + +/// Returned to the client when authentication is successful. +pub struct SaslSuccess(String); + +impl SaslSuccess { + #[must_use] + pub fn into_message(self) -> Message { + Message { + tags: None, + prefix: None, + command: Command::Response( + Response::RPL_SASLSUCCESS, + vec![self.0, "SASL authentication successful".to_string()], + ), + } + } +} + +/// Returned to the client when SASL authentication fails. +pub struct SaslFail(String); + +impl SaslFail { + #[must_use] + pub fn into_message(self) -> Message { + Message { + tags: None, + prefix: None, + command: Command::Response( + Response::ERR_SASLFAIL, + vec![self.0, "SASL authentication failed".to_string()], + ), + } + } +} + +/// Returned to the client when they abort SASL. +pub struct SaslAborted(String); + +impl SaslAborted { + #[must_use] + pub fn into_message(self) -> Message { + Message { + tags: None, + prefix: None, + command: Command::Response( + Response::ERR_SASLABORT, + vec![self.0, "SASL authentication aborted".to_string()], + ), + } + } } diff --git a/src/database/mod.rs b/src/database/mod.rs new file mode 100644 index 0000000..0fb5ba7 --- /dev/null +++ b/src/database/mod.rs @@ -0,0 +1,52 @@ +use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; +use rand::rngs::OsRng; +use sqlx::{database::HasArguments, Database, Encode, Executor, FromRow, IntoArguments, Type}; + +/// Fetches the given user's password from the database. +pub async fn fetch_password_hash<'a, E: Executor<'a>>( + conn: E, + username: &'a str, +) -> Result, sqlx::Error> +where + for<'b> &'b str: Type + Encode<'b, E::Database>, + >::Arguments: IntoArguments<'a, E::Database>, + for<'b> (String,): FromRow<'b, ::Row>, +{ + let res = sqlx::query_as("SELECT password FROM users WHERE username = ?") + .bind(username) + .fetch_optional(conn) + .await? + .map(|(v,)| v); + + Ok(res) +} + +/// Creates a new user, returning an error if the user already exists. +pub async fn create_user<'a, E: Executor<'a>>( + conn: E, + username: &'a str, + password: &[u8], +) -> Result<(), sqlx::Error> +where + for<'b> &'b str: Type + Encode<'b, E::Database>, + for<'b> String: Type + Encode<'b, E::Database>, + >::Arguments: IntoArguments<'a, E::Database>, +{ + let salt = SaltString::generate(&mut OsRng); + let password_hash = Argon2::default() + .hash_password(password, &salt) + .unwrap() + .to_string(); + + sqlx::query("INSERT INTO users (username, password) VALUES (?, ?)") + .bind(username) + .bind(password_hash) + .execute(conn) + .await + .map(|_| ()) +} + +/// Compares a password to a hash stored in the database. +pub fn verify_password(password: &[u8], hash: &PasswordHash) -> argon2::password_hash::Result<()> { + Argon2::default().verify_password(password, hash) +} diff --git a/src/main.rs b/src/main.rs index 4b9f2a0..dd746c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,24 @@ #![deny(clippy::nursery, clippy::pedantic)] -#![allow(clippy::module_name_repetitions)] +#![allow( + clippy::module_name_repetitions, + clippy::missing_panics_doc, + clippy::missing_errors_doc +)] -use std::{collections::HashMap, sync::Arc}; +use std::{collections::HashMap, str::FromStr, sync::Arc}; use actix::{io::FramedWrite, Actor, Addr, AsyncContext, Supervisor}; use actix_rt::{Arbiter, System}; +use bytes::BytesMut; use clap::Parser; use irc_proto::IrcCodec; use rand::seq::SliceRandom; -use tokio::{net::TcpListener, time::Instant}; +use sqlx::migrate::Migrator; +use tokio::{ + io::WriteHalf, + net::{TcpListener, TcpStream}, + time::Instant, +}; use tokio_util::codec::FramedRead; use tracing::{error, info, info_span, Instrument}; use tracing_subscriber::EnvFilter; @@ -19,11 +29,14 @@ pub mod channel; pub mod client; pub mod config; pub mod connection; +pub mod database; pub mod messages; pub mod server; pub const SERVER_NAME: &str = "my.cool.server"; +static MIGRATOR: Migrator = sqlx::migrate!(); + #[actix_rt::main] async fn main() -> anyhow::Result<()> { // parse CLI arguments @@ -45,18 +58,41 @@ async fn main() -> anyhow::Result<()> { .pretty(); subscriber.init(); + let database = sqlx::Pool::connect_with({ + let mut options = sqlx::any::AnyConnectOptions::from_str(&opts.config.database_uri)?; + + if let Some(sqlite_options) = options.as_sqlite_mut() { + *sqlite_options = sqlite_options.clone().create_if_missing(true); + } + + options + }) + .await?; + + MIGRATOR.run(&database).await?; + let listen_address = opts.config.listen_address; let client_threads = opts.config.client_threads; - let server = Supervisor::start_in_arbiter(&Arbiter::new().handle(), |_ctx| Server { - channels: HashMap::default(), - clients: HashMap::default(), - channel_arbiters: build_arbiters(opts.config.channel_threads), - config: opts.config, - }); + let server = { + let database = database.clone(); + + Supervisor::start_in_arbiter(&Arbiter::new().handle(), move |_ctx| Server { + channels: HashMap::default(), + clients: HashMap::default(), + channel_arbiters: build_arbiters(opts.config.channel_threads), + config: opts.config, + database, + }) + }; let listener = TcpListener::bind(listen_address).await?; - actix_rt::spawn(start_tcp_acceptor_loop(listener, server, client_threads)); + actix_rt::spawn(start_tcp_acceptor_loop( + listener, + database, + server, + client_threads, + )); info!("Server listening on {}", listen_address); @@ -70,6 +106,7 @@ async fn main() -> anyhow::Result<()> { /// them. async fn start_tcp_acceptor_loop( listener: TcpListener, + database: sqlx::Pool, server: Addr, client_threads: usize, ) { @@ -81,17 +118,19 @@ async fn start_tcp_acceptor_loop( info!("Accepted connection"); + let database = database.clone(); let server = server.clone(); let client_arbiters = client_arbiters.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()); + let mut read = FramedRead::new(read, irc_codec()); + let mut write = tokio_util::codec::FramedWrite::new(writer, irc_codec()); // 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 { + let Some(connection) = connection::negotiate_client_connection(&mut read, &mut write, database).await.unwrap() else { error!("Failed to fully handshake with client, dropping connection"); return; }; @@ -105,7 +144,8 @@ async fn start_tcp_acceptor_loop( Client::start_in_arbiter(&arbiter, move |ctx| { // setup the writer codec for the user - let writer = FramedWrite::new(writer, IrcCodec::new("utf8").unwrap(), ctx); + let (stream, codec, buffer) = unpack_writer(write); + let writer = FramedWrite::from_buffer(stream, codec, buffer, 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` @@ -130,6 +170,24 @@ async fn start_tcp_acceptor_loop( } } +/// Unpacks a tokio framed writer, for instantiating an Actix framed writer once connection +/// instantiation is complete. +#[must_use] +pub fn unpack_writer( + mut writer: tokio_util::codec::FramedWrite, IrcCodec>, +) -> (WriteHalf, IrcCodec, BytesMut) { + let codec = std::mem::replace(writer.encoder_mut(), irc_codec()); + let bytes = writer.write_buffer_mut().split(); + let stream = writer.into_inner(); + + (stream, codec, bytes) +} + +#[must_use] +pub fn irc_codec() -> IrcCodec { + IrcCodec::new("utf8").unwrap() +} + #[must_use] pub fn build_arbiters(count: usize) -> Vec { std::iter::repeat(()) diff --git a/src/server.rs b/src/server.rs index b2a0208..b78cec7 100644 --- a/src/server.rs +++ b/src/server.rs @@ -34,6 +34,7 @@ pub struct Server { pub channels: HashMap>, pub clients: HashMap, InitiatedConnection>, pub config: Config, + pub database: sqlx::Pool, } impl Supervised for Server {} -- libgit2 1.7.2