From 81b9abe4952a98a738e4971c27df0b49a705bc2d Mon Sep 17 00:00:00 2001 From: Ryan Daum Date: Wed, 1 Nov 2023 18:44:56 -0400 Subject: [PATCH] Phase 1 of auth/auth work All RPC requests to the daemon now have attached Client and Auth PASETO tokens. The former is for validating that the RPC connection is who it says it is. The latter is for validating that the user is who they say they are. It is now possible to get an auth token from logging in through `/ws/auth` endpoint. Though there's nothing yet in place to use it. The idea is that websocket connections (and HTTP generally as well) will work this way: 1) auth and get a PASETO token for the player that is auth'd to. 2) subsequent calls to the system -- such as a websocket attach, or a request to retrieve a property or verb, etc.) will attach the auth token in a `X-Moor-Auth-Token` header on each request. TODO: * validate the player inside the Scheduler/Task layer before launching a task (and return E_PERM etc.) * add a websocket connect that uses the authorization header to skip login, and remove the Basic-Auth (which can't work from a browser now anyways) (https://github.com/rdaum/moor/issues/30) --- Cargo.lock | 376 ++++++++++++++++++---------- Cargo.toml | 6 +- crates/README.md | 3 +- crates/console-host/Cargo.toml | 3 +- crates/console-host/src/main.rs | 75 ++++-- crates/daemon/Cargo.toml | 3 + crates/daemon/src/main.rs | 50 ++++ crates/daemon/src/rpc_server.rs | 286 +++++++++++++++++++-- crates/kernel/src/tasks/sessions.rs | 2 + crates/rpc-common/src/lib.rs | 29 ++- crates/telnet-host/Cargo.toml | 3 +- crates/telnet-host/src/telnet.rs | 45 ++-- crates/web-host/Cargo.toml | 3 +- crates/web-host/src/main.rs | 6 +- crates/web-host/src/ws_host.rs | 121 ++++++++- docker-compose.yml | 2 +- 16 files changed, 790 insertions(+), 223 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b12f5e6..251bdad7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array", +] + [[package]] name = "ahash" version = "0.7.6" @@ -78,15 +87,6 @@ dependencies = [ "libc", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anstream" version = "0.6.4" @@ -212,17 +212,6 @@ dependencies = [ "windows-sys 0.42.0", ] -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -338,29 +327,6 @@ dependencies = [ "virtue", ] -[[package]] -name = "bindgen" -version = "0.59.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" -dependencies = [ - "bitflags 1.3.2", - "cexpr", - "clang-sys", - "clap 2.34.0", - "env_logger", - "lazy_static", - "lazycell", - "log", - "peeking_take_while", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "which", -] - [[package]] name = "bindgen" version = "0.65.1" @@ -409,6 +375,17 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703642b98a00b3b90513279a8ede3fcfa479c126c5fb46e78f3051522f021403" +[[package]] +name = "blake2" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4e37d16930f5459780f5621038b6382b9bb37c19016f39fb6b5808d831f174" +dependencies = [ + "crypto-mac 0.8.0", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -434,7 +411,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32fa6a061124e37baba002e496d203e23ba3d7b73750be82dbfbc92913048a5b" dependencies = [ "byteorder", - "cipher", + "cipher 0.2.5", "opaque-debug", ] @@ -523,6 +500,31 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chacha20" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f08493fa7707effc63254c66c6ea908675912493cd67952eda23c09fae2610b1" +dependencies = [ + "cfg-if", + "cipher 0.3.0", + "cpufeatures", + "zeroize", +] + +[[package]] +name = "chacha20poly1305" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6547abe025f4027edacd9edaa357aded014eecec42a5070d9b885c3c334aba2" +dependencies = [ + "aead", + "chacha20", + "cipher 0.3.0", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.31" @@ -533,6 +535,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets", ] @@ -568,6 +571,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + [[package]] name = "clang-sys" version = "1.6.1" @@ -579,21 +591,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "clap" -version = "2.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" -dependencies = [ - "ansi_term", - "atty", - "bitflags 1.3.2", - "strsim 0.8.0", - "textwrap", - "unicode-width", - "vec_map", -] - [[package]] name = "clap" version = "4.4.6" @@ -612,7 +609,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.10.0", + "strsim", ] [[package]] @@ -751,6 +748,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "crypto-mac" version = "0.10.1" @@ -844,19 +851,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "env_logger" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -894,6 +888,28 @@ dependencies = [ "str-buf", ] +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure 0.12.6", +] + [[package]] name = "fastrand" version = "2.0.0" @@ -927,6 +943,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.0" @@ -1113,15 +1144,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - [[package]] name = "hermit-abi" version = "0.3.2" @@ -1134,7 +1156,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" dependencies = [ - "crypto-mac", + "crypto-mac 0.10.1", "digest 0.9.0", ] @@ -1187,12 +1209,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" version = "0.14.27" @@ -1372,7 +1388,7 @@ version = "0.11.0+8.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3386f101bcb4bd252d8e9d2fb41ec3b0862a15a62b478c355b2982efa469e3e" dependencies = [ - "bindgen 0.65.1", + "bindgen", "bzip2-sys", "cc", "glob", @@ -1596,13 +1612,14 @@ dependencies = [ "anyhow", "async-trait", "bincode", - "clap 4.4.6", + "clap", "clap_derive", "futures", "futures-util", "itertools", "moor-kernel", "moor-values", + "paseto", "rpc-common", "rustyline", "strum", @@ -1623,7 +1640,7 @@ dependencies = [ "async-trait", "bincode", "chrono", - "clap 4.4.6", + "clap", "clap_derive", "futures", "futures-util", @@ -1635,6 +1652,8 @@ dependencies = [ "moor-db", "moor-kernel", "moor-values", + "paseto", + "ring", "rpc-common", "serde", "serde_json", @@ -1725,7 +1744,7 @@ dependencies = [ "anyhow", "async-trait", "bincode", - "clap 4.4.6", + "clap", "clap_derive", "futures", "futures-util", @@ -1736,6 +1755,7 @@ dependencies = [ "metrics-util", "moor-kernel", "moor-values", + "paseto", "rpc-common", "strum", "tmq", @@ -1776,7 +1796,7 @@ dependencies = [ "async-trait", "axum", "bincode", - "clap 4.4.6", + "clap", "clap_derive", "futures", "futures-util", @@ -1787,6 +1807,7 @@ dependencies = [ "metrics-util", "moor-kernel", "moor-values", + "paseto", "rpc-common", "serde", "serde_derive", @@ -1856,7 +1877,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.2", + "hermit-abi", "libc", ] @@ -1893,7 +1914,6 @@ version = "69.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" dependencies = [ - "bindgen 0.59.2", "cc", "pkg-config", ] @@ -1904,6 +1924,44 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl" +version = "0.10.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" +dependencies = [ + "bitflags 2.4.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "openssl-sys" +version = "0.9.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "overload" version = "0.1.1" @@ -1942,6 +2000,23 @@ dependencies = [ "regex", ] +[[package]] +name = "paseto" +version = "2.0.2+1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04a17c4dbcb8f4a6b17b390aa66ac8e335349f829488950e95617f4b28d72b1f" +dependencies = [ + "base64 0.13.1", + "blake2", + "chacha20poly1305", + "chrono", + "failure", + "failure_derive", + "openssl", + "ring", + "serde_json", +] + [[package]] name = "paste" version = "1.0.14" @@ -2081,6 +2156,17 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "poly1305" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.4.3" @@ -2319,6 +2405,21 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +[[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", + "untrusted", + "web-sys", + "winapi", +] + [[package]] name = "rocksdb" version = "0.21.0" @@ -2617,6 +2718,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -2629,12 +2736,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - [[package]] name = "strsim" version = "0.10.0" @@ -2697,6 +2798,18 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + [[package]] name = "synstructure" version = "0.13.0" @@ -2741,15 +2854,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "termcolor" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" -dependencies = [ - "winapi-util", -] - [[package]] name = "test-case" version = "3.2.1" @@ -2791,15 +2895,6 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f0c8eb2ad70c12a6a69508f499b3051c924f4b1cfeae85bfad96e6bc5bba46" -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - [[package]] name = "thiserror" version = "1.0.49" @@ -3206,6 +3301,22 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "2.4.0" @@ -3250,12 +3361,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "version-compare" version = "0.1.1" @@ -3363,17 +3468,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "which" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" -dependencies = [ - "either", - "libc", - "once_cell", -] - [[package]] name = "winapi" version = "0.3.9" @@ -3572,7 +3666,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.29", - "synstructure", + "synstructure 0.13.0", ] [[package]] @@ -3581,6 +3675,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df54d76c3251de27615dfcce21e636c172dafb2549cd7fd93e21c66f6ca6bea2" +[[package]] +name = "zeroize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" + [[package]] name = "zeromq-src" version = "0.2.6+4.3.4" diff --git a/Cargo.toml b/Cargo.toml index 736cb4bb..135b2d27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,7 +74,7 @@ yoke-derive = "0.7.2" pwhash = "1.0.0" # For MOO's hokey "crypt" function, which is unix's crypt(3) basically md5 = "0.7.0" # For MOO's "string_hash" rand = "0.8.5" -onig = "6.4.0" +onig = { version = "6.4.0", default-features = false } chrono-tz = "0.8.3" iana-time-zone = "0.1.57" @@ -103,3 +103,7 @@ tempfile = "3.8.0" pretty_assertions = "1.4.0" test-case = "3.2.1" unindent = "0.2.3" + +# Auth/Auth +paseto = "2.0.2" +ring = { version = "*", features = ["std"] } \ No newline at end of file diff --git a/crates/README.md b/crates/README.md index 78b2d0fc..b40460e9 100644 --- a/crates/README.md +++ b/crates/README.md @@ -15,8 +15,7 @@ Binaries: Libraries: * `values` - crate that implements the core MOO discriminated union (`Var`) value type, plus all associated types and traits. - * `db` - implementation of the `WorldState` object database trait w/ a (for now) RocksDB backend, along with mock/testing - database implementations. + * `db` - implementation of the `WorldState` object database trait w/ a custom MVCC in-memory database. * `compiler` - the MOO language grammar, parser, AST, and codegen, as well as the decompiler & unparser * `kernel` - the kernel of the MOO driver: virtual machine, task scheduler, implementations of all builtin functions diff --git a/crates/console-host/Cargo.toml b/crates/console-host/Cargo.toml index ab041f63..c812ccbe 100644 --- a/crates/console-host/Cargo.toml +++ b/crates/console-host/Cargo.toml @@ -31,10 +31,11 @@ tracing.workspace = true tracing-subscriber.workspace = true tracing-chrome.workspace = true -## ZMQ +## ZMQ / RPC tmq.workspace = true uuid.workspace = true itertools.workspace = true +paseto.workspace = true ## For console rustyline.workspace = true diff --git a/crates/console-host/src/main.rs b/crates/console-host/src/main.rs index 19fbecc0..7d1c0870 100644 --- a/crates/console-host/src/main.rs +++ b/crates/console-host/src/main.rs @@ -19,7 +19,8 @@ use uuid::Uuid; use rpc_common::pubsub_client::{broadcast_recv, narrative_recv}; use rpc_common::rpc_client::RpcSendClient; use rpc_common::{ - BroadcastEvent, ConnectionEvent, RpcRequest, RpcResponse, RpcResult, BROADCAST_TOPIC, + AuthToken, BroadcastEvent, ClientToken, ConnectionEvent, RpcRequest, RpcResponse, RpcResult, + BROADCAST_TOPIC, }; #[derive(Parser, Debug)] @@ -60,7 +61,7 @@ struct Args { async fn establish_connection( client_id: Uuid, rpc_client: &mut RpcSendClient, -) -> Result { +) -> Result<(ClientToken, Objid), anyhow::Error> { match rpc_client .make_rpc_call( client_id, @@ -68,7 +69,7 @@ async fn establish_connection( ) .await { - Ok(RpcResult::Success(RpcResponse::NewConnection(conn_id))) => Ok(conn_id), + Ok(RpcResult::Success(RpcResponse::NewConnection(token, conn_id))) => Ok((token, conn_id)), Ok(RpcResult::Success(other)) => { error!("Unexpected response: {:?}", other); Err(Error::msg("Unexpected response")) @@ -85,26 +86,34 @@ async fn establish_connection( } async fn perform_auth( + token: ClientToken, client_id: Uuid, rpc_client: &mut RpcSendClient, username: &str, password: &str, -) -> Result { +) -> Result<(AuthToken, Objid), Error> { // Need to first authenticate with the server. match rpc_client .make_rpc_call( client_id, - RpcRequest::LoginCommand(vec![ - "connect".to_string(), - username.to_string(), - password.to_string(), - ]), + RpcRequest::LoginCommand( + token, + vec![ + "connect".to_string(), + username.to_string(), + password.to_string(), + ], + ), ) .await { - Ok(RpcResult::Success(RpcResponse::LoginResult(Some((connect_type, player))))) => { + Ok(RpcResult::Success(RpcResponse::LoginResult(Some(( + auth_token, + connect_type, + player, + ))))) => { info!("Authenticated as {:?} with id {:?}", connect_type, player); - Ok(player) + Ok((auth_token, player)) } Ok(RpcResult::Success(RpcResponse::LoginResult(None))) => { error!("Authentication failed"); @@ -126,6 +135,8 @@ async fn perform_auth( } async fn handle_console_line( + client_token: ClientToken, + auth_token: AuthToken, client_id: Uuid, line: &str, rpc_client: &mut RpcSendClient, @@ -143,7 +154,12 @@ async fn handle_console_line( match rpc_client .make_rpc_call( client_id, - RpcRequest::RequestedInput(input_request_id.as_u128(), line.to_string()), + RpcRequest::RequestedInput( + client_token.clone(), + auth_token.clone(), + input_request_id.as_u128(), + line.to_string(), + ), ) .await { @@ -164,7 +180,10 @@ async fn handle_console_line( } match rpc_client - .make_rpc_call(client_id, RpcRequest::Command(line.to_string())) + .make_rpc_call( + client_id, + RpcRequest::Command(client_token.clone(), auth_token.clone(), line.to_string()), + ) .await { Ok(RpcResult::Success(RpcResponse::CommandSubmitted(_))) => { @@ -201,11 +220,18 @@ async fn console_loop( let mut rpc_client = RpcSendClient::new(rcp_request_sock); - let conn_obj_id = establish_connection(client_id, &mut rpc_client).await?; + let (client_token, conn_obj_id) = establish_connection(client_id, &mut rpc_client).await?; debug!("Transitional connection ID before auth: {:?}", conn_obj_id); // Now authenticate with the server. - let player = perform_auth(client_id, &mut rpc_client, username, password).await?; + let (auth_token, player) = perform_auth( + client_token.clone(), + client_id, + &mut rpc_client, + username, + password, + ) + .await?; info!("Authenticated as {:?} / {}", username, player); @@ -256,12 +282,17 @@ async fn console_loop( .connect(rpc_server) .expect("Unable to bind RPC server for connection"); let mut broadcast_rpc_client = RpcSendClient::new(broadcast_rcp_request_sock); + + let broadcast_client_token = client_token.clone(); let broadcast_loop = tokio::spawn(async move { loop { match broadcast_recv(&mut broadcast_subscriber).await { Ok(BroadcastEvent::PingPong(_)) => { if let Err(e) = broadcast_rpc_client - .make_rpc_call(client_id, RpcRequest::Pong(SystemTime::now())) + .make_rpc_call( + client_id, + RpcRequest::Pong(broadcast_client_token.clone(), SystemTime::now()), + ) .await { error!("Error sending pong: {:?}", e); @@ -276,6 +307,8 @@ async fn console_loop( } }); + let edit_client_token = client_token.clone(); + let edit_auth_token = auth_token.clone(); let edit_loop = tokio::spawn(async move { let mut rl = DefaultEditor::new().unwrap(); loop { @@ -293,7 +326,15 @@ async fn console_loop( Ok(line) => { rl.add_history_entry(line.clone()) .expect("Could not add history"); - handle_console_line(client_id, &line, &mut rpc_client, input_request_id).await; + handle_console_line( + edit_client_token.clone(), + edit_auth_token.clone(), + client_id, + &line, + &mut rpc_client, + input_request_id, + ) + .await; } Err(ReadlineError::Eof) => { println!(""); diff --git a/crates/daemon/Cargo.toml b/crates/daemon/Cargo.toml index b8c49b62..d720b7c3 100644 --- a/crates/daemon/Cargo.toml +++ b/crates/daemon/Cargo.toml @@ -45,3 +45,6 @@ serde_json.workspace = true serde.workspace = true chrono.workspace = true +# Auth/Auth +paseto.workspace = true +ring.workspace = true \ No newline at end of file diff --git a/crates/daemon/src/main.rs b/crates/daemon/src/main.rs index 0e339bd4..7047d191 100644 --- a/crates/daemon/src/main.rs +++ b/crates/daemon/src/main.rs @@ -9,6 +9,7 @@ use tmq::Multipart; use tokio::select; use tokio::signal::unix::{signal, SignalKind}; use tracing::info; +use {ring::rand::SystemRandom, ring::signature::Ed25519KeyPair}; use moor_db::{DatabaseBuilder, DatabaseType}; use moor_kernel::tasks::scheduler::Scheduler; @@ -75,6 +76,22 @@ struct Args { default_value = "tcp://0.0.0.0:7898" )] narrative_listen: String, + + #[arg( + long, + value_name = "keypair", + help = "file containing a pkcs8 ed25519, used for authenticating client connections", + default_value = "keypair.pkcs8" + )] + keypair: PathBuf, + + #[arg( + long, + value_name = "generate-keypair", + help = "Generate a new keypair and save it to the keypair files, if they don't exist already", + default_value = "false" + )] + generate_keypair: bool, } pub(crate) fn make_response(result: Result) -> Multipart { @@ -111,6 +128,38 @@ async fn main() { .install() .expect("failed to install Prometheus recorder"); + // Check the public/private keypair file to see if it exists. If it does, parse it and establish + // the PASETO keypair from it... + let keypair = if args.keypair.exists() { + let keypair_bytes = std::fs::read(args.keypair).expect("Unable to read keypair file"); + let keypair = Ed25519KeyPair::from_pkcs8(keypair_bytes.as_ref()) + .expect("Unable to parse keypair file"); + keypair + } else { + // Otherwise, check to see if --generate-keypair was passed. If it was, generate a new + // keypair and save it to the file; otherwise, error out. + + if args.generate_keypair { + let sys_rand = SystemRandom::new(); + let key_pkcs8 = + Ed25519KeyPair::generate_pkcs8(&sys_rand).expect("Failed to generate pkcs8 key!"); + let keypair = + Ed25519KeyPair::from_pkcs8(key_pkcs8.as_ref()).expect("Failed to parse keypair"); + let pkcs8_keypair_bytes: &[u8] = key_pkcs8.as_ref(); + + // Now write it out... + std::fs::write(args.keypair, pkcs8_keypair_bytes) + .expect("Unable to write keypair file"); + + keypair + // Write + } else { + panic!( + "Public/private keypair files do not exist, and --generate-keypair was not passed" + ); + } + }; + info!("Daemon starting..."); let db_source_builder = DatabaseBuilder::new() .with_db_type(args.db_type) @@ -157,6 +206,7 @@ async fn main() { let scheduler_loop = tokio::spawn(async move { loop_scheduler.run().await }); let zmq_server_loop = zmq_loop( + keypair, args.connections_file, state_source, scheduler.clone(), diff --git a/crates/daemon/src/rpc_server.rs b/crates/daemon/src/rpc_server.rs index 516a0baf..276df1ca 100644 --- a/crates/daemon/src/rpc_server.rs +++ b/crates/daemon/src/rpc_server.rs @@ -6,6 +6,8 @@ use std::time::{Instant, SystemTime}; use anyhow::{Context, Error}; use futures_util::SinkExt; use metrics_macros::increment_counter; +use ring::signature::Ed25519KeyPair; +use serde_json::json; use tmq::publish::Publish; use tmq::{publish, reply, Multipart}; use tokio::sync::Mutex; @@ -26,16 +28,20 @@ use moor_values::var::{v_bool, v_objid, v_str, v_string}; use moor_values::SYSTEM_OBJECT; use rpc_common::RpcResponse::{LoginResult, NewConnection}; use rpc_common::{ - BroadcastEvent, ConnectType, ConnectionEvent, RpcRequest, RpcRequestError, RpcResponse, - BROADCAST_TOPIC, + AuthToken, BroadcastEvent, ClientToken, ConnectType, ConnectionEvent, RpcRequest, + RpcRequestError, RpcResponse, BROADCAST_TOPIC, }; +pub const MOOR_SESSION_TOKEN_FOOTER: &str = "key-id:moor_rpc"; +pub const MOOR_AUTH_TOKEN_FOOTER: &str = "key-id:moor_player"; + use crate::connections::ConnectionsDB; use crate::connections_tb::ConnectionsTb; use crate::make_response; use crate::rpc_session::RpcSession; pub struct RpcServer { + keypair: Ed25519KeyPair, publish: Arc>, world_state_source: Arc, scheduler: Scheduler, @@ -53,7 +59,8 @@ struct ConnectionRecord { impl RpcServer { pub async fn new( - connections_file: PathBuf, + keypair: Ed25519KeyPair, + connections_db_path: PathBuf, zmq_context: tmq::Context, narrative_endpoint: &str, wss: Arc, @@ -67,12 +74,13 @@ impl RpcServer { .set_sndtimeo(1) .bind(narrative_endpoint) .unwrap(); - let connections = Arc::new(ConnectionsTb::new(Some(connections_file)).await); + let connections = Arc::new(ConnectionsTb::new(Some(connections_db_path)).await); info!( "Created connections list, with {} initial known connections", connections.connections().await.len() ); Self { + keypair, world_state_source: wss, scheduler, connections, @@ -92,11 +100,14 @@ impl RpcServer { increment_counter!("rpc_server.connection_establish"); match self.connections.new_connection(client_id, hostname).await { - Ok(oid) => make_response(Ok(NewConnection(oid))), + Ok(oid) => { + let token = self.make_client_token(client_id); + make_response(Ok(NewConnection(token, oid))) + } Err(e) => make_response(Err(e)), } } - RpcRequest::Pong(_client_sys_time) => { + RpcRequest::Pong(token, _client_sys_time) => { // Always respond with a ThanksPong, even if it's somebody we don't know. // Can easily be a connection that was in the middle of negotiation at the time the // ping was sent out, or dangling in some other way. @@ -110,6 +121,15 @@ impl RpcServer { warn!("Received Pong from invalid client: {}", client_id); return response; }; + let Ok(_) = self.validate_client_token(token, client_id) else { + warn!( + ?client_id, + ?connection, + "Client token validation failed for request" + ); + return make_response(Err(RpcRequestError::PermissionDenied)); + }; + // Let 'connections' know that the connection is still alive. let Ok(_) = self .connections @@ -121,16 +141,27 @@ impl RpcServer { }; response } - RpcRequest::RequestSysProp(object, property) => { + RpcRequest::RequestSysProp(token, object, property) => { increment_counter!("rpc_server.request_sys_prop"); - if !self.connections.is_valid_client(client_id).await { - warn!("Received RequestSysProp from invalid client: {}", client_id); - + let Some(connection) = self + .connections + .connection_object_for_client(client_id) + .await + else { return make_response(Err(RpcRequestError::NoConnection)); - } + }; + let Ok(_) = self.validate_client_token(token, client_id) else { + warn!( + ?client_id, + ?connection, + "Client token validation failed for request" + ); + return make_response(Err(RpcRequestError::PermissionDenied)); + }; + make_response(self.clone().request_sys_prop(object, property).await) } - RpcRequest::LoginCommand(args) => { + RpcRequest::LoginCommand(token, args) => { increment_counter!("rpc_server.login_command"); let Some(connection) = self .connections @@ -139,6 +170,14 @@ impl RpcServer { else { return make_response(Err(RpcRequestError::NoConnection)); }; + let Ok(_) = self.validate_client_token(token, client_id) else { + warn!( + ?client_id, + ?connection, + "Client token validation failed for request" + ); + return make_response(Err(RpcRequestError::PermissionDenied)); + }; make_response( self.clone() @@ -146,7 +185,7 @@ impl RpcServer { .await, ) } - RpcRequest::Command(command) => { + RpcRequest::Command(token, auth_token, command) => { increment_counter!("rpc_server.command"); let Some(connection) = self .connections @@ -156,13 +195,30 @@ impl RpcServer { return make_response(Err(RpcRequestError::NoConnection)); }; + let Ok(_) = self.validate_client_token(token, client_id) else { + warn!( + ?client_id, + ?connection, + "Client token validation failed for request" + ); + return make_response(Err(RpcRequestError::PermissionDenied)); + }; + + let Ok(_) = self.validate_auth_token(auth_token, connection) else { + warn!( + ?client_id, + ?connection, + "Auth token validation failed for request" + ); + return make_response(Err(RpcRequestError::PermissionDenied)); + }; make_response( self.clone() .perform_command(client_id, connection, command) .await, ) } - RpcRequest::RequestedInput(request_id, input) => { + RpcRequest::RequestedInput(token, auth_token, request_id, input) => { increment_counter!("rpc_server.requested_input"); let Some(connection) = self .connections @@ -172,6 +228,23 @@ impl RpcServer { return make_response(Err(RpcRequestError::NoConnection)); }; + let Ok(_) = self.validate_client_token(token, client_id) else { + warn!( + ?client_id, + ?connection, + "Client token validation failed for request" + ); + return make_response(Err(RpcRequestError::PermissionDenied)); + }; + + let Ok(_) = self.validate_auth_token(auth_token, connection) else { + warn!( + ?client_id, + ?connection, + "Auth token validation failed for request" + ); + return make_response(Err(RpcRequestError::PermissionDenied)); + }; let request_id = Uuid::from_u128(request_id); make_response( self.clone() @@ -179,7 +252,7 @@ impl RpcServer { .await, ) } - RpcRequest::OutOfBand(command) => { + RpcRequest::OutOfBand(token, auth_token, command) => { increment_counter!("rpc_server.out_of_band_received"); let Some(connection) = self .connections @@ -188,6 +261,23 @@ impl RpcServer { else { return make_response(Err(RpcRequestError::NoConnection)); }; + let Ok(_) = self.validate_client_token(token, client_id) else { + warn!( + ?client_id, + ?connection, + "Client token validation failed for request" + ); + return make_response(Err(RpcRequestError::PermissionDenied)); + }; + + let Ok(_) = self.validate_auth_token(auth_token, connection) else { + warn!( + ?client_id, + ?connection, + "Auth token validation failed for request" + ); + return make_response(Err(RpcRequestError::PermissionDenied)); + }; make_response( self.clone() @@ -196,7 +286,7 @@ impl RpcServer { ) } - RpcRequest::Eval(evalstr) => { + RpcRequest::Eval(token, auth_token, evalstr) => { increment_counter!("rpc_server.eval"); let Some(connection) = self .connections @@ -206,10 +296,33 @@ impl RpcServer { return make_response(Err(RpcRequestError::NoConnection)); }; + let Ok(_) = self.validate_client_token(token, client_id) else { + warn!( + ?client_id, + ?connection, + "Client token validation failed for request" + ); + return make_response(Err(RpcRequestError::PermissionDenied)); + }; + + let Ok(_) = self.validate_auth_token(auth_token, connection) else { + warn!( + ?client_id, + ?connection, + "Auth token validation failed for request" + ); + return make_response(Err(RpcRequestError::PermissionDenied)); + }; make_response(self.clone().eval(client_id, connection, evalstr).await) } - RpcRequest::Detach => { + RpcRequest::Detach(token) => { increment_counter!("rpc_server.detach"); + + let Ok(connection) = self.validate_client_token(token, client_id) else { + warn!(?client_id, "Client token validation failed for request"); + return make_response(Err(RpcRequestError::PermissionDenied)); + }; + info!("Detaching client: {}", client_id); // Detach this client id from the player/connection object. @@ -434,7 +547,10 @@ impl RpcServer { } increment_counter!("rpc_server.perform_login.success"); - Ok(LoginResult(Some((connect_type, player)))) + + let auth_token = self.make_auth_token(player); + + Ok(LoginResult(Some((auth_token, connect_type, player)))) } async fn submit_connected_task( @@ -788,10 +904,139 @@ impl RpcServer { self.connections.ping_check().await; Ok(()) } + + /// Construct a PASETO token for this client_id and player combination. This token is used to + /// validate the client connection to the daemon for future requests. + fn make_client_token(&self, client_id: Uuid) -> ClientToken { + let token = paseto::tokens::PasetoBuilder::new() + .set_ed25519_key(&self.keypair) + .set_issued_at(None) + .set_claim("client_id", json!(client_id.to_string())) + .set_issuer("moor") + .set_audience("moor_connection") + .set_footer(MOOR_SESSION_TOKEN_FOOTER) + .build() + .expect("Unable to build Paseto token"); + ClientToken(token) + } + + /// Construct a PASETO token for this player login. This token is used to provide credentials + /// for requests, to allow reconnection with a different client_id. + fn make_auth_token(&self, oid: Objid) -> AuthToken { + let token = paseto::tokens::PasetoBuilder::new() + .set_ed25519_key(&self.keypair) + .set_issued_at(None) + .set_claim("player", json!(oid.0.to_string())) + .set_issuer("moor") + .set_audience("moor_credentials") + .set_footer(MOOR_AUTH_TOKEN_FOOTER) + .build() + .expect("Unable to build Paseto token"); + AuthToken(token) + } + + /// Validate the provided PASETO token against the provided client id and (optionally) player + /// objid. If they do not match, the request is rejected, permissions denied. + fn validate_client_token( + &self, + token: ClientToken, + client_id: Uuid, + ) -> Result<(), SessionError> { + let pk = paseto::tokens::PasetoPublicKey::ED25519KeyPair(&self.keypair); + let verified_token = paseto::tokens::validate_public_token( + &token.0, + Some(MOOR_SESSION_TOKEN_FOOTER), + &pk, + &paseto::tokens::TimeBackend::Chrono, + ) + .map_err(|e| { + warn!(error = ?e, "Unable to parse/validate token"); + SessionError::InvalidToken + })?; + + // Issuer & audience must match. + let (Some(Some("moor")), Some(Some("moor_connection"))) = ( + verified_token.get("iss").map(|s| s.as_str()), + verified_token.get("aud").map(|s| s.as_str()), + ) else { + debug!("Token does not contain valid issuer/audience"); + return Err(SessionError::InvalidToken); + }; + + // Does the token match the client it came from? If not, reject it. + let Some(token_client_id) = verified_token.get("client_id") else { + debug!("Token does not contain client_id"); + return Err(SessionError::InvalidToken); + }; + let Some(token_client_id) = token_client_id.as_str() else { + debug!("Token client_id is null"); + return Err(SessionError::InvalidToken); + }; + let Ok(token_client_id) = Uuid::parse_str(token_client_id) else { + debug!("Token client_id is not a valid UUID"); + return Err(SessionError::InvalidToken); + }; + if client_id != token_client_id { + debug!( + ?client_id, + ?token_client_id, + "Token client_id does not match client_id" + ); + return Err(SessionError::InvalidToken); + } + + Ok(()) + } + + /// Validate that the provided PASETO token is valid for the provided player Objid. + /// Note that this is merely validating that the token is valid, not that the actual player + /// inside the token is valid and has the capapilities it thinks it has. That must be done in + /// the server. + fn validate_auth_token(&self, token: AuthToken, objid: Objid) -> Result<(), SessionError> { + let pk = paseto::tokens::PasetoPublicKey::ED25519KeyPair(&self.keypair); + let verified_token = paseto::tokens::validate_public_token( + &token.0, + Some(MOOR_AUTH_TOKEN_FOOTER), + &pk, + &paseto::tokens::TimeBackend::Chrono, + ) + .map_err(|e| { + warn!(error = ?e, "Unable to parse/validate token"); + SessionError::InvalidToken + })?; + + // Does the 'player' match objid? If not, reject it. + let Some(token_player) = verified_token.get("player") else { + debug!("Token does not contain player"); + return Err(SessionError::InvalidToken); + }; + let Some(token_player) = token_player.as_str() else { + debug!("Token player is not a string"); + return Err(SessionError::InvalidToken); + }; + let Ok(token_player) = token_player.parse() else { + debug!("Token player is not a valid Objid"); + return Err(SessionError::InvalidToken); + }; + let token_player = Objid(token_player); + if objid != token_player { + debug!(?objid, ?token_player, "Token player does not match objid"); + return Err(SessionError::InvalidToken); + } + + // TODO: we will need to verify that the player object id inside the token is valid inside + // moor itself. And really only something with a WorldState can do that. So it's not + // enough to have validated the auth token here, we will need to pepper the scheduler/task + // code with checks to make sure that the player objid is valid before letting it go + // forwards. + + Ok(()) + } } pub(crate) async fn zmq_loop( - connections_file: PathBuf, + keypair: Ed25519KeyPair, + connections_db_path: PathBuf, wss: Arc, scheduler: Scheduler, rpc_endpoint: &str, @@ -804,7 +1049,8 @@ pub(crate) async fn zmq_loop( let rpc_server = Arc::new( RpcServer::new( - connections_file, + keypair, + connections_db_path, zmq_ctx.clone(), narrative_endpoint, wss, diff --git a/crates/kernel/src/tasks/sessions.rs b/crates/kernel/src/tasks/sessions.rs index 340d28d4..76b25ae1 100644 --- a/crates/kernel/src/tasks/sessions.rs +++ b/crates/kernel/src/tasks/sessions.rs @@ -99,6 +99,8 @@ pub enum SessionError { DeliveryError, #[error("Could not commit session: {0}")] CommitError(String), + #[error("Invalid authorization token")] + InvalidToken, } /// A simple no-op implementation of the Sessions trait, for use in unit tests. diff --git a/crates/rpc-common/src/lib.rs b/crates/rpc-common/src/lib.rs index af7243bd..f950316f 100644 --- a/crates/rpc-common/src/lib.rs +++ b/crates/rpc-common/src/lib.rs @@ -21,17 +21,26 @@ pub enum RpcError { CouldNotDecode(String), } +/// PASETO public token for a connection, used for the validation of RPC requests after the initial +/// connection is established. +#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode)] +pub struct ClientToken(pub String); + +/// PASTEO public token for an authenticated player, encoding the player's identity. +#[derive(Debug, Clone, Eq, PartialEq, Encode, Decode)] +pub struct AuthToken(pub String); + #[derive(Debug, Clone, Eq, PartialEq, Encode, Decode)] pub enum RpcRequest { ConnectionEstablish(String), - RequestSysProp(String, String), - LoginCommand(Vec), - Command(String), - RequestedInput(u128, String), - OutOfBand(String), - Eval(String), - Pong(SystemTime), - Detach, + RequestSysProp(ClientToken, String, String), + LoginCommand(ClientToken, Vec), + Command(ClientToken, AuthToken, String), + RequestedInput(ClientToken, AuthToken, u128, String), + OutOfBand(ClientToken, AuthToken, String), + Eval(ClientToken, AuthToken, String), + Pong(ClientToken, SystemTime), + Detach(ClientToken), } #[derive(Debug, Copy, Clone, Eq, PartialEq, Encode, Decode)] @@ -50,9 +59,9 @@ pub enum RpcResult { #[derive(Debug, Clone, Eq, PartialEq, Encode, Decode)] pub enum RpcResponse { - NewConnection(Objid), + NewConnection(ClientToken, Objid), SysPropValue(Option), - LoginResult(Option<(ConnectType, Objid)>), + LoginResult(Option<(AuthToken, ConnectType, Objid)>), CommandSubmitted(usize /* task id */), InputThanks, EvalResult(Var), diff --git a/crates/telnet-host/Cargo.toml b/crates/telnet-host/Cargo.toml index e39910a0..1ce59d6b 100644 --- a/crates/telnet-host/Cargo.toml +++ b/crates/telnet-host/Cargo.toml @@ -36,8 +36,9 @@ metrics.workspace = true metrics-util.workspace = true metrics-macros.workspace = true -## ZMQ +## ZMQ / RPC tmq.workspace = true uuid.workspace = true itertools.workspace = true +paseto.workspace = true diff --git a/crates/telnet-host/src/telnet.rs b/crates/telnet-host/src/telnet.rs index 52c5b025..f64ba07c 100644 --- a/crates/telnet-host/src/telnet.rs +++ b/crates/telnet-host/src/telnet.rs @@ -21,7 +21,8 @@ use rpc_common::pubsub_client::{broadcast_recv, narrative_recv}; use rpc_common::rpc_client::RpcSendClient; use rpc_common::RpcRequest::ConnectionEstablish; use rpc_common::{ - BroadcastEvent, ConnectType, ConnectionEvent, RpcRequestError, RpcResult, BROADCAST_TOPIC, + AuthToken, BroadcastEvent, ClientToken, ConnectType, ConnectionEvent, RpcRequestError, + RpcResult, BROADCAST_TOPIC, }; use rpc_common::{RpcRequest, RpcResponse}; @@ -30,6 +31,8 @@ const OUT_OF_BAND_PREFIX: &str = "#$#"; pub(crate) struct TelnetConnection { client_id: Uuid, + /// Current PASETO token. + client_token: ClientToken, write: SplitSink, String>, read: SplitStream>, } @@ -44,11 +47,14 @@ impl TelnetConnection { // Provoke welcome message, which is a login command with no arguments, and we // don't care about the reply at this point. rpc_client - .make_rpc_call(self.client_id, RpcRequest::LoginCommand(vec![])) + .make_rpc_call( + self.client_id, + RpcRequest::LoginCommand(self.client_token.clone(), vec![]), + ) .await .expect("Unable to send login request to RPC server"); - let Ok((player, connect_type)) = self + let Ok((auth_token, player, connect_type)) = self .authorization_phase(narrative_sub, broadcast_sub, rpc_client) .await else { @@ -64,7 +70,7 @@ impl TelnetConnection { debug!(?player, client_id = ?self.client_id, "Entering command dispatch loop"); if self - .command_loop(narrative_sub, broadcast_sub, rpc_client) + .command_loop(auth_token.clone(), narrative_sub, broadcast_sub, rpc_client) .await .is_err() { @@ -73,7 +79,10 @@ impl TelnetConnection { // Let the server know this client is gone. rpc_client - .make_rpc_call(self.client_id, RpcRequest::Detach) + .make_rpc_call( + self.client_id, + RpcRequest::Detach(self.client_token.clone()), + ) .await?; Ok(()) @@ -84,7 +93,7 @@ impl TelnetConnection { narrative_sub: &mut Subscribe, broadcast_sub: &mut Subscribe, rpc_client: &mut RpcSendClient, - ) -> Result<(Objid, ConnectType), anyhow::Error> { + ) -> Result<(AuthToken, Objid, ConnectType), anyhow::Error> { debug!(client_id = ?self.client_id, "Entering auth loop"); loop { select! { @@ -93,7 +102,7 @@ impl TelnetConnection { match event { BroadcastEvent::PingPong(_server_time) => { let _ = rpc_client.make_rpc_call(self.client_id, - RpcRequest::Pong(SystemTime::now())).await?; + RpcRequest::Pong(self.client_token.clone(), SystemTime::now())).await?; } } } @@ -124,10 +133,10 @@ impl TelnetConnection { let line = line.unwrap(); let words = parse_into_words(&line); let response = rpc_client.make_rpc_call(self.client_id, - RpcRequest::LoginCommand(words)).await.expect("Unable to send login request to RPC server"); - if let RpcResult::Success(RpcResponse::LoginResult(Some((connect_type, player)))) = response { + RpcRequest::LoginCommand(self.client_token.clone(), words)).await.expect("Unable to send login request to RPC server"); + if let RpcResult::Success(RpcResponse::LoginResult(Some((auth_token, connect_type, player)))) = response { info!(?player, client_id = ?self.client_id, "Login successful"); - return Ok((player, connect_type)) + return Ok((auth_token, player, connect_type)) } } } @@ -136,6 +145,7 @@ impl TelnetConnection { async fn command_loop( &mut self, + auth_token: AuthToken, narrative_sub: &mut Subscribe, broadcast_sub: &mut Subscribe, rpc_client: &mut RpcSendClient, @@ -153,15 +163,15 @@ impl TelnetConnection { // Are we expecting to respond to prompt input? If so, send this through to that. let response = match expecting_input_reply.take() { Some(input_reply_id) => { - rpc_client.make_rpc_call(self.client_id, RpcRequest::RequestedInput(input_reply_id, line)).await? + rpc_client.make_rpc_call(self.client_id, RpcRequest::RequestedInput(self.client_token.clone(), auth_token.clone(), input_reply_id, line)).await? } None => { // If the line begins with the out of band prefix, then send it that way, // instead. And really just fire and forget. if line.starts_with(OUT_OF_BAND_PREFIX) { - rpc_client.make_rpc_call(self.client_id, RpcRequest::OutOfBand(line)).await? + rpc_client.make_rpc_call(self.client_id, RpcRequest::OutOfBand(self.client_token.clone(), auth_token.clone(), line)).await? } else { - rpc_client.make_rpc_call(self.client_id, RpcRequest::Command(line)).await? + rpc_client.make_rpc_call(self.client_id, RpcRequest::Command(self.client_token.clone(), auth_token.clone(), line)).await? } } }; @@ -198,7 +208,7 @@ impl TelnetConnection { match event { BroadcastEvent::PingPong(_server_time) => { let _ = rpc_client.make_rpc_call(self.client_id, - RpcRequest::Pong(SystemTime::now())).await?; + RpcRequest::Pong(self.client_token.clone(), SystemTime::now())).await?; } } } @@ -260,13 +270,13 @@ pub async fn telnet_listen_loop( debug!(rpc_address, "Contacting RPC server to establish connection"); let mut rpc_client = RpcSendClient::new(rcp_request_sock); - let connection_oid = match rpc_client + let (token, connection_oid) = match rpc_client .make_rpc_call(client_id, ConnectionEstablish(peer_addr.to_string())) .await { - Ok(RpcResult::Success(RpcResponse::NewConnection(objid))) => { + Ok(RpcResult::Success(RpcResponse::NewConnection(token, objid))) => { info!("Connection established, connection ID: {}", objid); - objid + (token, objid) } Ok(RpcResult::Failure(f)) => { bail!("RPC failure in connection establishment: {}", f); @@ -306,6 +316,7 @@ pub async fn telnet_listen_loop( let (write, read): (SplitSink, String>, _) = framed_stream.split(); let mut tcp_connection = TelnetConnection { + client_token: token, client_id, write, read, diff --git a/crates/web-host/Cargo.toml b/crates/web-host/Cargo.toml index e8c7874f..e15f7d15 100644 --- a/crates/web-host/Cargo.toml +++ b/crates/web-host/Cargo.toml @@ -35,10 +35,11 @@ metrics.workspace = true metrics-util.workspace = true metrics-macros.workspace = true -## ZMQ +## ZMQ / RPC tmq.workspace = true uuid.workspace = true itertools.workspace = true +paseto.workspace = true # HTTP/websockets layer axum.workspace = true diff --git a/crates/web-host/src/main.rs b/crates/web-host/src/main.rs index 4cab84d2..9d00e786 100644 --- a/crates/web-host/src/main.rs +++ b/crates/web-host/src/main.rs @@ -1,8 +1,8 @@ mod ws_host; -use crate::ws_host::WebSocketHost; +use crate::ws_host::{auth_handler, WebSocketHost}; use anyhow::Context; -use axum::routing::get; +use axum::routing::{get, post}; use axum::Router; use clap::Parser; use clap_derive::Parser; @@ -52,11 +52,11 @@ fn mk_routes(ws_host: WebSocketHost) -> anyhow::Result { let websocket_router = Router::new() .route("/connect", get(ws_host::ws_connect_handler)) .route("/create", get(ws_host::ws_create_handler)) + .route("/auth/:player", post(auth_handler)) .with_state(ws_host); Ok(Router::new() .nest("/ws", websocket_router) - // .nest("/properties", property_router) .route("/metrics", get(move || ready(recorder_handle.render())))) } diff --git a/crates/web-host/src/ws_host.rs b/crates/web-host/src/ws_host.rs index a2f3197a..18f1b15c 100644 --- a/crates/web-host/src/ws_host.rs +++ b/crates/web-host/src/ws_host.rs @@ -1,8 +1,10 @@ +use axum::body::Bytes; use axum::extract::ws::{Message, WebSocket}; -use axum::extract::{ConnectInfo, State, WebSocketUpgrade}; +use axum::extract::{ConnectInfo, Path, State, WebSocketUpgrade}; use axum::headers::authorization::Basic; -use axum::headers::Authorization; -use axum::response::IntoResponse; +use axum::headers::{Authorization, HeaderValue}; +use axum::http::{HeaderMap, StatusCode}; +use axum::response::{IntoResponse, Response}; use axum::TypedHeader; use futures_util::stream::SplitSink; use futures_util::{SinkExt, StreamExt}; @@ -89,13 +91,13 @@ impl WebSocketHost { ); let mut rpc_client = RpcSendClient::new(rcp_request_sock); - let connection_oid = match rpc_client + let (client_token, connection_oid) = match rpc_client .make_rpc_call(client_id, ConnectionEstablish(peer_addr.to_string())) .await { - Ok(RpcResult::Success(RpcResponse::NewConnection(objid))) => { + Ok(RpcResult::Success(RpcResponse::NewConnection(client_token, objid))) => { info!("Connection established, connection ID: {}", objid); - objid + (client_token, objid) } Ok(RpcResult::Failure(f)) => { error!("RPC failure in connection establishment: {}", f); @@ -146,10 +148,14 @@ impl WebSocketHost { auth.password().to_string(), ]; let response = rpc_client - .make_rpc_call(client_id, RpcRequest::LoginCommand(words)) + .make_rpc_call( + client_id, + RpcRequest::LoginCommand(client_token.clone(), words), + ) .await .expect("Unable to send login request to RPC server"); - let RpcResult::Success(RpcResponse::LoginResult(Some((connect_type, player)))) = response + let RpcResult::Success(RpcResponse::LoginResult(Some((auth_token, connect_type, player)))) = + response else { error!(?response, "Login failed"); @@ -180,11 +186,11 @@ impl WebSocketHost { let response = match expecting_input.take() { Some(input_request_id) => { - rpc_client.make_rpc_call(client_id, RpcRequest::RequestedInput(input_request_id, cmd)) + rpc_client.make_rpc_call(client_id, RpcRequest::RequestedInput(client_token.clone(), auth_token.clone(), input_request_id, cmd)) .await.expect("Unable to send input to RPC server") } None => { - rpc_client.make_rpc_call(client_id, RpcRequest::Command(cmd)) + rpc_client.make_rpc_call(client_id, RpcRequest::Command(client_token.clone(), auth_token.clone(), cmd)) .await.expect("Unable to send command to RPC server") } } ; @@ -221,7 +227,7 @@ impl WebSocketHost { match event { BroadcastEvent::PingPong(_server_time) => { let _ = rpc_client.make_rpc_call(client_id, - RpcRequest::Pong(SystemTime::now())).await.expect("Unable to send pong to RPC server"); + RpcRequest::Pong(client_token.clone(), SystemTime::now())).await.expect("Unable to send pong to RPC server"); } } } @@ -250,6 +256,99 @@ impl WebSocketHost { } } +/// Stand-alone HTTP GET authentication handler which connects and then gets a valid authentication token +/// which can then be used in the headers for subsequent websocket request. +pub async fn auth_handler( + ConnectInfo(addr): ConnectInfo, + State(ws_host): State, + Path(player): Path, + body: Bytes, +) -> impl IntoResponse { + increment_counter!("ws_host.auth"); + + let zmq_ctx = ws_host.zmq_context.clone(); + let rcp_request_sock = request(&zmq_ctx) + .set_rcvtimeo(100) + .set_sndtimeo(100) + .connect(ws_host.rpc_addr.as_str()) + .expect("Unable to bind RPC server for connection"); + + let client_id = Uuid::new_v4(); + let mut rpc_client = RpcSendClient::new(rcp_request_sock); + + let client_token = match rpc_client + .make_rpc_call(client_id, ConnectionEstablish(addr.to_string())) + .await + { + Ok(RpcResult::Success(RpcResponse::NewConnection(client_token, objid))) => { + info!("Connection established, connection ID: {}", objid); + client_token + } + Ok(RpcResult::Failure(f)) => { + error!("RPC failure in connection establishment: {}", f); + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body("".to_string()) + .unwrap(); + } + Ok(_) => { + error!("Unexpected response from RPC server"); + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body("".to_string()) + .unwrap(); + } + Err(e) => { + error!("Unable to establish connection: {}", e); + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body("".to_string()) + .unwrap(); + } + }; + + // Read the password string out of 'body'. + let password = String::from_utf8(body.to_vec()).unwrap(); + + let words = vec!["connect".to_string(), player, password]; + let response = rpc_client + .make_rpc_call( + client_id, + RpcRequest::LoginCommand(client_token.clone(), words), + ) + .await + .expect("Unable to send login request to RPC server"); + let RpcResult::Success(RpcResponse::LoginResult(Some((auth_token, connect_type, player)))) = + response + else { + error!(?response, "Login failed"); + + return Response::builder() + .status(StatusCode::UNAUTHORIZED) + .body("".to_string()) + .unwrap(); + }; + + // We now have a valid auth token for the player, so we return it in the response headers, + // along with an empty body and an OK. + let mut headers = HeaderMap::new(); + headers.insert( + "X-Moor-Auth-Token", + HeaderValue::from_str(&auth_token.0).expect("Invalid token"), + ); + + let _ = rpc_client + .make_rpc_call(client_id, RpcRequest::Detach(client_token.clone())) + .await + .expect("Unable to send detach to RPC server"); + + Response::builder() + .status(StatusCode::OK) + .header("X-Moor-Auth-Token", auth_token.0) + .body(format!("{} {:?}\n", player, connect_type)) + .unwrap() +} + pub async fn ws_connect_handler( ws: WebSocketUpgrade, TypedHeader(auth): TypedHeader>, diff --git a/docker-compose.yml b/docker-compose.yml index 4f526088..b578a433 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,7 +35,7 @@ services: working_dir: /moor command: > sh -c "RUST_BACKTRACE=1 cargo watch -w crates/kernel -w crates/rpc-common -w crates/daemon -w crates/values -w crates/db -x - 'run -p moor-daemon -- development.db --db-type=Tuplebox --textdump=JHCore-DEV-2.db'" + 'run -p moor-daemon -- development.db --db-type=Tuplebox --textdump=JHCore-DEV-2.db --generate-keypair'" ports: # ZMQ ports - "7899:7899"