From 8aca5851f2bb2d1d1a31c89f051e1899e90f6eca Mon Sep 17 00:00:00 2001 From: "Sijie.Sun" Date: Sat, 2 Nov 2024 15:13:19 +0800 Subject: [PATCH] feat/web: Patchset 3 (#455) https://apifox.com/apidoc/shared-ceda7a60-e817-4ea8-827b-de4e874dc45e implement all backend API --- Cargo.lock | 1445 ++++++++++++++++- easytier-web/Cargo.toml | 23 + easytier-web/migrations/20241026_init.sql | 85 + easytier-web/resources/robot.ttf | Bin 0 -> 45072 bytes easytier-web/src/client_manager/mod.rs | 30 +- easytier-web/src/client_manager/session.rs | 127 +- easytier-web/src/client_manager/storage.rs | 18 +- easytier-web/src/db/entity/groups.rs | 35 + .../src/db/entity/groups_permissions.rs | 47 + easytier-web/src/db/entity/mod.rs | 11 + easytier-web/src/db/entity/permissions.rs | 27 + easytier-web/src/db/entity/prelude.rs | 9 + easytier-web/src/db/entity/tower_sessions.rs | 19 + .../db/entity/user_running_network_configs.rs | 39 + easytier-web/src/db/entity/users.rs | 36 + easytier-web/src/db/entity/users_groups.rs | 47 + easytier-web/src/db/mod.rs | 215 +++ easytier-web/src/main.rs | 24 +- .../src/migrator/m20241029_000001_init.rs | 450 +++++ easytier-web/src/migrator/mod.rs | 12 + easytier-web/src/restful/auth.rs | 171 ++ .../src/restful/captcha/base/captcha.rs | 308 ++++ easytier-web/src/restful/captcha/base/mod.rs | 4 + .../src/restful/captcha/base/randoms.rs | 86 + .../src/restful/captcha/captcha/mod.rs | 1 + .../src/restful/captcha/captcha/spec.rs | 318 ++++ .../captcha/extension/axum_tower_sessions.rs | 69 + .../src/restful/captcha/extension/mod.rs | 41 + easytier-web/src/restful/captcha/mod.rs | 134 ++ .../src/restful/captcha/utils/color.rs | 53 + .../src/restful/captcha/utils/font.rs | 45 + easytier-web/src/restful/captcha/utils/mod.rs | 4 + easytier-web/src/restful/mod.rs | 278 ++-- easytier-web/src/restful/network.rs | 321 ++++ easytier-web/src/restful/users.rs | 241 +++ easytier/src/common/mod.rs | 1 - easytier/src/proto/common.rs | 6 + easytier/src/proto/web.proto | 10 +- easytier/src/tunnel/packet_def.rs | 2 +- easytier/src/web_client/controller.rs | 8 +- easytier/src/web_client/session.rs | 38 +- 41 files changed, 4621 insertions(+), 217 deletions(-) create mode 100644 easytier-web/migrations/20241026_init.sql create mode 100644 easytier-web/resources/robot.ttf create mode 100644 easytier-web/src/db/entity/groups.rs create mode 100644 easytier-web/src/db/entity/groups_permissions.rs create mode 100644 easytier-web/src/db/entity/mod.rs create mode 100644 easytier-web/src/db/entity/permissions.rs create mode 100644 easytier-web/src/db/entity/prelude.rs create mode 100644 easytier-web/src/db/entity/tower_sessions.rs create mode 100644 easytier-web/src/db/entity/user_running_network_configs.rs create mode 100644 easytier-web/src/db/entity/users.rs create mode 100644 easytier-web/src/db/entity/users_groups.rs create mode 100644 easytier-web/src/db/mod.rs create mode 100644 easytier-web/src/migrator/m20241029_000001_init.rs create mode 100644 easytier-web/src/migrator/mod.rs create mode 100644 easytier-web/src/restful/auth.rs create mode 100644 easytier-web/src/restful/captcha/base/captcha.rs create mode 100644 easytier-web/src/restful/captcha/base/mod.rs create mode 100644 easytier-web/src/restful/captcha/base/randoms.rs create mode 100644 easytier-web/src/restful/captcha/captcha/mod.rs create mode 100644 easytier-web/src/restful/captcha/captcha/spec.rs create mode 100644 easytier-web/src/restful/captcha/extension/axum_tower_sessions.rs create mode 100644 easytier-web/src/restful/captcha/extension/mod.rs create mode 100644 easytier-web/src/restful/captcha/mod.rs create mode 100644 easytier-web/src/restful/captcha/utils/color.rs create mode 100644 easytier-web/src/restful/captcha/utils/font.rs create mode 100644 easytier-web/src/restful/captcha/utils/mod.rs create mode 100644 easytier-web/src/restful/network.rs create mode 100644 easytier-web/src/restful/users.rs diff --git a/Cargo.lock b/Cargo.lock index e885bd2..252f03b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" + [[package]] name = "addr2line" version = "0.22.0" @@ -52,6 +58,29 @@ dependencies = [ "subtle", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -61,6 +90,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -76,6 +111,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -146,6 +187,15 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "arboard" version = "3.4.0" @@ -170,6 +220,24 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash 0.5.0", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "async-broadcast" version = "0.7.1" @@ -379,6 +447,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-shim" version = "0.2.0" @@ -484,6 +561,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-login" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5260ed0ecc8ace8e7e61a7406672faba598c8a86b8f4742fcdde0ddc979a318f" +dependencies = [ + "async-trait", + "axum", + "form_urlencoded", + "serde", + "subtle", + "thiserror", + "tower-cookies", + "tower-layer", + "tower-service", + "tower-sessions", + "tracing", + "urlencoding", +] + [[package]] name = "axum-macros" version = "0.4.2" @@ -495,6 +592,23 @@ dependencies = [ "syn 2.0.74", ] +[[package]] +name = "axum-messages" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40e85c86a8bd84f54833bca296a0204bd865958ade62bacadeae92dda34cfb8a" +dependencies = [ + "async-trait", + "axum-core", + "http 1.1.0", + "parking_lot", + "serde", + "serde_json", + "tower 0.4.13", + "tower-sessions-core", + "tracing", +] + [[package]] name = "backtrace" version = "0.3.73" @@ -540,6 +654,20 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bigdecimal" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f850665a0385e070b64c38d2354e6c104c8479c59868d1e48a0c13ee2c7a1c1" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + [[package]] name = "bit_field" version = "0.10.2" @@ -561,6 +689,18 @@ dependencies = [ "serde", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.10.6" @@ -632,6 +772,30 @@ dependencies = [ "x25519-dalek", ] +[[package]] +name = "borsh" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" +dependencies = [ + "once_cell", + "proc-macro-crate 3.1.0", + "proc-macro2", + "quote", + "syn 2.0.74", + "syn_derive", +] + [[package]] name = "brotli" version = "3.5.0" @@ -669,6 +833,28 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytecodec" version = "0.4.15" @@ -1072,18 +1258,49 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "constant_time_eq" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "conv" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ff10625fd0ac447827aa30ea8b861fead473bb60aeb73af6c1c58caf0d1299" +dependencies = [ + "custom_derive", +] + [[package]] name = "convert_case" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "base64 0.22.1", + "hmac", + "percent-encoding", + "rand 0.8.5", + "sha2", + "subtle", + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1342,6 +1559,12 @@ dependencies = [ "syn 2.0.74", ] +[[package]] +name = "custom_derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9" + [[package]] name = "darling" version = "0.20.10" @@ -1443,6 +1666,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.3.11" @@ -1494,6 +1728,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -1568,6 +1803,12 @@ dependencies = [ "syn 2.0.74", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "dpi" version = "0.1.1" @@ -1750,12 +1991,27 @@ dependencies = [ "anyhow", "async-trait", "axum", + "axum-login", + "axum-messages", + "base64 0.22.1", "clap", "dashmap", "easytier", + "image 0.24.9", + "imageproc", + "password-auth", + "rand 0.8.5", + "rust-embed", + "rusttype", + "sea-orm", + "sea-orm-migration", "serde", + "sqlx", "thiserror", "tokio", + "tower-http", + "tower-sessions", + "tower-sessions-sqlx-store", "tracing", "url", "uuid", @@ -1766,6 +2022,9 @@ name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +dependencies = [ + "serde", +] [[package]] name = "embed-resource" @@ -1919,6 +2178,17 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "event-listener" version = "5.3.1" @@ -2009,6 +2279,8 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ + "futures-core", + "futures-sink", "spin 0.9.8", ] @@ -2069,6 +2341,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futf" version = "0.1.5" @@ -2121,6 +2399,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.30" @@ -2356,8 +2645,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -2621,12 +2912,28 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.11", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] [[package]] name = "heapless" @@ -2668,6 +2975,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -2950,6 +3266,24 @@ dependencies = [ "tiff", ] +[[package]] +name = "imageproc" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aee993351d466301a29655d628bfc6f5a35a0d062b6160ca0808f425805fd7" +dependencies = [ + "approx", + "conv", + "image 0.24.9", + "itertools 0.10.5", + "nalgebra", + "num", + "rand 0.7.3", + "rand_distr", + "rayon", + "rusttype", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -2981,6 +3315,17 @@ dependencies = [ "cfb", ] +[[package]] +name = "inherent" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0122b7114117e64a63ac49f752a5ca4624d534c7b1c7de796ac196381cd2d947" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.74", +] + [[package]] name = "inout" version = "0.1.3" @@ -3061,6 +3406,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.11.0" @@ -3070,6 +3424,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -3217,6 +3580,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin 0.9.8", +] [[package]] name = "lebe" @@ -3290,6 +3656,17 @@ dependencies = [ "libc", ] +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "libyml" version = "0.0.4" @@ -3310,6 +3687,7 @@ checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", + "serde", ] [[package]] @@ -3399,6 +3777,26 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "matrixmultiply" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "md5" version = "0.7.0" @@ -3446,6 +3844,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.4" @@ -3493,6 +3897,21 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +[[package]] +name = "nalgebra" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb2d0de08694bed883320212c18ee3008576bfe8c306f4c3c4a58b4876998be" +dependencies = [ + "approx", + "matrixmultiply", + "num-complex", + "num-rational", + "num-traits", + "simba", + "typenum", +] + [[package]] name = "native-tls" version = "0.2.12" @@ -3680,6 +4099,16 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "normpath" version = "1.3.0" @@ -3699,6 +4128,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -3709,6 +4152,32 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -3724,6 +4193,28 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3731,6 +4222,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -3973,6 +4465,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "3.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -4004,12 +4505,46 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "ouroboros" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "944fa20996a25aded6b4795c6d63f10014a7a83f8be9828a11860b08c5fc4a67" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39b0deead1528fd0e5947a8546a9642a9777c25f6e1e26f34c97b204bbb465bd" +dependencies = [ + "heck 0.4.1", + "itertools 0.12.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.74", +] + [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "owned_ttf_parser" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e6affeb1632d6ff6a23d2cd40ffed138e82f1532571a26f527c8a284bb2fbb" +dependencies = [ + "ttf-parser", +] + [[package]] name = "pango" version = "0.18.3" @@ -4075,6 +4610,18 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-auth" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2a4764cc1f8d961d802af27193c6f4f0124bd0e76e8393cf818e18880f0524" +dependencies = [ + "argon2", + "getrandom 0.2.15", + "password-hash 0.5.0", + "rand_core 0.6.4", +] + [[package]] name = "password-hash" version = "0.4.2" @@ -4086,6 +4633,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" @@ -4106,7 +4664,7 @@ checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ "digest", "hmac", - "password-hash", + "password-hash 0.4.2", "sha2", ] @@ -4120,6 +4678,15 @@ dependencies = [ "serde", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -4313,6 +4880,27 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -4571,6 +5159,28 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.74", +] + [[package]] name = "proc-macro-hack" version = "0.5.20+deprecated" @@ -4586,6 +5196,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.74", + "version_check", + "yansi", +] + [[package]] name = "prost" version = "0.13.2" @@ -4639,6 +5262,26 @@ dependencies = [ "prost", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "qoi" version = "0.4.1" @@ -4715,6 +5358,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -4778,6 +5427,15 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "rand_distr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96977acbdd3a6576fb1d27391900035bf3863d4a16422973a409b488cf29ffb2" +dependencies = [ + "rand 0.7.3", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -4808,6 +5466,12 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "rayon" version = "1.10.0" @@ -4910,6 +5574,15 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "reqwest" version = "0.11.27" @@ -5026,6 +5699,77 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rstest" version = "0.18.2" @@ -5055,6 +5799,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "rust-embed" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.74", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rust-i18n" version = "3.1.2" @@ -5109,6 +5887,22 @@ dependencies = [ "triomphe", ] +[[package]] +name = "rust_decimal" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -5233,6 +6027,16 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "rusttype" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff8374aa04134254b7995b63ad3dc41c7f7236f69528b28553da7d72efaa967" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + [[package]] name = "rustversion" version = "1.0.17" @@ -5245,6 +6049,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "safe_arch" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3460605018fdc9612bce72735cba0d27efbcd9904780d44c7e3a9948f96148a" +dependencies = [ + "bytemuck", +] + [[package]] name = "same-file" version = "1.0.6" @@ -5316,6 +6129,171 @@ version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0495e4577c672de8254beb68d01a9b62d0e8a13c099edecdbedccce3223cd29f" +[[package]] +name = "sea-bae" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25" +dependencies = [ + "heck 0.4.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.74", +] + +[[package]] +name = "sea-orm" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4872675cc5d5d399a2a202c60f3a393ec8d3f3307c36adb166517f348e4db5" +dependencies = [ + "async-stream", + "async-trait", + "bigdecimal", + "chrono", + "futures", + "log", + "ouroboros", + "rust_decimal", + "sea-orm-macros", + "sea-query", + "sea-query-binder", + "serde", + "serde_json", + "sqlx", + "strum", + "thiserror", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sea-orm-cli" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aefbd960c9ed7b2dfbab97b11890f5d8c314ad6e2f68c7b36c73ea0967fcc25" +dependencies = [ + "chrono", + "clap", + "dotenvy", + "glob", + "regex", + "sea-schema", + "tracing", + "tracing-subscriber", + "url", +] + +[[package]] +name = "sea-orm-macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85f714906b72e7265c0b2077d0ad8f235dabebda513c92f1326d5d40cef0dd01" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "sea-bae", + "syn 2.0.74", + "unicode-ident", +] + +[[package]] +name = "sea-orm-migration" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa7bbfbe3bec60b5925193acc9c98b9f8ae9853f52c8004df0c1ea5193c01ea0" +dependencies = [ + "async-trait", + "clap", + "dotenvy", + "futures", + "sea-orm", + "sea-orm-cli", + "sea-schema", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "sea-query" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff504d13b5e4b52fffcf2fb203d0352a5722fa5151696db768933e41e1e591bb" +dependencies = [ + "bigdecimal", + "chrono", + "inherent", + "ordered-float", + "rust_decimal", + "sea-query-derive", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "sea-query-binder" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608" +dependencies = [ + "bigdecimal", + "chrono", + "rust_decimal", + "sea-query", + "serde_json", + "sqlx", + "time", + "uuid", +] + +[[package]] +name = "sea-query-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9834af2c4bd8c5162f00c89f1701fb6886119a88062cf76fe842ea9e232b9839" +dependencies = [ + "darling", + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.74", + "thiserror", +] + +[[package]] +name = "sea-schema" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aab1592d17860a9a8584d9b549aebcd06f7bdc3ff615f71752486ba0b05b1e6e" +dependencies = [ + "futures", + "sea-query", + "sea-schema-derive", +] + +[[package]] +name = "sea-schema-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdc8729c37fdbf88472f97fd470393089f997a909e535ff67c544d18cfccf0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.74", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "security-framework" version = "2.11.1" @@ -5625,12 +6603,41 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simba" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3fd720c48c53cace224ae62bef1bbff363a70c68c4802a78b5cc6159618176" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + [[package]] name = "simd-adler32" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "0.3.11" @@ -5657,6 +6664,9 @@ name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] [[package]] name = "smoltcp" @@ -5746,6 +6756,242 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +dependencies = [ + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e" +dependencies = [ + "atoi", + "bigdecimal", + "byteorder", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.14.5", + "hashlink", + "hex", + "indexmap 2.4.0", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rust_decimal", + "rustls", + "rustls-pemfile 2.1.3", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.74", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.74", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" +dependencies = [ + "atoi", + "base64 0.22.1", + "bigdecimal", + "bitflags 2.6.0", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa 1.0.11", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "rust_decimal", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" +dependencies = [ + "atoi", + "base64 0.22.1", + "bigdecimal", + "bitflags 2.6.0", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa 1.0.11", + "log", + "md-5", + "memchr", + "num-bigint", + "once_cell", + "rand 0.8.5", + "rust_decimal", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "time", + "tracing", + "url", + "uuid", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -5793,12 +7039,29 @@ dependencies = [ "quote", ] +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + [[package]] name = "stun_codec" version = "0.3.5" @@ -5853,6 +7116,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.74", +] + [[package]] name = "sync_wrapper" version = "0.1.2" @@ -5995,6 +7270,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "target-lexicon" version = "0.12.16" @@ -6688,6 +7969,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -6706,6 +7988,37 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-cookies" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fd0118512cf0b3768f7fcccf0bef1ae41d68f2b45edc1e77432b36c97c56c6d" +dependencies = [ + "async-trait", + "axum-core", + "cookie", + "futures-util", + "http 1.1.0", + "parking_lot", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97" +dependencies = [ + "bitflags 2.6.0", + "bytes", + "http 1.1.0", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -6718,6 +8031,71 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +[[package]] +name = "tower-sessions" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65856c81ee244e0f8a55ab0f7b769b72fbde387c235f0a73cd97c579818d05eb" +dependencies = [ + "async-trait", + "http 1.1.0", + "time", + "tokio", + "tower-cookies", + "tower-layer", + "tower-service", + "tower-sessions-core", + "tower-sessions-memory-store", + "tracing", +] + +[[package]] +name = "tower-sessions-core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb6abbfcaf6436ec5a772cd9f965401da12db793e404ae6134eac066fa5a04f3" +dependencies = [ + "async-trait", + "axum-core", + "base64 0.22.1", + "futures", + "http 1.1.0", + "parking_lot", + "rand 0.8.5", + "serde", + "serde_json", + "thiserror", + "time", + "tokio", + "tracing", +] + +[[package]] +name = "tower-sessions-memory-store" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fad75660c8afbe74f4e7cbbe8e9090171a056b57370ea4d7d5e9eb3e4af3092" +dependencies = [ + "async-trait", + "time", + "tokio", + "tower-sessions-core", +] + +[[package]] +name = "tower-sessions-sqlx-store" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81096d43c87c3bfa559116ac2b02d4a8398f0718be33e63685c467890ff4194" +dependencies = [ + "async-trait", + "rmp-serde", + "sqlx", + "thiserror", + "time", + "tower-sessions-core", +] + [[package]] name = "tracing" version = "0.1.40" @@ -6859,6 +8237,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ttf-parser" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b3e06c9b9d80ed6b745c7159c40b311ad2916abb34a49e9be2653b90db0d8dd" + [[package]] name = "tun-easytier" version = "1.1.1" @@ -6974,6 +8358,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + [[package]] name = "unicode-segmentation" version = "1.11.0" @@ -6986,6 +8376,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "universal-hash" version = "0.5.1" @@ -7020,6 +8416,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "urlpattern" version = "0.2.0" @@ -7149,6 +8551,12 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.93" @@ -7346,6 +8754,26 @@ dependencies = [ "rustix", ] +[[package]] +name = "whoami" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +dependencies = [ + "redox_syscall", + "wasite", +] + +[[package]] +name = "wide" +version = "0.7.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b828f995bf1e9622031f8009f8481a85406ce1f4d4588ff746d872043e855690" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "widestring" version = "1.1.0" @@ -7809,6 +9237,15 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11" version = "2.21.0" @@ -7869,6 +9306,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yasna" version = "0.5.2" diff --git a/easytier-web/Cargo.toml b/easytier-web/Cargo.toml index c876d9d..9d1cde5 100644 --- a/easytier-web/Cargo.toml +++ b/easytier-web/Cargo.toml @@ -12,7 +12,30 @@ tokio = { version = "1", features = ["full"] } dashmap = "6.1" url = "2.2" async-trait = "0.1" + axum = { version = "0.7", features = ["macros"] } +axum-login = { version = "0.16" } +password-auth = { version = "1.0.0" } +axum-messages = "0.7.0" +tower-sessions-sqlx-store = { version = "0.14.1", features = ["sqlite"] } +tower-sessions = { version = "0.13.0", default-features = false, features = [ + "signed", +] } +tower-http = { version = "0.6", features = ["cors"] } +sqlx = { version = "0.8", features = ["sqlite"] } +sea-orm = { version = "1.1", features = [ "sqlx-sqlite", "runtime-tokio-rustls", "macros" ] } +sea-orm-migration = { version = "1.1" } + + +# for captcha +rust-embed = { version = "8.5.0", features = ["debug-embed"] } +base64 = "0.22" +rand = "0.8" +image = {version="0.24", default-features = false, features = ["png"]} +rusttype = "0.9.3" +imageproc = "0.23.0" + + clap = { version = "4.4.8", features = [ "string", "unicode", diff --git a/easytier-web/migrations/20241026_init.sql b/easytier-web/migrations/20241026_init.sql new file mode 100644 index 0000000..43e19a4 --- /dev/null +++ b/easytier-web/migrations/20241026_init.sql @@ -0,0 +1,85 @@ +-- # Entity schema. + +-- Create `users` table. +create table if not exists users ( + id integer primary key autoincrement, + username text not null unique, + password text not null +); + +-- Create `groups` table. +create table if not exists groups ( + id integer primary key autoincrement, + name text not null unique +); + +-- Create `permissions` table. +create table if not exists permissions ( + id integer primary key autoincrement, + name text not null unique +); + + +-- # Join tables. + +-- Create `users_groups` table for many-to-many relationships between users and groups. +create table if not exists users_groups ( + user_id integer references users(id), + group_id integer references groups(id), + primary key (user_id, group_id) +); + +-- Create `groups_permissions` table for many-to-many relationships between groups and permissions. +create table if not exists groups_permissions ( + group_id integer references groups(id), + permission_id integer references permissions(id), + primary key (group_id, permission_id) +); + + +-- # Fixture hydration. + +-- Insert "user" user. password: "user" +insert into users (username, password) +values ( + 'user', + '$argon2i$v=19$m=16,t=2,p=1$dHJ5dXZkYmZkYXM$UkrNqWz0BbSVBq4ykLSuJw' +); + +-- Insert "admin" user. password: "admin" +insert into users (username, password) +values ( + 'admin', + '$argon2i$v=19$m=16,t=2,p=1$Ymd1Y2FlcnQ$x0q4oZinW9S1ZB9BcaHEpQ' +); + +-- Insert "users" and "superusers" groups. +insert into groups (name) values ('users'); +insert into groups (name) values ('superusers'); + +-- Insert individual permissions. +insert into permissions (name) values ('sessions'); +insert into permissions (name) values ('devices'); + +-- Insert group permissions. +insert into groups_permissions (group_id, permission_id) +values ( + (select id from groups where name = 'users'), + (select id from permissions where name = 'devices') +), ( + (select id from groups where name = 'superusers'), + (select id from permissions where name = 'sessions') +); + +-- Insert users into groups. +insert into users_groups (user_id, group_id) +values ( + (select id from users where username = 'user'), + (select id from groups where name = 'users') +), ( + (select id from users where username = 'admin'), + (select id from groups where name = 'users') +), ( + (select id from users where username = 'admin'), + (select id from groups where name = 'superusers') +); diff --git a/easytier-web/resources/robot.ttf b/easytier-web/resources/robot.ttf new file mode 100644 index 0000000000000000000000000000000000000000..09e2de05db1cde8324746959043eb93401f626bb GIT binary patch literal 45072 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 Zt$j`#?Tpw)^_wC1r+zT1{CoBNe*r|Ms>, @@ -18,13 +22,13 @@ pub struct ClientManager { } impl ClientManager { - pub fn new() -> Self { + pub fn new(db: Db) -> Self { ClientManager { accept_task: None, clear_task: None, client_sessions: Arc::new(DashMap::new()), - storage: Storage::new(), + storage: Storage::new(db), } } @@ -41,7 +45,8 @@ impl ClientManager { let info = tunnel.info().unwrap(); let client_url: url::Url = info.remote_addr.unwrap().into(); println!("New session from {:?}", tunnel.info()); - let session = Session::new(tunnel, storage.clone(), client_url.clone()); + let mut session = Session::new(storage.clone(), client_url.clone()); + session.serve(tunnel).await; sessions.insert(client_url, Arc::new(session)); } }); @@ -87,6 +92,19 @@ impl ClientManager { .get(&c_url) .map(|item| item.value().clone()) } + + pub fn list_machine_by_token(&self, token: String) -> Vec { + self.storage.list_token_clients(&token) + } + + pub async fn get_heartbeat_requests(&self, client_url: &url::Url) -> Option { + let s = self.client_sessions.get(client_url)?.clone(); + s.data().read().await.req() + } + + pub fn db(&self) -> &Db { + self.storage.db() + } } #[cfg(test)] @@ -101,12 +119,12 @@ mod tests { web_client::WebClient, }; - use crate::client_manager::ClientManager; + use crate::{client_manager::ClientManager, db::Db}; #[tokio::test] async fn test_client() { let listener = UdpTunnelListener::new("udp://0.0.0.0:54333".parse().unwrap()); - let mut mgr = ClientManager::new(); + let mut mgr = ClientManager::new(Db::memory_db().await); mgr.serve(Box::new(listener)).await.unwrap(); let connector = UdpTunnelConnector::new("udp://127.0.0.1:54333".parse().unwrap()); diff --git a/easytier-web/src/client_manager/session.rs b/easytier-web/src/client_manager/session.rs index 506974d..2294947 100644 --- a/easytier-web/src/client_manager/session.rs +++ b/easytier-web/src/client_manager/session.rs @@ -1,12 +1,13 @@ use std::{fmt::Debug, sync::Arc}; use easytier::{ + common::scoped_task::ScopedTask, proto::{ rpc_impl::bidirect::BidirectRpcManager, rpc_types::{self, controller::BaseController}, web::{ - HeartbeatRequest, HeartbeatResponse, WebClientService, WebClientServiceClientFactory, - WebServerService, WebServerServiceServer, + HeartbeatRequest, HeartbeatResponse, RunNetworkInstanceRequest, WebClientService, + WebClientServiceClientFactory, WebServerService, WebServerServiceServer, }, }, tunnel::Tunnel, @@ -98,6 +99,8 @@ pub struct Session { rpc_mgr: BidirectRpcManager, data: SharedSessionData, + + run_network_on_start_task: Option>, } impl Debug for Session { @@ -106,20 +109,122 @@ impl Debug for Session { } } +type SessionRpcClient = Box + Send>; + impl Session { - pub fn new(tunnel: Box, storage: WeakRefStorage, client_url: url::Url) -> Self { + pub fn new(storage: WeakRefStorage, client_url: url::Url) -> Self { + let session_data = SessionData::new(storage, client_url); + let data = Arc::new(RwLock::new(session_data)); + let rpc_mgr = BidirectRpcManager::new().set_rx_timeout(Some(std::time::Duration::from_secs(30))); - rpc_mgr.run_with_tunnel(tunnel); - - let data = Arc::new(RwLock::new(SessionData::new(storage, client_url))); rpc_mgr.rpc_server().registry().register( WebServerServiceServer::new(SessionRpcService { data: data.clone() }), "", ); - Session { rpc_mgr, data } + Session { + rpc_mgr, + data, + run_network_on_start_task: None, + } + } + + pub async fn serve(&mut self, tunnel: Box) { + self.rpc_mgr.run_with_tunnel(tunnel); + + let data = self.data.read().await; + self.run_network_on_start_task.replace( + tokio::spawn(Self::run_network_on_start( + data.heartbeat_waiter(), + data.storage.clone(), + self.scoped_rpc_client(), + )) + .into(), + ); + } + + async fn run_network_on_start( + mut heartbeat_waiter: broadcast::Receiver, + storage: WeakRefStorage, + rpc_client: SessionRpcClient, + ) { + loop { + heartbeat_waiter = heartbeat_waiter.resubscribe(); + let req = heartbeat_waiter.recv().await; + if req.is_err() { + tracing::error!( + "Failed to receive heartbeat request, error: {:?}", + req.err() + ); + return; + } + let req = req.unwrap(); + let running_inst_ids = req + .running_network_instances + .iter() + .map(|x| x.to_string()) + .collect::>(); + let Some(storage) = storage.upgrade() else { + tracing::error!("Failed to get storage"); + return; + }; + + let user_id = match storage + .db + .get_user_id_by_token(req.user_token.clone()) + .await + { + Ok(Some(user_id)) => user_id, + Ok(None) => { + tracing::info!("User not found by token: {:?}", req.user_token); + return; + } + Err(e) => { + tracing::error!("Failed to get user id by token, error: {:?}", e); + return; + } + }; + + let local_configs = match storage.db.list_network_configs(user_id, true).await { + Ok(configs) => configs, + Err(e) => { + tracing::error!("Failed to list network configs, error: {:?}", e); + return; + } + }; + + let mut has_failed = false; + + for c in local_configs { + if running_inst_ids.contains(&c.network_instance_id) { + continue; + } + let ret = rpc_client + .run_network_instance( + BaseController::default(), + RunNetworkInstanceRequest { + inst_id: Some(c.network_instance_id.clone().into()), + config: c.network_config, + }, + ) + .await; + tracing::info!( + ?user_id, + "Run network instance: {:?}, user_token: {:?}", + ret, + req.user_token + ); + + has_failed |= ret.is_err(); + } + + if !has_failed { + tracing::info!(?req, "All network instances are running"); + break; + } + } } pub fn is_running(&self) -> bool { @@ -130,9 +235,7 @@ impl Session { self.data.clone() } - pub fn scoped_rpc_client( - &self, - ) -> Box + Send> { + pub fn scoped_rpc_client(&self) -> SessionRpcClient { self.rpc_mgr .rpc_client() .scoped_client::>(1, 1, "".to_string()) @@ -141,4 +244,8 @@ impl Session { pub async fn get_token(&self) -> Option { self.data.read().await.storage_token.clone() } + + pub async fn get_heartbeat_req(&self) -> Option { + self.data.read().await.req() + } } diff --git a/easytier-web/src/client_manager/storage.rs b/easytier-web/src/client_manager/storage.rs index 79dac9a..772fed0 100644 --- a/easytier-web/src/client_manager/storage.rs +++ b/easytier-web/src/client_manager/storage.rs @@ -2,6 +2,8 @@ use std::sync::{Arc, Weak}; use dashmap::{DashMap, DashSet}; +use crate::db::Db; + // use this to maintain Storage #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct StorageToken { @@ -15,6 +17,7 @@ pub struct StorageInner { // some map for indexing pub token_clients_map: DashMap>, pub machine_client_url_map: DashMap, + pub db: Db, } #[derive(Debug, Clone)] @@ -30,10 +33,11 @@ impl TryFrom for Storage { } impl Storage { - pub fn new() -> Self { + pub fn new(db: Db) -> Self { Storage(Arc::new(StorageInner { token_clients_map: DashMap::new(), machine_client_url_map: DashMap::new(), + db, })) } @@ -69,4 +73,16 @@ impl Storage { .get(&machine_id) .map(|url| url.clone()) } + + pub fn list_token_clients(&self, token: &str) -> Vec { + self.0 + .token_clients_map + .get(token) + .map(|set| set.iter().map(|url| url.clone()).collect()) + .unwrap_or_default() + } + + pub fn db(&self) -> &Db { + &self.0.db + } } diff --git a/easytier-web/src/db/entity/groups.rs b/easytier-web/src/db/entity/groups.rs new file mode 100644 index 0000000..0a6820c --- /dev/null +++ b/easytier-web/src/db/entity/groups.rs @@ -0,0 +1,35 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "groups")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(unique)] + pub name: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::groups_permissions::Entity")] + GroupsPermissions, + #[sea_orm(has_many = "super::users_groups::Entity")] + UsersGroups, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::GroupsPermissions.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::UsersGroups.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/easytier-web/src/db/entity/groups_permissions.rs b/easytier-web/src/db/entity/groups_permissions.rs new file mode 100644 index 0000000..10f22a6 --- /dev/null +++ b/easytier-web/src/db/entity/groups_permissions.rs @@ -0,0 +1,47 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "groups_permissions")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub group_id: i32, + pub permission_id: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::groups::Entity", + from = "Column::GroupId", + to = "super::groups::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Groups, + #[sea_orm( + belongs_to = "super::permissions::Entity", + from = "Column::PermissionId", + to = "super::permissions::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Permissions, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Groups.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Permissions.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/easytier-web/src/db/entity/mod.rs b/easytier-web/src/db/entity/mod.rs new file mode 100644 index 0000000..1cda5cf --- /dev/null +++ b/easytier-web/src/db/entity/mod.rs @@ -0,0 +1,11 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 + +pub mod prelude; + +pub mod groups; +pub mod groups_permissions; +pub mod permissions; +pub mod tower_sessions; +pub mod user_running_network_configs; +pub mod users; +pub mod users_groups; diff --git a/easytier-web/src/db/entity/permissions.rs b/easytier-web/src/db/entity/permissions.rs new file mode 100644 index 0000000..13897bc --- /dev/null +++ b/easytier-web/src/db/entity/permissions.rs @@ -0,0 +1,27 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "permissions")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(unique)] + pub name: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::groups_permissions::Entity")] + GroupsPermissions, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::GroupsPermissions.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/easytier-web/src/db/entity/prelude.rs b/easytier-web/src/db/entity/prelude.rs new file mode 100644 index 0000000..8191783 --- /dev/null +++ b/easytier-web/src/db/entity/prelude.rs @@ -0,0 +1,9 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 + +pub use super::groups::Entity as Groups; +pub use super::groups_permissions::Entity as GroupsPermissions; +pub use super::permissions::Entity as Permissions; +pub use super::tower_sessions::Entity as TowerSessions; +pub use super::user_running_network_configs::Entity as UserRunningNetworkConfigs; +pub use super::users::Entity as Users; +pub use super::users_groups::Entity as UsersGroups; diff --git a/easytier-web/src/db/entity/tower_sessions.rs b/easytier-web/src/db/entity/tower_sessions.rs new file mode 100644 index 0000000..473d234 --- /dev/null +++ b/easytier-web/src/db/entity/tower_sessions.rs @@ -0,0 +1,19 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "tower_sessions")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false, column_type = "Text")] + pub id: String, + #[sea_orm(column_type = "Blob")] + pub data: Vec, + pub expiry_date: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/easytier-web/src/db/entity/user_running_network_configs.rs b/easytier-web/src/db/entity/user_running_network_configs.rs new file mode 100644 index 0000000..0580e59 --- /dev/null +++ b/easytier-web/src/db/entity/user_running_network_configs.rs @@ -0,0 +1,39 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "user_running_network_configs")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub user_id: i32, + #[sea_orm(column_type = "Text", unique)] + pub network_instance_id: String, + #[sea_orm(column_type = "Text")] + pub network_config: String, + pub disabled: bool, + pub create_time: DateTimeWithTimeZone, + pub update_time: DateTimeWithTimeZone, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::users::Entity", + from = "Column::UserId", + to = "super::users::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Users, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Users.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/easytier-web/src/db/entity/users.rs b/easytier-web/src/db/entity/users.rs new file mode 100644 index 0000000..343771e --- /dev/null +++ b/easytier-web/src/db/entity/users.rs @@ -0,0 +1,36 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "users")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(unique)] + pub username: String, + pub password: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::user_running_network_configs::Entity")] + UserRunningNetworkConfigs, + #[sea_orm(has_many = "super::users_groups::Entity")] + UsersGroups, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserRunningNetworkConfigs.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::UsersGroups.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/easytier-web/src/db/entity/users_groups.rs b/easytier-web/src/db/entity/users_groups.rs new file mode 100644 index 0000000..2bde90f --- /dev/null +++ b/easytier-web/src/db/entity/users_groups.rs @@ -0,0 +1,47 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "users_groups")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub user_id: i32, + pub group_id: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::groups::Entity", + from = "Column::GroupId", + to = "super::groups::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Groups, + #[sea_orm( + belongs_to = "super::users::Entity", + from = "Column::UserId", + to = "super::users::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Users, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Groups.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Users.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/easytier-web/src/db/mod.rs b/easytier-web/src/db/mod.rs new file mode 100644 index 0000000..6db3dab --- /dev/null +++ b/easytier-web/src/db/mod.rs @@ -0,0 +1,215 @@ +// sea-orm-cli generate entity -u sqlite:./et.db -o easytier-web/src/db/entity/ --with-serde both --with-copy-enums +#[allow(unused_imports)] +pub mod entity; + +use entity::user_running_network_configs; +use sea_orm::{ + sea_query::OnConflict, ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait as _, + QueryFilter as _, SqlxSqliteConnector, TransactionTrait as _, +}; +use sea_orm_migration::MigratorTrait as _; +use sqlx::{migrate::MigrateDatabase as _, types::chrono, Sqlite, SqlitePool}; + +use crate::migrator; + +type UserIdInDb = i32; + +#[derive(Debug, Clone)] +pub struct Db { + db_path: String, + db: SqlitePool, + orm_db: DatabaseConnection, +} + +impl Db { + pub async fn new(db_path: T) -> anyhow::Result { + let db = Self::prepare_db(db_path.to_string().as_str()).await?; + let orm_db = SqlxSqliteConnector::from_sqlx_sqlite_pool(db.clone()); + migrator::Migrator::up(&orm_db, None).await?; + + Ok(Self { + db_path: db_path.to_string(), + db, + orm_db, + }) + } + + pub async fn memory_db() -> Self { + Self::new(":memory:").await.unwrap() + } + + #[tracing::instrument(ret)] + async fn prepare_db(db_path: &str) -> anyhow::Result { + if !Sqlite::database_exists(db_path).await.unwrap_or(false) { + tracing::info!("Database not found, creating a new one"); + Sqlite::create_database(db_path).await?; + } + + let db = sqlx::pool::PoolOptions::new() + .max_lifetime(None) + .idle_timeout(None) + .connect(db_path) + .await?; + + Ok(db) + } + + pub fn inner(&self) -> SqlitePool { + self.db.clone() + } + + pub fn orm_db(&self) -> &DatabaseConnection { + &self.orm_db + } + + pub async fn insert_or_update_user_network_config( + &self, + user_id: UserIdInDb, + network_inst_id: uuid::Uuid, + network_config: T, + ) -> Result<(), DbErr> { + let txn = self.orm_db().begin().await?; + + use entity::user_running_network_configs as urnc; + + let on_conflict = OnConflict::column(urnc::Column::NetworkInstanceId) + .update_columns([ + urnc::Column::NetworkConfig, + urnc::Column::Disabled, + urnc::Column::UpdateTime, + ]) + .to_owned(); + let insert_m = urnc::ActiveModel { + user_id: sea_orm::Set(user_id), + network_instance_id: sea_orm::Set(network_inst_id.to_string()), + network_config: sea_orm::Set(network_config.to_string()), + disabled: sea_orm::Set(false), + create_time: sea_orm::Set(chrono::Local::now().fixed_offset()), + update_time: sea_orm::Set(chrono::Local::now().fixed_offset()), + ..Default::default() + }; + urnc::Entity::insert(insert_m) + .on_conflict(on_conflict) + .do_nothing() + .exec(&txn) + .await?; + + txn.commit().await + } + + pub async fn delete_network_config( + &self, + user_id: UserIdInDb, + network_inst_id: uuid::Uuid, + ) -> Result<(), DbErr> { + use entity::user_running_network_configs as urnc; + + urnc::Entity::delete_many() + .filter(urnc::Column::UserId.eq(user_id)) + .filter(urnc::Column::NetworkInstanceId.eq(network_inst_id.to_string())) + .exec(self.orm_db()) + .await?; + + Ok(()) + } + + pub async fn list_network_configs( + &self, + user_id: UserIdInDb, + only_enabled: bool, + ) -> Result, DbErr> { + use entity::user_running_network_configs as urnc; + + let configs = urnc::Entity::find().filter(urnc::Column::UserId.eq(user_id)); + let configs = if only_enabled { + configs.filter(urnc::Column::Disabled.eq(false)) + } else { + configs + }; + + let configs = configs.all(self.orm_db()).await?; + + Ok(configs) + } + + pub async fn get_user_id( + &self, + user_name: T, + ) -> Result, DbErr> { + use entity::users as u; + + let user = u::Entity::find() + .filter(u::Column::Username.eq(user_name.to_string())) + .one(self.orm_db()) + .await?; + + Ok(user.map(|u| u.id)) + } + + // TODO: currently we don't have a token system, so we just use the user name as token + pub async fn get_user_id_by_token( + &self, + token: T, + ) -> Result, DbErr> { + self.get_user_id(token).await + } +} + +#[cfg(test)] +mod tests { + use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _}; + + use crate::db::{entity::user_running_network_configs, Db}; + + #[tokio::test] + async fn test_user_network_config_management() { + let db = Db::memory_db().await; + let user_id = 1; + let network_config = "test_config"; + let inst_id = uuid::Uuid::new_v4(); + + db.insert_or_update_user_network_config(user_id, inst_id, network_config) + .await + .unwrap(); + + let result = user_running_network_configs::Entity::find() + .filter(user_running_network_configs::Column::UserId.eq(user_id)) + .one(db.orm_db()) + .await + .unwrap() + .unwrap(); + println!("{:?}", result); + assert_eq!(result.network_config, network_config); + + // overwrite the config + let network_config = "test_config2"; + db.insert_or_update_user_network_config(user_id, inst_id, network_config) + .await + .unwrap(); + + let result2 = user_running_network_configs::Entity::find() + .filter(user_running_network_configs::Column::UserId.eq(user_id)) + .one(db.orm_db()) + .await + .unwrap() + .unwrap(); + println!("{:?}", result2); + assert_eq!(result2.network_config, network_config); + + assert_eq!(result.create_time, result2.create_time); + assert_ne!(result.update_time, result2.update_time); + + assert_eq!( + db.list_network_configs(user_id, true).await.unwrap().len(), + 1 + ); + + db.delete_network_config(user_id, inst_id).await.unwrap(); + let result3 = user_running_network_configs::Entity::find() + .filter(user_running_network_configs::Column::UserId.eq(user_id)) + .one(db.orm_db()) + .await + .unwrap(); + assert!(result3.is_none()); + } +} diff --git a/easytier-web/src/main.rs b/easytier-web/src/main.rs index 2df5702..c12c0a0 100644 --- a/easytier-web/src/main.rs +++ b/easytier-web/src/main.rs @@ -2,21 +2,37 @@ use std::sync::Arc; -use easytier::tunnel::udp::UdpTunnelListener; +use easytier::{ + common::config::{ConfigLoader, ConsoleLoggerConfig, TomlConfigLoader}, + tunnel::udp::UdpTunnelListener, + utils::init_logger, +}; mod client_manager; +mod db; +mod migrator; mod restful; #[tokio::main] async fn main() { + let config = TomlConfigLoader::default(); + config.set_console_logger_config(ConsoleLoggerConfig { + level: Some("trace".to_string()), + }); + init_logger(config, false).unwrap(); + + // let db = db::Db::new(":memory:").await.unwrap(); + let db = db::Db::new("et.db").await.unwrap(); + let listener = UdpTunnelListener::new("udp://0.0.0.0:22020".parse().unwrap()); - let mut mgr = client_manager::ClientManager::new(); + let mut mgr = client_manager::ClientManager::new(db.clone()); mgr.serve(listener).await.unwrap(); let mgr = Arc::new(mgr); let mut restful_server = - restful::RestfulServer::new("0.0.0.0:11211".parse().unwrap(), mgr.clone()); + restful::RestfulServer::new("0.0.0.0:11211".parse().unwrap(), mgr.clone(), db) + .await + .unwrap(); restful_server.start().await.unwrap(); - tokio::signal::ctrl_c().await.unwrap(); } diff --git a/easytier-web/src/migrator/m20241029_000001_init.rs b/easytier-web/src/migrator/m20241029_000001_init.rs new file mode 100644 index 0000000..dc9b871 --- /dev/null +++ b/easytier-web/src/migrator/m20241029_000001_init.rs @@ -0,0 +1,450 @@ +// src/migrator/m20220602_000001_create_bakery_table.rs (create new file) + +use sea_orm_migration::{prelude::*, schema::*}; + +pub struct Migration; + +/* +-- # Entity schema. + +-- Create `users` table. +create table if not exists users ( + id integer primary key autoincrement, + username text not null unique, + password text not null +); + +-- Create `groups` table. +create table if not exists groups ( + id integer primary key autoincrement, + name text not null unique +); + +-- Create `permissions` table. +create table if not exists permissions ( + id integer primary key autoincrement, + name text not null unique +); + + +-- # Join tables. + +-- Create `users_groups` table for many-to-many relationships between users and groups. +create table if not exists users_groups ( + user_id integer references users(id), + group_id integer references groups(id), + primary key (user_id, group_id) +); + +-- Create `groups_permissions` table for many-to-many relationships between groups and permissions. +create table if not exists groups_permissions ( + group_id integer references groups(id), + permission_id integer references permissions(id), + primary key (group_id, permission_id) +); + + +-- # Fixture hydration. + +-- Insert "user" user. password: "user" +insert into users (username, password) +values ( + 'user', + '$argon2i$v=19$m=16,t=2,p=1$dHJ5dXZkYmZkYXM$UkrNqWz0BbSVBq4ykLSuJw' +); + +-- Insert "admin" user. password: "admin" +insert into users (username, password) +values ( + 'admin', + '$argon2i$v=19$m=16,t=2,p=1$Ymd1Y2FlcnQ$x0q4oZinW9S1ZB9BcaHEpQ' +); + +-- Insert "users" and "superusers" groups. +insert into groups (name) values ('users'); +insert into groups (name) values ('superusers'); + +-- Insert individual permissions. +insert into permissions (name) values ('sessions'); +insert into permissions (name) values ('devices'); + +-- Insert group permissions. +insert into groups_permissions (group_id, permission_id) +values ( + (select id from groups where name = 'users'), + (select id from permissions where name = 'devices') +), ( + (select id from groups where name = 'superusers'), + (select id from permissions where name = 'sessions') +); + +-- Insert users into groups. +insert into users_groups (user_id, group_id) +values ( + (select id from users where username = 'user'), + (select id from groups where name = 'users') +), ( + (select id from users where username = 'admin'), + (select id from groups where name = 'users') +), ( + (select id from users where username = 'admin'), + (select id from groups where name = 'superusers') +); + */ + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20241029_000001_init" + } +} + +#[derive(DeriveIden)] +pub enum Users { + Table, + Id, + Username, + Password, +} + +#[derive(DeriveIden)] +enum Groups { + Table, + Id, + Name, +} + +#[derive(DeriveIden)] +enum Permissions { + Table, + Id, + Name, +} + +#[derive(DeriveIden)] +enum UsersGroups { + Table, + Id, + UserId, + GroupId, +} + +#[derive(DeriveIden)] +enum GroupsPermissions { + Table, + Id, + GroupId, + PermissionId, +} + +#[derive(DeriveIden)] +enum UserRunningNetworkConfigs { + Table, + Id, + UserId, + NetworkInstanceId, + NetworkConfig, + Disabled, + CreateTime, + UpdateTime, +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + // Define how to apply this migration: Create the Bakery table. + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // Create the `users` table. + manager + .create_table( + Table::create() + .if_not_exists() + .table(Users::Table) + .col(pk_auto(Users::Id).not_null()) + .col(string(Users::Username).not_null().unique_key()) + .col(string(Users::Password).not_null()) + .to_owned(), + ) + .await?; + manager + .create_index( + Index::create() + .name("idx_users_username") + .table(Users::Table) + .col(Users::Username) + .to_owned(), + ) + .await?; + + // Create the `groups` table. + manager + .create_table( + Table::create() + .if_not_exists() + .table(Groups::Table) + .col(pk_auto(Groups::Id).not_null()) + .col(string(Groups::Name).not_null().unique_key()) + .to_owned(), + ) + .await?; + manager + .create_index( + Index::create() + .name("idx_groups_name") + .table(Groups::Table) + .col(Groups::Name) + .to_owned(), + ) + .await?; + + // Create the `permissions` table. + manager + .create_table( + Table::create() + .if_not_exists() + .table(Permissions::Table) + .col(pk_auto(Permissions::Id).not_null()) + .col(string(Permissions::Name).not_null().unique_key()) + .to_owned(), + ) + .await?; + + // Create the `users_groups` table. + manager + .create_table( + Table::create() + .if_not_exists() + .table(UsersGroups::Table) + .col(pk_auto(UsersGroups::Id).not_null()) + .col(integer(UsersGroups::UserId).not_null()) + .col(integer(UsersGroups::GroupId).not_null()) + .foreign_key( + ForeignKey::create() + .name("fk_users_groups_user_id_to_users_id") + .from(UsersGroups::Table, UsersGroups::UserId) + .to(Users::Table, Users::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_users_groups_group_id_to_groups_id") + .from(UsersGroups::Table, UsersGroups::GroupId) + .to(Groups::Table, Groups::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + // Create the `groups_permissions` table. + manager + .create_table( + Table::create() + .if_not_exists() + .table(GroupsPermissions::Table) + .col(pk_auto(GroupsPermissions::Id).not_null()) + .col(integer(GroupsPermissions::GroupId).not_null()) + .col(integer(GroupsPermissions::PermissionId).not_null()) + .foreign_key( + ForeignKey::create() + .name("fk_groups_permissions_group_id_to_groups_id") + .from(GroupsPermissions::Table, GroupsPermissions::GroupId) + .to(Groups::Table, Groups::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_groups_permissions_permission_id_to_permissions_id") + .from(GroupsPermissions::Table, GroupsPermissions::PermissionId) + .to(Permissions::Table, Permissions::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + // create user running network configs table + manager + .create_table( + Table::create() + .if_not_exists() + .table(UserRunningNetworkConfigs::Table) + .col(pk_auto(UserRunningNetworkConfigs::Id).not_null()) + .col(integer(UserRunningNetworkConfigs::UserId).not_null()) + .col( + text(UserRunningNetworkConfigs::NetworkInstanceId) + .unique_key() + .not_null(), + ) + .col(text(UserRunningNetworkConfigs::NetworkConfig).not_null()) + .col( + boolean(UserRunningNetworkConfigs::Disabled) + .not_null() + .default(false), + ) + .col(timestamp_with_time_zone(UserRunningNetworkConfigs::CreateTime).not_null()) + .col(timestamp_with_time_zone(UserRunningNetworkConfigs::UpdateTime).not_null()) + .foreign_key( + ForeignKey::create() + .name("fk_user_running_network_configs_user_id_to_users_id") + .from( + UserRunningNetworkConfigs::Table, + UserRunningNetworkConfigs::UserId, + ) + .to(Users::Table, Users::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + manager + .create_index( + Index::create() + .name("idx_user_running_network_configs_user_id") + .table(UserRunningNetworkConfigs::Table) + .col(UserRunningNetworkConfigs::UserId) + .to_owned(), + ) + .await?; + + // prepare data + let user = Query::insert() + .into_table(Users::Table) + .columns(vec![Users::Username, Users::Password]) + .values_panic(vec![ + "user".into(), + "$argon2i$v=19$m=16,t=2,p=1$dHJ5dXZkYmZkYXM$UkrNqWz0BbSVBq4ykLSuJw".into(), + ]) + .to_owned(); + manager.exec_stmt(user).await?; + + let admin = Query::insert() + .into_table(Users::Table) + .columns(vec![Users::Username, Users::Password]) + .values_panic(vec![ + "admin".into(), + "$argon2i$v=19$m=16,t=2,p=1$Ymd1Y2FlcnQ$x0q4oZinW9S1ZB9BcaHEpQ".into(), + ]) + .to_owned(); + manager.exec_stmt(admin).await?; + + let users = Query::insert() + .into_table(Groups::Table) + .columns(vec![Groups::Name]) + .values_panic(vec!["users".into()]) + .to_owned(); + manager.exec_stmt(users).await?; + + let admins = Query::insert() + .into_table(Groups::Table) + .columns(vec![Groups::Name]) + .values_panic(vec!["admins".into()]) + .to_owned(); + manager.exec_stmt(admins).await?; + + let sessions = Query::insert() + .into_table(Permissions::Table) + .columns(vec![Permissions::Name]) + .values_panic(vec!["sessions".into()]) + .to_owned(); + manager.exec_stmt(sessions).await?; + + let devices = Query::insert() + .into_table(Permissions::Table) + .columns(vec![Permissions::Name]) + .values_panic(vec!["devices".into()]) + .to_owned(); + manager.exec_stmt(devices).await?; + + let users_devices = Query::insert() + .into_table(GroupsPermissions::Table) + .columns(vec![ + GroupsPermissions::GroupId, + GroupsPermissions::PermissionId, + ]) + .select_from( + Query::select() + .column((Groups::Table, Groups::Id)) + .column((Permissions::Table, Permissions::Id)) + .from(Groups::Table) + .full_outer_join(Permissions::Table, all![]) + .cond_where(any![ + // users have devices permission + Expr::col((Groups::Table, Groups::Name)) + .eq("users") + .and(Expr::col((Permissions::Table, Permissions::Name)).eq("devices")), + // admins have all permissions + Expr::col((Groups::Table, Groups::Name)).eq("admins"), + ]) + .to_owned(), + ) + .unwrap() + .to_owned(); + manager.exec_stmt(users_devices).await?; + + let add_user_to_users = Query::insert() + .into_table(UsersGroups::Table) + .columns(vec![UsersGroups::UserId, UsersGroups::GroupId]) + .select_from( + Query::select() + .column((Users::Table, Users::Id)) + .column((Groups::Table, Groups::Id)) + .from(Users::Table) + .full_outer_join(Groups::Table, all![]) + .cond_where( + Expr::col(Users::Username) + .eq("user") + .and(Expr::col(Groups::Name).eq("users")), + ) + .to_owned(), + ) + .unwrap() + .to_owned(); + manager.exec_stmt(add_user_to_users).await?; + + let add_admin_to_admins = Query::insert() + .into_table(UsersGroups::Table) + .columns(vec![UsersGroups::UserId, UsersGroups::GroupId]) + .select_from( + Query::select() + .column((Users::Table, Users::Id)) + .column((Groups::Table, Groups::Id)) + .from(Users::Table) + .full_outer_join(Groups::Table, all![]) + .cond_where( + Expr::col(Users::Username) + .eq("admin") + .and(Expr::col(Groups::Name).eq("admins")), + ) + .to_owned(), + ) + .unwrap() + .to_owned(); + manager.exec_stmt(add_admin_to_admins).await?; + + Ok(()) + } + + // Define how to rollback this migration: Drop the Bakery table. + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Users::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(Groups::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(Permissions::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(UsersGroups::Table).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(GroupsPermissions::Table).to_owned()) + .await?; + Ok(()) + } +} diff --git a/easytier-web/src/migrator/mod.rs b/easytier-web/src/migrator/mod.rs new file mode 100644 index 0000000..652f473 --- /dev/null +++ b/easytier-web/src/migrator/mod.rs @@ -0,0 +1,12 @@ +use sea_orm_migration::prelude::*; + +mod m20241029_000001_init; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(m20241029_000001_init::Migration)] + } +} diff --git a/easytier-web/src/restful/auth.rs b/easytier-web/src/restful/auth.rs new file mode 100644 index 0000000..23068b7 --- /dev/null +++ b/easytier-web/src/restful/auth.rs @@ -0,0 +1,171 @@ +use axum::{ + http::StatusCode, + routing::{get, post, put}, + Router, +}; +use axum_login::login_required; +use axum_messages::Message; +use serde::{Deserialize, Serialize}; + +use crate::restful::users::Backend; + +use super::{ + users::{AuthSession, Credentials}, + AppStateInner, +}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct LoginResult { + messages: Vec, +} + +pub fn router() -> Router { + let r = Router::new() + .route("/api/v1/auth/password", put(self::put::change_password)) + .route_layer(login_required!(Backend)); + Router::new() + .merge(r) + .route("/api/v1/auth/login", post(self::post::login)) + .route("/api/v1/auth/logout", get(self::get::logout)) + .route("/api/v1/auth/captcha", get(self::get::get_captcha)) + .route("/api/v1/auth/register", post(self::post::register)) +} + +mod put { + use axum::Json; + use axum_login::AuthUser; + use easytier::proto::common::Void; + + use crate::restful::{other_error, users::ChangePassword, HttpHandleError}; + + use super::*; + + pub async fn change_password( + mut auth_session: AuthSession, + Json(req): Json, + ) -> Result, HttpHandleError> { + if let Err(e) = auth_session + .backend + .change_password(auth_session.user.as_ref().unwrap().id(), &req) + .await + { + tracing::error!("Failed to change password: {:?}", e); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json::from(other_error(format!("{:?}", e))), + )); + } + + let _ = auth_session.logout().await; + + Ok(Void::default().into()) + } +} + +mod post { + use axum::Json; + use easytier::proto::common::Void; + + use crate::restful::{ + captcha::extension::{axum_tower_sessions::CaptchaAxumTowerSessionStaticExt, CaptchaUtil}, + other_error, + users::RegisterNewUser, + HttpHandleError, + }; + + use super::*; + + pub async fn login( + mut auth_session: AuthSession, + Json(creds): Json, + ) -> Result, HttpHandleError> { + let user = match auth_session.authenticate(creds.clone()).await { + Ok(Some(user)) => user, + Ok(None) => { + return Err(( + StatusCode::UNAUTHORIZED, + Json::from(other_error("Invalid credentials")), + )); + } + Err(e) => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json::from(other_error(format!("{:?}", e))), + )) + } + }; + + if let Err(e) = auth_session.login(&user).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json::from(other_error(format!("{:?}", e))), + )); + } + + Ok(Void::default().into()) + } + + pub async fn register( + auth_session: AuthSession, + captcha_session: tower_sessions::Session, + Json(req): Json, + ) -> Result, HttpHandleError> { + // 调用CaptchaUtil的静态方法验证验证码是否正确 + if !CaptchaUtil::ver(&req.captcha, &captcha_session).await { + return Err(( + StatusCode::BAD_REQUEST, + other_error(format!("captcha verify error, input: {}", req.captcha)).into(), + )); + } + + if let Err(e) = auth_session.backend.register_new_user(&req).await { + tracing::error!("Failed to register new user: {:?}", e); + return Err(( + StatusCode::BAD_REQUEST, + other_error(format!("{:?}", e)).into(), + )); + } + + Ok(Void::default().into()) + } +} + +mod get { + use crate::restful::{ + captcha::{ + captcha::spec::SpecCaptcha, + extension::{axum_tower_sessions::CaptchaAxumTowerSessionExt as _, CaptchaUtil}, + NewCaptcha as _, + }, + other_error, HttpHandleError, + }; + use axum::{response::Response, Json}; + use easytier::proto::common::Void; + use tower_sessions::Session; + + use super::*; + + pub async fn logout(mut auth_session: AuthSession) -> Result, HttpHandleError> { + match auth_session.logout().await { + Ok(_) => Ok(Json(Void::default())), + Err(e) => { + tracing::error!("Failed to logout: {:?}", e); + Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json::from(other_error(format!("{:?}", e))), + )) + } + } + } + + pub async fn get_captcha(session: Session) -> Result { + let mut captcha: CaptchaUtil = CaptchaUtil::with_size_and_len(127, 48, 4); + match captcha.out(&session).await { + Ok(response) => Ok(response), + Err(e) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json::from(other_error(format!("{:?}", e))), + )), + } + } +} diff --git a/easytier-web/src/restful/captcha/base/captcha.rs b/easytier-web/src/restful/captcha/base/captcha.rs new file mode 100644 index 0000000..c7e45c8 --- /dev/null +++ b/easytier-web/src/restful/captcha/base/captcha.rs @@ -0,0 +1,308 @@ +use super::super::base::randoms::Randoms; + +use super::super::utils::color::Color; +use super::super::utils::font; +use base64::prelude::BASE64_STANDARD; +use base64::Engine; + +use rusttype::Font; +use std::fmt::Debug; +use std::io::Write; +use std::sync::Arc; + +/// 验证码抽象类 +pub(crate) struct Captcha { + /// 随机数工具类 + pub(crate) randoms: Randoms, + + /// 常用颜色 + color: Vec, + + /// 字体名称 + font_names: [&'static str; 1], + + /// 验证码的字体 + font_name: String, + + /// 验证码的字体大小 + font_size: f32, + + /// 验证码随机字符长度 + pub len: usize, + + /// 验证码显示宽度 + pub width: i32, + + /// 验证码显示高度 + pub height: i32, + + /// 验证码类型 + char_type: CaptchaType, + + /// 当前验证码 + pub(crate) chars: Option, +} + +/// 验证码文本类型 The character type of the captcha +pub enum CaptchaType { + /// 字母数字混合 + TypeDefault = 1, + + /// 纯数字 + TypeOnlyNumber, + + /// 纯字母 + TypeOnlyChar, + + /// 纯大写字母 + TypeOnlyUpper, + + /// 纯小写字母 + TypeOnlyLower, + + /// 数字大写字母 + TypeNumAndUpper, +} + +/// 内置字体 Fonts shipped with the library +pub enum CaptchaFont { + /// actionj + Font1, + /// epilog + Font2, + /// fresnel + Font3, + /// headache + Font4, + /// lexo + Font5, + /// prefix + Font6, + /// progbot + Font7, + /// ransom + Font8, + /// robot + Font9, + /// scandal + Font10, +} + +impl Captcha { + /// 生成随机验证码 + pub fn alphas(&mut self) -> Vec { + let mut cs = vec!['\0'; self.len]; + for i in 0..self.len { + match self.char_type { + CaptchaType::TypeDefault => cs[i] = self.randoms.alpha(), + CaptchaType::TypeOnlyNumber => { + cs[i] = self.randoms.alpha_under(self.randoms.num_max_index) + } + CaptchaType::TypeOnlyChar => { + cs[i] = self + .randoms + .alpha_between(self.randoms.char_min_index, self.randoms.char_max_index) + } + CaptchaType::TypeOnlyUpper => { + cs[i] = self + .randoms + .alpha_between(self.randoms.upper_min_index, self.randoms.upper_max_index) + } + CaptchaType::TypeOnlyLower => { + cs[i] = self + .randoms + .alpha_between(self.randoms.lower_min_index, self.randoms.lower_max_index) + } + CaptchaType::TypeNumAndUpper => { + cs[i] = self.randoms.alpha_under(self.randoms.upper_max_index) + } + } + } + + self.chars = Some(cs.iter().collect()); + cs + } + + /// 获取当前的验证码 + pub fn text(&mut self) -> String { + self.check_alpha(); + self.chars.clone().unwrap() + } + + /// 获取当前验证码的字符数组 + pub fn text_char(&mut self) -> Vec { + self.check_alpha(); + self.chars.clone().unwrap().chars().collect() + } + + /// 检查验证码是否生成,没有则立即生成 + pub fn check_alpha(&mut self) { + if self.chars.is_none() { + self.alphas(); + } + } + + pub fn get_font(&mut self) -> Arc { + if let Some(font) = font::get_font(&self.font_name) { + font + } else { + font::get_font(self.font_names[0]).unwrap() + } + } + + pub fn get_font_size(&mut self) -> f32 { + self.font_size + } + + pub fn set_font_by_enum(&mut self, font: CaptchaFont, size: Option) { + let font_name = self.font_names[font as usize]; + self.font_name = font_name.into(); + self.font_size = size.unwrap_or(32.); + } +} + +/// 初始化验证码的抽象方法 Traits for initialize a Captcha instance. +pub trait NewCaptcha +where + Self: Sized, +{ + /// 用默认参数初始化 + /// + /// Initialize the Captcha with the default properties. + fn new() -> Self; + + /// 使用输出图像大小初始化 + /// + /// Initialize the Captcha with the size of output image. + fn with_size(width: i32, height: i32) -> Self; + + /// 使用输出图像大小和验证码字符长度初始化 + /// + /// Initialize the Captcha with the size of output image and the character length of the Captcha. + /// + ///
+ /// + /// 特别地/In particular: + /// + /// - 对算术验证码[ArithmeticCaptcha](crate::captcha::arithmetic::ArithmeticCaptcha)而言,这里的`len`是验证码中数字的数量。 + /// For [ArithmeticCaptcha](crate::captcha::arithmetic::ArithmeticCaptcha), the `len` presents the count of the digits + /// in the Captcha. + fn with_size_and_len(width: i32, height: i32, len: usize) -> Self; + + /// 使用完整的参数来初始化,包括输出图像大小、验证码字符长度和输出字体及其大小 + /// + /// Initialize the Captcha with full properties, including the size of output image, the character length of the Captcha, + /// and the font used in Captcha with the font size. + /// + /// 关于`len`字段的注意事项,请参见[with_size_and_len](Self::with_size_and_len)中的说明。Refer to the document of + /// [with_size_and_len](Self::with_size_and_len) for the precautions of the `len` property. + fn with_all(width: i32, height: i32, len: usize, font: CaptchaFont, font_size: f32) -> Self; +} + +impl NewCaptcha for Captcha { + fn new() -> Self { + let color = [ + (0, 135, 255), + (51, 153, 51), + (255, 102, 102), + (255, 153, 0), + (153, 102, 0), + (153, 102, 153), + (51, 153, 153), + (102, 102, 255), + (0, 102, 204), + (204, 51, 51), + (0, 153, 204), + (0, 51, 102), + ] + .iter() + .map(|v| (*v).into()) + .collect(); + + let font_names = ["robot.ttf"]; + + let font_name = font_names[0].into(); + let font_size = 32.; + let len = 5; + let width = 130; + let height = 48; + let char_type = CaptchaType::TypeDefault; + let chars = None; + + Self { + randoms: Randoms::new(), + color, + font_names, + font_name, + font_size, + len, + width, + height, + char_type, + chars, + } + } + + fn with_size(width: i32, height: i32) -> Self { + let mut _self = Self::new(); + _self.width = width; + _self.height = height; + _self + } + + fn with_size_and_len(width: i32, height: i32, len: usize) -> Self { + let mut _self = Self::new(); + _self.width = width; + _self.height = height; + _self.len = len; + _self + } + + fn with_all(width: i32, height: i32, len: usize, font: CaptchaFont, font_size: f32) -> Self { + let mut _self = Self::new(); + _self.width = width; + _self.height = height; + _self.len = len; + _self.set_font_by_enum(font, None); + _self.font_size = font_size; + _self + } +} + +/// 验证码的抽象方法 Traits which a Captcha must implements. +pub trait AbstractCaptcha: NewCaptcha { + /// 错误类型 + type Error: std::error::Error + Debug + Send + Sync + 'static; + + /// 输出验证码到指定位置 + /// + /// Write the Captcha image to the specified place. + fn out(&mut self, out: impl Write) -> Result<(), Self::Error>; + + /// 获取验证码中的字符(即正确答案) + /// + /// Get the characters (i.e. the correct answer) of the Captcha + fn get_chars(&mut self) -> Vec; + + /// 输出Base64编码。注意,返回值会带编码头(例如`data:image/png;base64,`),可以直接在浏览器中显示;如不需要编码头, + /// 请使用[base64_with_head](Self::base64_with_head)方法并传入空参数以去除编码头。 + /// + /// Get the Base64 encoded image. Reminds: the returned Base64 strings will begin with an encoding head like + /// `data:image/png;base64,`, which make it possible to display in browsers directly. If you don't need it, you may + /// use [base64_with_head](Self::base64_with_head) and pass a null string. + fn base64(&mut self) -> Result; + + /// 获取验证码的MIME类型 + /// + /// Get the MIME Content type of the Captcha. + fn get_content_type(&mut self) -> String; + + /// 输出Base64编码(指定编码头) + /// + /// Get the Base64 encoded image, with specified encoding head. + fn base64_with_head(&mut self, head: &str) -> Result { + let mut output_stream = Vec::new(); + self.out(&mut output_stream)?; + Ok(String::from(head) + &BASE64_STANDARD.encode(&output_stream)) + } +} diff --git a/easytier-web/src/restful/captcha/base/mod.rs b/easytier-web/src/restful/captcha/base/mod.rs new file mode 100644 index 0000000..f0eb9a8 --- /dev/null +++ b/easytier-web/src/restful/captcha/base/mod.rs @@ -0,0 +1,4 @@ +//! Base traits + +pub(crate) mod captcha; +pub(crate) mod randoms; diff --git a/easytier-web/src/restful/captcha/base/randoms.rs b/easytier-web/src/restful/captcha/base/randoms.rs new file mode 100644 index 0000000..ed78579 --- /dev/null +++ b/easytier-web/src/restful/captcha/base/randoms.rs @@ -0,0 +1,86 @@ + +use rand::{random}; + + +/// 随机数工具类 +pub(crate) struct Randoms { + /// 定义验证码字符.去除了0、O、I、L等容易混淆的字母 + pub alpha: [char; 54], + + /// 数字的最大索引,不包括最大值 + pub num_max_index: usize, + + /// 字符的最小索引,包括最小值 + pub char_min_index: usize, + + /// 字符的最大索引,不包括最大值 + pub char_max_index: usize, + + /// 大写字符最小索引 + pub upper_min_index: usize, + + /// 大写字符最大索引 + pub upper_max_index: usize, + + /// 小写字母最小索引 + pub lower_min_index: usize, + + /// 小写字母最大索引 + pub lower_max_index: usize, +} + +impl Randoms { + pub fn new() -> Self { + // Defines the Captcha characters, removing characters like 0, O, I, l, etc. + let alpha = [ + '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', + 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', + 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', + 'x', 'y', 'z', + ]; + + let num_max_index = 8; + let char_min_index = num_max_index; + let char_max_index = alpha.len(); + let upper_min_index = char_min_index; + let upper_max_index = upper_min_index + 23; + let lower_min_index = upper_max_index; + let lower_max_index = char_max_index; + + Self { + alpha, + num_max_index, + char_min_index, + char_max_index, + upper_min_index, + upper_max_index, + lower_min_index, + lower_max_index, + } + } + + /// 产生两个数之间的随机数 + pub fn num_between(&mut self, min: i32, max: i32) -> i32 { + min + (random::() % (max - min) as usize) as i32 + } + + /// 产生0-num的随机数,不包括num + pub fn num(&mut self, num: usize) -> usize { + random::() % num + } + + /// 返回ALPHA中的随机字符 + pub fn alpha(&mut self) -> char { + self.alpha[self.num(self.alpha.len())] + } + + /// 返回ALPHA中第0位到第num位的随机字符 + pub fn alpha_under(&mut self, num: usize) -> char { + self.alpha[self.num(num)] + } + + /// 返回ALPHA中第min位到第max位的随机字符 + pub fn alpha_between(&mut self, min: usize, max: usize) -> char { + self.alpha[self.num_between(min as i32, max as i32) as usize] + } +} diff --git a/easytier-web/src/restful/captcha/captcha/mod.rs b/easytier-web/src/restful/captcha/captcha/mod.rs new file mode 100644 index 0000000..cc05df3 --- /dev/null +++ b/easytier-web/src/restful/captcha/captcha/mod.rs @@ -0,0 +1 @@ +pub mod spec; diff --git a/easytier-web/src/restful/captcha/captcha/spec.rs b/easytier-web/src/restful/captcha/captcha/spec.rs new file mode 100644 index 0000000..c58d01f --- /dev/null +++ b/easytier-web/src/restful/captcha/captcha/spec.rs @@ -0,0 +1,318 @@ +//! Static alphabetical PNG Captcha +//! +//! PNG格式验证码 +//! + +use super::super::base::captcha::{AbstractCaptcha, Captcha}; + +use super::super::{CaptchaFont, NewCaptcha}; + +use image::{ImageBuffer, Rgba}; +use imageproc::drawing; +use rand::{rngs::ThreadRng, Rng}; +use rusttype::{Font, Scale}; +use std::io::{Cursor, Write}; +use std::sync::Arc; + +mod color { + use image::Rgba; + use rand::{rngs::ThreadRng, Rng}; + pub fn gen_background_color(rng: &mut ThreadRng) -> Rgba { + let red = rng.gen_range(200..=255); + let green = rng.gen_range(200..=255); + let blue = rng.gen_range(200..=255); + //let a=rng.gen_range(0..255); + Rgba([red, green, blue, 255]) + } + pub fn gen_text_color(rng: &mut ThreadRng) -> Rgba { + let red = rng.gen_range(0..=150); + let green = rng.gen_range(0..=150); + let blue = rng.gen_range(0..=150); + Rgba([red, green, blue, 255]) + } + + pub fn gen_line_color(rng: &mut ThreadRng) -> Rgba { + let red = rng.gen_range(100..=255); + let green = rng.gen_range(100..=255); + let blue = rng.gen_range(100..=255); + Rgba([red, green, blue, 255]) + } +} + +///the builder of captcha +pub struct CaptchaBuilder<'a, 'b> { + ///captcha image width + pub width: u32, + ///captcha image height + pub height: u32, + + ///random string length. + pub length: u32, + + ///source is a unicode which is the rand string from. + pub source: String, + + ///image background color (optional) + pub background_color: Option>, + ///fonts collection for text + pub fonts: &'b [Arc>], + ///The maximum number of lines to draw behind of the image + pub max_behind_lines: Option, + ///The maximum number of lines to draw in front of the image + pub max_front_lines: Option, + ///The maximum number of ellipse lines to draw in front of the image + pub max_ellipse_lines: Option, +} + +impl<'a, 'b> Default for CaptchaBuilder<'a, 'b> { + fn default() -> Self { + Self { + width: 150, + height: 40, + length: 5, + source: String::from("1234567890qwertyuioplkjhgfdsazxcvbnm"), + background_color: None, + fonts: &[], + max_behind_lines: None, + max_front_lines: None, + max_ellipse_lines: None, + } + } +} + +impl<'a, 'b> CaptchaBuilder<'a, 'b> { + fn write_phrase( + &self, + image: &mut ImageBuffer, Vec>, + rng: &mut ThreadRng, + phrase: &str, + ) { + //println!("phrase={}", phrase); + //println!("width={}, height={}", self.width, self.height); + let font_size = (self.width as f32) / (self.length as f32) - rng.gen_range(1.0..=4.0); + let scale = Scale::uniform(font_size); + if self.fonts.is_empty() { + panic!("no fonts loaded"); + } + let font_index = rng.gen_range(0..self.fonts.len()); + let font = &self.fonts[font_index]; + let glyphs: Vec<_> = font + .layout(phrase, scale, rusttype::point(0.0, 0.0)) + .collect(); + let text_height = { + let v_metrics = font.v_metrics(scale); + (v_metrics.ascent - v_metrics.descent).ceil() as u32 + }; + let text_width = { + let min_x = glyphs.first().unwrap().pixel_bounding_box().unwrap().min.x; + let max_x = glyphs.last().unwrap().pixel_bounding_box().unwrap().max.x; + let last_x_pos = glyphs.last().unwrap().position().x as i32; + (max_x + last_x_pos - min_x) as u32 + }; + let node_width = text_width / self.length; + //println!("text_width={}, text_height={}", text_width, text_height); + let mut x = ((self.width as i32) - (text_width as i32)) / 2; + let y = ((self.height as i32) - (text_height as i32)) / 2; + // + for s in phrase.chars() { + let text_color = color::gen_text_color(rng); + let offset = rng.gen_range(-5..=5); + //println!("x={}, y={}", x, y); + drawing::draw_text_mut( + image, + text_color, + x, + y + offset, + scale, + font, + &s.to_string(), + ); + x += node_width as i32; + } + } + + fn draw_line(&self, image: &mut ImageBuffer, Vec>, rng: &mut ThreadRng) { + let line_color = color::gen_line_color(rng); + let is_h = rng.gen(); + let (start, end) = if is_h { + let xa = rng.gen_range(0.0..(self.width as f32) / 2.0); + let ya = rng.gen_range(0.0..(self.height as f32)); + let xb = rng.gen_range((self.width as f32) / 2.0..(self.width as f32)); + let yb = rng.gen_range(0.0..(self.height as f32)); + ((xa, ya), (xb, yb)) + } else { + let xa = rng.gen_range(0.0..(self.width as f32)); + let ya = rng.gen_range(0.0..(self.height as f32) / 2.0); + let xb = rng.gen_range(0.0..(self.width as f32)); + let yb = rng.gen_range((self.height as f32) / 2.0..(self.height as f32)); + ((xa, ya), (xb, yb)) + }; + let thickness = rng.gen_range(2..4); + for i in 0..thickness { + let offset = i as f32; + if is_h { + drawing::draw_line_segment_mut( + image, + (start.0, start.1 + offset), + (end.0, end.1 + offset), + line_color, + ); + } else { + drawing::draw_line_segment_mut( + image, + (start.0 + offset, start.1), + (end.0 + offset, end.1), + line_color, + ); + } + } + } + + fn draw_ellipse(&self, image: &mut ImageBuffer, Vec>, rng: &mut ThreadRng) { + let line_color = color::gen_line_color(rng); + let thickness = rng.gen_range(2..4); + for i in 0..thickness { + let center = ( + rng.gen_range(-(self.width as i32) / 4..(self.width as i32) * 5 / 4), + rng.gen_range(-(self.height as i32) / 4..(self.height as i32) * 5 / 4), + ); + drawing::draw_hollow_ellipse_mut( + image, + (center.0, center.1 + i), + (self.width * 6 / 7) as i32, + (self.height * 5 / 8) as i32, + line_color, + ); + } + } + + fn build_image(&self, phrase: String) -> ImageBuffer, Vec> { + let mut rng = rand::thread_rng(); + let bgc = match self.background_color { + Some(v) => v, + None => color::gen_background_color(&mut rng), + }; + let mut image = ImageBuffer::from_fn(self.width, self.height, |_, _| bgc); + //draw behind line + let square = self.width * self.height; + let effects = match self.max_behind_lines { + Some(s) => { + if s > 0 { + rng.gen_range(square / 3000..square / 2000).min(s) + } else { + 0 + } + } + None => rng.gen_range(square / 3000..square / 2000), + }; + for _ in 0..effects { + self.draw_line(&mut image, &mut rng); + } + //write phrase + self.write_phrase(&mut image, &mut rng, &phrase); + //draw front line + let effects = match self.max_front_lines { + Some(s) => { + if s > 0 { + rng.gen_range(square / 3000..=square / 2000).min(s) + } else { + 0 + } + } + None => rng.gen_range(square / 3000..=square / 2000), + }; + for _ in 0..effects { + self.draw_line(&mut image, &mut rng); + } + //draw ellipse + let effects = match self.max_front_lines { + Some(s) => { + if s > 0 { + rng.gen_range(square / 4000..=square / 3000).min(s) + } else { + 0 + } + } + None => rng.gen_range(square / 4000..=square / 3000), + }; + for _ in 0..effects { + self.draw_ellipse(&mut image, &mut rng); + } + + image + } +} + +/// PNG格式验证码 +pub struct SpecCaptcha { + pub(crate) captcha: Captcha, +} + +impl NewCaptcha for SpecCaptcha { + fn new() -> Self { + Self { + captcha: Captcha::new(), + } + } + + fn with_size(width: i32, height: i32) -> Self { + Self { + captcha: Captcha::with_size(width, height), + } + } + + fn with_size_and_len(width: i32, height: i32, len: usize) -> Self { + Self { + captcha: Captcha::with_size_and_len(width, height, len), + } + } + + fn with_all(width: i32, height: i32, len: usize, font: CaptchaFont, font_size: f32) -> Self { + Self { + captcha: Captcha::with_all(width, height, len, font, font_size), + } + } +} + +impl AbstractCaptcha for SpecCaptcha { + type Error = image::ImageError; + + fn out(&mut self, mut out: impl Write) -> Result<(), Self::Error> { + let phrase = self.captcha.text_char(); + let builder = CaptchaBuilder { + width: self.captcha.width as u32, + height: self.captcha.height as u32, + length: self.captcha.len as u32, + background_color: None, + fonts: &[self.captcha.get_font()], + max_behind_lines: Some(0), + max_front_lines: Some(0), + max_ellipse_lines: Some(0), + ..Default::default() + }; + let image = builder.build_image(phrase.iter().collect()); + let format = image::ImageOutputFormat::Png; + let mut raw_data: Vec = Vec::new(); + image.write_to(&mut Cursor::new(&mut raw_data), format)?; + out.write_all(&raw_data)?; + Ok(()) + } + + fn get_chars(&mut self) -> Vec { + self.captcha.text_char() + } + + fn base64(&mut self) -> Result { + self.base64_with_head("data:image/png;base64,") + } + + fn get_content_type(&mut self) -> String { + "image/png".into() + } +} + +#[cfg(test)] +mod test { + #[test] + fn it_works() {} +} diff --git a/easytier-web/src/restful/captcha/extension/axum_tower_sessions.rs b/easytier-web/src/restful/captcha/extension/axum_tower_sessions.rs new file mode 100644 index 0000000..81597db --- /dev/null +++ b/easytier-web/src/restful/captcha/extension/axum_tower_sessions.rs @@ -0,0 +1,69 @@ +//! Axum & Tower_sessions 组合 +//! +//! - Axum: [axum](https://docs.rs/axum) +//! - Tower Sessions: [axum](https://docs.rs/tower-sessions) + +use super::AbstractCaptcha; +use super::CaptchaUtil; +use async_trait::async_trait; +use axum::response::Response; +use std::fmt::Debug; +use tower_sessions::Session; + +const CAPTCHA_KEY: &'static str = "ez-captcha"; + +/// Axum & Tower_Sessions +#[async_trait] +pub trait CaptchaAxumTowerSessionExt { + /// 错误类型 + type Error: Debug + Send + Sync + 'static; + + /// 将验证码图片写入响应,并将用户的验证码信息保存至Session中 + /// + /// Write the Captcha Image into the response and save the Captcha information into the user's Session. + async fn out(&mut self, session: &Session) -> Result; +} + +/// Axum & Tower_Sessions - 静态方法 +#[async_trait] +pub trait CaptchaAxumTowerSessionStaticExt { + /// 验证验证码,返回的布尔值代表验证码是否正确 + /// + /// Verify the Captcha code, and return whether user's code is correct. + async fn ver(code: &str, session: &Session) -> bool { + match session.get::(CAPTCHA_KEY).await { + Ok(Some(ans)) => ans.to_ascii_lowercase() == code.to_ascii_lowercase(), + _ => false, + } + } + + /// 清除Session中的验证码 + /// + /// Clear the Captcha in the session. + async fn clear(session: &Session) { + if session.remove::(CAPTCHA_KEY).await.is_err() { + tracing::warn!("Exception occurs during clearing the session.") + } + } +} + +#[async_trait] +impl CaptchaAxumTowerSessionExt for CaptchaUtil { + type Error = anyhow::Error; + + async fn out(&mut self, session: &Session) -> Result { + let mut data = vec![]; + self.captcha_instance.out(&mut data)?; + + let ans: String = self.captcha_instance.get_chars().iter().collect(); + session.insert(CAPTCHA_KEY, ans).await?; + + let resp = Response::builder() + .header("Content-Type", self.captcha_instance.get_content_type()) + .body(data.into())?; + Ok(resp) + } +} + +#[async_trait] +impl CaptchaAxumTowerSessionStaticExt for CaptchaUtil {} diff --git a/easytier-web/src/restful/captcha/extension/mod.rs b/easytier-web/src/restful/captcha/extension/mod.rs new file mode 100644 index 0000000..c11a976 --- /dev/null +++ b/easytier-web/src/restful/captcha/extension/mod.rs @@ -0,0 +1,41 @@ +pub mod axum_tower_sessions; + +use super::base::captcha::AbstractCaptcha; +use super::captcha::spec::SpecCaptcha; +use super::{CaptchaFont, NewCaptcha}; + +/// 验证码工具类 - Captcha Utils +/// +/// 默认使用[SpecCaptcha](静态PNG字母验证码)作为验证码实现,用户也可以指定其他实现了[AbstractCaptcha]的类型。 +/// +/// Use [SpecCaptcha] (static PNG-format alphabetical Captcha) as the default implement of the Captcha service. Users may use other implementation of [AbstractCaptcha] they prefer. +/// +pub struct CaptchaUtil { + captcha_instance: T, +} + +impl NewCaptcha for CaptchaUtil { + fn new() -> Self { + Self { + captcha_instance: T::new(), + } + } + + fn with_size(width: i32, height: i32) -> Self { + Self { + captcha_instance: T::with_size(width, height), + } + } + + fn with_size_and_len(width: i32, height: i32, len: usize) -> Self { + Self { + captcha_instance: T::with_size_and_len(width, height, len), + } + } + + fn with_all(width: i32, height: i32, len: usize, font: CaptchaFont, font_size: f32) -> Self { + Self { + captcha_instance: T::with_all(width, height, len, font, font_size), + } + } +} diff --git a/easytier-web/src/restful/captcha/mod.rs b/easytier-web/src/restful/captcha/mod.rs new file mode 100644 index 0000000..6f504e4 --- /dev/null +++ b/easytier-web/src/restful/captcha/mod.rs @@ -0,0 +1,134 @@ +//! Rust图形验证码,由Java同名开源库[whvcse/EasyCaptcha](https://github.com/ele-admin/EasyCaptcha)移植而来👏,100%纯Rust实现,支持gif、算术等类型。 +//! +//! Rust Captcha library, which is ported from Java's same-name library [whvcse/EasyCaptcha](https://github.com/ele-admin/EasyCaptcha), +//! implemented in 100% pure Rust, supporting GIF and arithmetic problems. +//! +//!
+//! +//! 目前已适配框架 / Frameworks which is adapted now: +//! +//! - `axum` + `tower-sessions` +//! +//! 更多框架欢迎您提交PR,参与适配🙏 PR for new frameworks are welcomed +//! +//!
+//! +//! ## 安装 Install +//! +//! 请参考Github README为Linux系统安装依赖。 +//! +//! If you are compiling this project in linux, please refer to README in repository to install +//! dependencies into you system. +//! +//! ## 使用 Usage +//! +//! 若您正在使用的框架已适配,您可直接通过[CaptchaUtil](extension::CaptchaUtil)类(并导入相应框架的trait)来使用验证码: +//! +//! If your framework is adapted, you can just use [CaptchaUtil](extension::CaptchaUtil) and importing traits of your +//! framework to use the Captcha: +//! +//! ``` +//! use std::collections::HashMap; +//! use axum::extract::Query; +//! use axum::response::IntoResponse; +//! use easy_captcha::captcha::gif::GifCaptcha; +//! use easy_captcha::extension::axum_tower_sessions::{ +//! CaptchaAxumTowerSessionExt, CaptchaAxumTowerSessionStaticExt, +//! }; +//! use easy_captcha::extension::CaptchaUtil; +//! use easy_captcha::NewCaptcha; +//! +//! /// 接口:获取验证码 +//! /// Handler: Get a captcha +//! async fn get_captcha(session: tower_sessions::Session) -> Result { +//! let mut captcha: CaptchaUtil = CaptchaUtil::new(); +//! match captcha.out(&session).await { +//! Ok(response) => Ok(response), +//! Err(_) => Err(axum::http::StatusCode::INTERNAL_SERVER_ERROR), +//! } +//! } +//! +//! /// 接口:验证验证码 +//! /// Handler: Verify captcha codes +//! async fn verify_captcha( +//! session: tower_sessions::Session, +//! Query(query): Query>, +//! ) -> axum::response::Response { +//! // 从请求中获取验证码 Getting code from the request. +//! if let Some(code) = query.get("code") { +//! // 调用CaptchaUtil的静态方法验证验证码是否正确 Use a static method in CaptchaUtil to verify. +//! if CaptchaUtil::ver(code, &session).await { +//! CaptchaUtil::clear(&session).await; // 如果愿意的话,你可以从Session中清理掉验证码 You may clear the Captcha from the Session if you want +//! "Your code is valid, thank you.".into_response() +//! } else { +//! "Your code is not valid, I'm sorry.".into_response() +//! } +//! } else { +//! "You didn't provide the code.".into_response() +//! } +//! } +//! ``` +//! +//! 您也可以自定义验证码的各项属性 +//! +//! You can also specify properties of the Captcha. +//! +//! ```rust +//! use easy_captcha::captcha::gif::GifCaptcha; +//! use easy_captcha::extension::axum_tower_sessions::CaptchaAxumTowerSessionExt; +//! use easy_captcha::extension::CaptchaUtil; +//! use easy_captcha::NewCaptcha; +//! +//! async fn get_captcha(session: tower_sessions::Session) -> Result { +//! let mut captcha: CaptchaUtil = CaptchaUtil::with_size_and_len(127, 48, 4); +//! match captcha.out(&session).await { +//! Ok(response) => Ok(response), +//! Err(_) => Err(axum::http::StatusCode::INTERNAL_SERVER_ERROR), +//! } +//! } +//! ``` +//! +//! 项目当前提供了三种验证码实现:[SpecCaptcha](captcha::spec::SpecCaptcha)(静态PNG)、[GifCaptcha](captcha::gif::GifCaptcha)(动态GIF) +//! 、[ArithmeticCaptcha](captcha::arithmetic::ArithmeticCaptcha)(算术PNG),您可按需使用。 +//! +//! There is three implementation of Captcha currently, which are [SpecCaptcha](captcha::spec::SpecCaptcha)(static PNG), +//! [GifCaptcha](captcha::gif::GifCaptcha)(GIF), [ArithmeticCaptcha](captcha::arithmetic::ArithmeticCaptcha)(Arithmetic problems), +//! you can use them according to your need. +//! +//!
+//! +//! 自带字体效果 / Fonts shipped +//! +//! | 字体/Fonts | 效果/Preview | +//! |---------------------|------------------------------------------------| +//! | CaptchaFont::Font1 | ![](https://s2.ax1x.com/2019/08/23/msMe6U.png) | +//! | CaptchaFont::Font2 | ![](https://s2.ax1x.com/2019/08/23/msMAf0.png) | +//! | CaptchaFont::Font3 | ![](https://s2.ax1x.com/2019/08/23/msMCwj.png) | +//! | CaptchaFont::Font4 | ![](https://s2.ax1x.com/2019/08/23/msM9mQ.png) | +//! | CaptchaFont::Font5 | ![](https://s2.ax1x.com/2019/08/23/msKz6S.png) | +//! | CaptchaFont::Font6 | ![](https://s2.ax1x.com/2019/08/23/msKxl8.png) | +//! | CaptchaFont::Font7 | ![](https://s2.ax1x.com/2019/08/23/msMPTs.png) | +//! | CaptchaFont::Font8 | ![](https://s2.ax1x.com/2019/08/23/msMmXF.png) | +//! | CaptchaFont::Font9 | ![](https://s2.ax1x.com/2019/08/23/msMVpV.png) | +//! | CaptchaFont::Font10 | ![](https://s2.ax1x.com/2019/08/23/msMZlT.png) | +//! + +#![warn(missing_docs)] +#![allow(dead_code)] + +pub(crate) mod base; +pub mod captcha; +pub mod extension; +mod utils; + +pub use base::captcha::*; + +// #[cfg(test)] +// mod tests { +// use super::*; +// +// #[test] +// fn it_works() { +// +// } +// } diff --git a/easytier-web/src/restful/captcha/utils/color.rs b/easytier-web/src/restful/captcha/utils/color.rs new file mode 100644 index 0000000..a5345c7 --- /dev/null +++ b/easytier-web/src/restful/captcha/utils/color.rs @@ -0,0 +1,53 @@ +//! RGBA颜色 +use std::fmt::{Debug, Formatter}; + +#[derive(Clone)] +pub struct Color(f64, f64, f64, f64); + +impl Color { + pub fn set_alpha(&mut self, a: f64) { + self.3 = a; + } +} + +impl Debug for Color { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Color") + .field("r", &self.0) + .field("g", &self.1) + .field("b", &self.2) + .field("a", &self.3) + .finish() + } +} + +impl From<(u8, u8, u8)> for Color { + fn from(value: (u8, u8, u8)) -> Self { + Self( + value.0 as f64 / 255.0, + value.1 as f64 / 255.0, + value.2 as f64 / 255.0, + 1.0, + ) + } +} + +impl Into<(u8, u8, u8, u8)> for Color { + fn into(self) -> (u8, u8, u8, u8) { + ( + (self.0 * 255.0) as u8, + (self.1 * 255.0) as u8, + (self.2 * 255.0) as u8, + (self.3 * 255.0) as u8, + ) + } +} + +impl Into for Color { + fn into(self) -> u32 { + let color: (u8, u8, u8, u8) = self.into(); + (color.0 as u32) << 24 + (color.1 as u32) << 16 + (color.2 as u32) << 8 + (color.3 as u32) + } +} + +impl Color {} diff --git a/easytier-web/src/restful/captcha/utils/font.rs b/easytier-web/src/restful/captcha/utils/font.rs new file mode 100644 index 0000000..6bce05c --- /dev/null +++ b/easytier-web/src/restful/captcha/utils/font.rs @@ -0,0 +1,45 @@ +use rust_embed::RustEmbed; +use rusttype::Font; +use std::error::Error; +use std::sync::Arc; + +#[derive(RustEmbed)] +#[folder = "resources/"] +struct FontAssets; + +// lazy_static! { +// pub(crate) static ref FONTS: RwLock>> = Default::default(); +// } + +pub fn get_font(font_name: &str) -> Option> { + // let fonts_cell = FONTS.get_or_init(|| Default::default()); + // let guard = fonts_cell.read(); + // + // if guard.contains_key(font_name) { + // Some(guard.get(font_name).unwrap().clone()) + // } else { + // drop(guard); + + if let Ok(Some(font)) = load_font(font_name) { + // let mut guard = fonts_cell.write(); + let font = Arc::new(font); + // guard.insert(String::from(font_name), font.clone()); + Some(font) + } else { + None + } + // } +} + +pub fn load_font(font_name: &str) -> Result, Box> { + match FontAssets::get(font_name) { + Some(assets) => { + let font = Font::try_from_vec(Vec::from(assets.data)).unwrap(); + Ok(Some(font)) + } + None => { + tracing::error!("Unable to find the specified font."); + Ok(None) + } + } +} diff --git a/easytier-web/src/restful/captcha/utils/mod.rs b/easytier-web/src/restful/captcha/utils/mod.rs new file mode 100644 index 0000000..99e5821 --- /dev/null +++ b/easytier-web/src/restful/captcha/utils/mod.rs @@ -0,0 +1,4 @@ +//! Utilities + +pub(crate) mod color; +pub(crate) mod font; diff --git a/easytier-web/src/restful/mod.rs b/easytier-web/src/restful/mod.rs index fe5c06e..71658cd 100644 --- a/easytier-web/src/restful/mod.rs +++ b/easytier-web/src/restful/mod.rs @@ -1,23 +1,40 @@ -use std::vec; +mod auth; +pub(crate) mod captcha; +mod network; +mod users; + use std::{net::SocketAddr, sync::Arc}; -use axum::extract::{Path, Query}; use axum::http::StatusCode; -use axum::routing::post; use axum::{extract::State, routing::get, Json, Router}; -use easytier::proto::{self, rpc_types, web::*}; -use easytier::{common::scoped_task::ScopedTask, proto::rpc_types::controller::BaseController}; +use axum_login::tower_sessions::{ExpiredDeletion, SessionManagerLayer}; +use axum_login::{login_required, AuthManagerLayerBuilder, AuthzBackend}; +use axum_messages::MessagesManagerLayer; +use easytier::common::scoped_task::ScopedTask; +use easytier::proto::{self, rpc_types}; +use network::NetworkApi; +use sea_orm::DbErr; use tokio::net::TcpListener; +use tower_sessions::cookie::time::Duration; +use tower_sessions::cookie::Key; +use tower_sessions::Expiry; +use tower_sessions_sqlx_store::SqliteStore; +use users::{AuthSession, Backend}; use crate::client_manager::session::Session; use crate::client_manager::storage::StorageToken; use crate::client_manager::ClientManager; +use crate::db::Db; pub struct RestfulServer { bind_addr: SocketAddr, client_mgr: Arc, + db: Db, serve_task: Option>, + delete_task: Option>>, + + network_api: NetworkApi, } type AppStateInner = Arc; @@ -26,52 +43,44 @@ type AppState = State; #[derive(Debug, serde::Deserialize, serde::Serialize)] struct ListSessionJsonResp(Vec); -#[derive(Debug, serde::Deserialize, serde::Serialize)] -struct ValidateConfigJsonReq { - config: String, -} - -#[derive(Debug, serde::Deserialize, serde::Serialize)] -struct RunNetworkJsonReq { - config: String, -} - -#[derive(Debug, serde::Deserialize, serde::Serialize)] -struct ColletNetworkInfoJsonReq { - inst_ids: Option>, -} - -#[derive(Debug, serde::Deserialize, serde::Serialize)] -struct RemoveNetworkJsonReq { - inst_ids: Vec, -} - -#[derive(Debug, serde::Deserialize, serde::Serialize)] -struct ListNetworkInstanceIdsJsonResp(Vec); - -type Error = proto::error::Error; -type ErrorKind = proto::error::error::ErrorKind; +pub type Error = proto::error::Error; +pub type ErrorKind = proto::error::error::ErrorKind; type RpcError = rpc_types::error::Error; type HttpHandleError = (StatusCode, Json); -fn convert_rpc_error(e: RpcError) -> (StatusCode, Json) { - let status_code = match &e { - RpcError::ExecutionError(_) => StatusCode::BAD_REQUEST, - RpcError::Timeout(_) => StatusCode::GATEWAY_TIMEOUT, - _ => StatusCode::BAD_GATEWAY, - }; - let error = Error::from(&e); - (status_code, Json(error)) +pub fn other_error(error_message: T) -> Error { + Error { + error_kind: Some(ErrorKind::OtherError(proto::error::OtherError { + error_message: error_message.to_string(), + })), + } +} + +pub fn convert_db_error(e: DbErr) -> HttpHandleError { + ( + StatusCode::INTERNAL_SERVER_ERROR, + other_error(format!("DB Error: {:#}", e)).into(), + ) } impl RestfulServer { - pub fn new(bind_addr: SocketAddr, client_mgr: Arc) -> Self { + pub async fn new( + bind_addr: SocketAddr, + client_mgr: Arc, + db: Db, + ) -> anyhow::Result { assert!(client_mgr.is_running()); - RestfulServer { + + let network_api = NetworkApi::new(); + + Ok(RestfulServer { bind_addr, client_mgr, + db, serve_task: None, - } + delete_task: None, + network_api, + }) } async fn get_session_by_machine_id( @@ -79,162 +88,69 @@ impl RestfulServer { machine_id: &uuid::Uuid, ) -> Result, HttpHandleError> { let Some(result) = client_mgr.get_session_by_machine_id(machine_id) else { - return Err(( - StatusCode::NOT_FOUND, - Error { - error_kind: Some(ErrorKind::OtherError(proto::error::OtherError { - error_message: "No such session".to_string(), - })), - } - .into(), - )); + return Err((StatusCode::NOT_FOUND, other_error("No such session").into())); }; Ok(result) } async fn handle_list_all_sessions( + auth_session: AuthSession, State(client_mgr): AppState, ) -> Result, HttpHandleError> { + let pers = auth_session + .backend + .get_group_permissions(auth_session.user.as_ref().unwrap()) + .await + .unwrap(); + println!("{:?}", pers); let ret = client_mgr.list_sessions().await; Ok(ListSessionJsonResp(ret).into()) } - async fn handle_validate_config( - State(client_mgr): AppState, - Path(machine_id): Path, - Json(payload): Json, - ) -> Result<(), HttpHandleError> { - let config = payload.config; - let result = Self::get_session_by_machine_id(&client_mgr, &machine_id).await?; - - let c = result.scoped_rpc_client(); - c.validate_config(BaseController::default(), ValidateConfigRequest { config }) - .await - .map_err(convert_rpc_error)?; - Ok(()) - } - - async fn handle_run_network_instance( - State(client_mgr): AppState, - Path(machine_id): Path, - Json(payload): Json, - ) -> Result<(), HttpHandleError> { - let config = payload.config; - let result = Self::get_session_by_machine_id(&client_mgr, &machine_id).await?; - - let c = result.scoped_rpc_client(); - c.run_network_instance( - BaseController::default(), - RunNetworkInstanceRequest { config }, - ) - .await - .map_err(convert_rpc_error)?; - Ok(()) - } - - async fn handle_collect_one_network_info( - State(client_mgr): AppState, - Path((machine_id, inst_id)): Path<(uuid::Uuid, uuid::Uuid)>, - ) -> Result, HttpHandleError> { - let result = Self::get_session_by_machine_id(&client_mgr, &machine_id).await?; - - let c = result.scoped_rpc_client(); - let ret = c - .collect_network_info( - BaseController::default(), - CollectNetworkInfoRequest { - inst_ids: vec![inst_id.into()], - }, - ) - .await - .map_err(convert_rpc_error)?; - Ok(ret.into()) - } - - async fn handle_collect_network_info( - State(client_mgr): AppState, - Path(machine_id): Path, - Query(payload): Query, - ) -> Result, HttpHandleError> { - let result = Self::get_session_by_machine_id(&client_mgr, &machine_id).await?; - - let c = result.scoped_rpc_client(); - let ret = c - .collect_network_info( - BaseController::default(), - CollectNetworkInfoRequest { - inst_ids: payload - .inst_ids - .unwrap_or_default() - .into_iter() - .map(Into::into) - .collect(), - }, - ) - .await - .map_err(convert_rpc_error)?; - Ok(ret.into()) - } - - async fn handle_list_network_instance_ids( - State(client_mgr): AppState, - Path(machine_id): Path, - ) -> Result, HttpHandleError> { - let result = Self::get_session_by_machine_id(&client_mgr, &machine_id).await?; - - let c = result.scoped_rpc_client(); - let ret = c - .list_network_instance(BaseController::default(), ListNetworkInstanceRequest {}) - .await - .map_err(convert_rpc_error)?; - Ok( - ListNetworkInstanceIdsJsonResp(ret.inst_ids.into_iter().map(Into::into).collect()) - .into(), - ) - } - - async fn handle_remove_network_instance( - State(client_mgr): AppState, - Path((machine_id, inst_id)): Path<(uuid::Uuid, uuid::Uuid)>, - ) -> Result<(), HttpHandleError> { - let result = Self::get_session_by_machine_id(&client_mgr, &machine_id).await?; - - let c = result.scoped_rpc_client(); - c.delete_network_instance( - BaseController::default(), - DeleteNetworkInstanceRequest { - inst_ids: vec![inst_id.into()], - }, - ) - .await - .map_err(convert_rpc_error)?; - Ok(()) - } - pub async fn start(&mut self) -> Result<(), anyhow::Error> { - let listener = TcpListener::bind(self.bind_addr).await.unwrap(); + let listener = TcpListener::bind(self.bind_addr).await?; + + // Session layer. + // + // This uses `tower-sessions` to establish a layer that will provide the session + // as a request extension. + let session_store = SqliteStore::new(self.db.inner()); + session_store.migrate().await?; + + self.delete_task.replace( + tokio::task::spawn( + session_store + .clone() + .continuously_delete_expired(tokio::time::Duration::from_secs(60)), + ) + .into(), + ); + + // Generate a cryptographic key to sign the session cookie. + let key = Key::generate(); + + let session_layer = SessionManagerLayer::new(session_store) + .with_secure(false) + .with_expiry(Expiry::OnInactivity(Duration::days(1))) + .with_signed(key); + + // Auth service. + // + // This combines the session layer with our backend to establish the auth + // service which will provide the auth session as a request extension. + let backend = Backend::new(self.db.clone()); + let auth_layer = AuthManagerLayerBuilder::new(backend, session_layer).build(); let app = Router::new() .route("/api/v1/sessions", get(Self::handle_list_all_sessions)) - .route( - "/api/v1/network/:machine-id/validate-config", - post(Self::handle_validate_config), - ) - .route( - "/api/v1/network/:machine-id", - post(Self::handle_run_network_instance).get(Self::handle_list_network_instance_ids), - ) - .route( - "/api/v1/network/:machine-id/info", - get(Self::handle_collect_network_info), - ) - .route( - "/api/v1/network/:machine-id/:inst-id", - get(Self::handle_collect_one_network_info) - .delete(Self::handle_remove_network_instance), - ) - .with_state(self.client_mgr.clone()); + .merge(self.network_api.build_route()) + .route_layer(login_required!(Backend)) + .merge(auth::router()) + .with_state(self.client_mgr.clone()) + .layer(MessagesManagerLayer) + .layer(auth_layer) + .layer(tower_http::cors::CorsLayer::very_permissive()); let task = tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); diff --git a/easytier-web/src/restful/network.rs b/easytier-web/src/restful/network.rs new file mode 100644 index 0000000..228cdfc --- /dev/null +++ b/easytier-web/src/restful/network.rs @@ -0,0 +1,321 @@ +use std::sync::Arc; + +use axum::extract::{Path, Query}; +use axum::http::StatusCode; +use axum::routing::{delete, post}; +use axum::{extract::State, routing::get, Json, Router}; +use axum_login::AuthUser; +use dashmap::DashSet; +use easytier::proto::common::Void; +use easytier::proto::rpc_types::controller::BaseController; +use easytier::proto::{self, web::*}; + +use crate::client_manager::session::Session; +use crate::client_manager::ClientManager; + +use super::users::AuthSession; +use super::{ + convert_db_error, AppState, AppStateInner, Error, ErrorKind, HttpHandleError, RpcError, +}; + +fn convert_rpc_error(e: RpcError) -> (StatusCode, Json) { + let status_code = match &e { + RpcError::ExecutionError(_) => StatusCode::BAD_REQUEST, + RpcError::Timeout(_) => StatusCode::GATEWAY_TIMEOUT, + _ => StatusCode::BAD_GATEWAY, + }; + let error = Error::from(&e); + (status_code, Json(error)) +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +struct ValidateConfigJsonReq { + config: String, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +struct RunNetworkJsonReq { + config: String, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +struct ColletNetworkInfoJsonReq { + inst_ids: Option>, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +struct RemoveNetworkJsonReq { + inst_ids: Vec, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +struct ListNetworkInstanceIdsJsonResp(Vec); + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +struct ListMachineItem { + client_url: Option, + info: Option, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +struct ListMachineJsonResp { + machines: Vec, +} + +pub struct NetworkApi {} + +impl NetworkApi { + pub fn new() -> Self { + Self {} + } + + async fn get_session_by_machine_id( + auth_session: &AuthSession, + client_mgr: &ClientManager, + machine_id: &uuid::Uuid, + ) -> Result, HttpHandleError> { + let Some(result) = client_mgr.get_session_by_machine_id(machine_id) else { + return Err(( + StatusCode::NOT_FOUND, + Error { + error_kind: Some(ErrorKind::OtherError(proto::error::OtherError { + error_message: format!("No such session: {}", machine_id), + })), + } + .into(), + )); + }; + + let Some(token) = result.get_token().await else { + return Err(( + StatusCode::UNAUTHORIZED, + Error { + error_kind: Some(ErrorKind::OtherError(proto::error::OtherError { + error_message: "No token reported".to_string(), + })), + } + .into(), + )); + }; + + if !auth_session + .user + .as_ref() + .map(|x| x.tokens.contains(&token.token)) + .unwrap_or(false) + { + return Err(( + StatusCode::FORBIDDEN, + Error { + error_kind: Some(ErrorKind::OtherError(proto::error::OtherError { + error_message: "Token mismatch".to_string(), + })), + } + .into(), + )); + } + + Ok(result) + } + + async fn handle_validate_config( + auth_session: AuthSession, + State(client_mgr): AppState, + Path(machine_id): Path, + Json(payload): Json, + ) -> Result, HttpHandleError> { + let config = payload.config; + let result = + Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?; + + let c = result.scoped_rpc_client(); + c.validate_config(BaseController::default(), ValidateConfigRequest { config }) + .await + .map_err(convert_rpc_error)?; + Ok(Void::default().into()) + } + + async fn handle_run_network_instance( + auth_session: AuthSession, + State(client_mgr): AppState, + Path(machine_id): Path, + Json(payload): Json, + ) -> Result, HttpHandleError> { + let config = payload.config; + let result = + Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?; + + let c = result.scoped_rpc_client(); + let resp = c + .run_network_instance( + BaseController::default(), + RunNetworkInstanceRequest { + inst_id: None, + config: config.clone(), + }, + ) + .await + .map_err(convert_rpc_error)?; + + client_mgr + .db() + .insert_or_update_user_network_config( + auth_session.user.as_ref().unwrap().id(), + resp.inst_id.clone().unwrap_or_default().into(), + config, + ) + .await + .map_err(convert_db_error)?; + + Ok(Void::default().into()) + } + + async fn handle_collect_one_network_info( + auth_session: AuthSession, + State(client_mgr): AppState, + Path((machine_id, inst_id)): Path<(uuid::Uuid, uuid::Uuid)>, + ) -> Result, HttpHandleError> { + let result = + Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?; + + let c = result.scoped_rpc_client(); + let ret = c + .collect_network_info( + BaseController::default(), + CollectNetworkInfoRequest { + inst_ids: vec![inst_id.into()], + }, + ) + .await + .map_err(convert_rpc_error)?; + Ok(ret.into()) + } + + async fn handle_collect_network_info( + auth_session: AuthSession, + State(client_mgr): AppState, + Path(machine_id): Path, + Query(payload): Query, + ) -> Result, HttpHandleError> { + let result = + Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?; + + let c = result.scoped_rpc_client(); + let ret = c + .collect_network_info( + BaseController::default(), + CollectNetworkInfoRequest { + inst_ids: payload + .inst_ids + .unwrap_or_default() + .into_iter() + .map(Into::into) + .collect(), + }, + ) + .await + .map_err(convert_rpc_error)?; + Ok(ret.into()) + } + + async fn handle_list_network_instance_ids( + auth_session: AuthSession, + State(client_mgr): AppState, + Path(machine_id): Path, + ) -> Result, HttpHandleError> { + let result = + Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?; + + let c = result.scoped_rpc_client(); + let ret = c + .list_network_instance(BaseController::default(), ListNetworkInstanceRequest {}) + .await + .map_err(convert_rpc_error)?; + Ok( + ListNetworkInstanceIdsJsonResp(ret.inst_ids.into_iter().map(Into::into).collect()) + .into(), + ) + } + + async fn handle_remove_network_instance( + auth_session: AuthSession, + State(client_mgr): AppState, + Path((machine_id, inst_id)): Path<(uuid::Uuid, uuid::Uuid)>, + ) -> Result<(), HttpHandleError> { + let result = + Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?; + + client_mgr + .db() + .delete_network_config(auth_session.user.as_ref().unwrap().id(), inst_id) + .await + .map_err(convert_db_error)?; + + let c = result.scoped_rpc_client(); + c.delete_network_instance( + BaseController::default(), + DeleteNetworkInstanceRequest { + inst_ids: vec![inst_id.into()], + }, + ) + .await + .map_err(convert_rpc_error)?; + Ok(()) + } + + async fn handle_list_machines( + auth_session: AuthSession, + State(client_mgr): AppState, + ) -> Result, HttpHandleError> { + let tokens = auth_session + .user + .as_ref() + .map(|x| x.tokens.clone()) + .unwrap_or_default(); + + let client_urls = DashSet::new(); + for token in tokens { + let urls = client_mgr.list_machine_by_token(token); + for url in urls { + client_urls.insert(url); + } + } + + let mut machines = vec![]; + for item in client_urls.iter() { + let client_url = item.key().clone(); + let session = client_mgr.get_heartbeat_requests(&client_url).await; + machines.push(ListMachineItem { + client_url: Some(client_url), + info: session, + }); + } + + Ok(Json(ListMachineJsonResp { machines })) + } + + pub fn build_route(&mut self) -> Router { + Router::new() + .route("/api/v1/machines", get(Self::handle_list_machines)) + .route( + "/api/v1/machines/:machine-id/validate-config", + post(Self::handle_validate_config), + ) + .route( + "/api/v1/machines/:machine-id/networks", + post(Self::handle_run_network_instance).get(Self::handle_list_network_instance_ids), + ) + .route( + "/api/v1/machines/:machine-id/networks/:inst-id", + delete(Self::handle_remove_network_instance), + ) + .route( + "/api/v1/machines/:machine-id/networks/info", + get(Self::handle_collect_network_info), + ) + .route( + "/api/v1/machines/:machine-id/networks/info/:inst-id", + get(Self::handle_collect_one_network_info), + ) + } +} diff --git a/easytier-web/src/restful/users.rs b/easytier-web/src/restful/users.rs new file mode 100644 index 0000000..1e879ae --- /dev/null +++ b/easytier-web/src/restful/users.rs @@ -0,0 +1,241 @@ +use std::collections::HashSet; + +use async_trait::async_trait; +use axum_login::{AuthUser, AuthnBackend, AuthzBackend, UserId}; +use password_auth::verify_password; +use sea_orm::{ + ActiveModelTrait as _, ColumnTrait, EntityTrait, FromQueryResult, IntoActiveModel, JoinType, + QueryFilter, QuerySelect as _, RelationTrait, Set, TransactionTrait, +}; +use serde::{Deserialize, Serialize}; +use tokio::task; + +use crate::db::{self, entity}; + +#[derive(Clone, Serialize, Deserialize)] +pub struct User { + db_user: entity::users::Model, + pub tokens: Vec, +} + +// Here we've implemented `Debug` manually to avoid accidentally logging the +// password hash. +impl std::fmt::Debug for User { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("User") + .field("id", &self.db_user.id) + .field("username", &self.db_user.username) + .field("password", &"[redacted]") + .finish() + } +} + +impl AuthUser for User { + type Id = i32; + + fn id(&self) -> Self::Id { + self.db_user.id + } + + fn session_auth_hash(&self) -> &[u8] { + self.db_user.password.as_bytes() // We use the password hash as the auth + // hash--what this means + // is when the user changes their password the + // auth session becomes invalid. + } +} + +// This allows us to extract the authentication fields from forms. We use this +// to authenticate requests with the backend. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Credentials { + pub username: String, + pub password: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct RegisterNewUser { + pub credentials: Credentials, + pub captcha: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ChangePassword { + pub new_password: String, +} + +#[derive(Debug, Clone)] +pub struct Backend { + db: db::Db, +} + +impl Backend { + pub fn new(db: db::Db) -> Self { + Self { db } + } + + pub async fn register_new_user(&self, new_user: &RegisterNewUser) -> anyhow::Result<()> { + let hashed_password = password_auth::generate_hash(new_user.credentials.password.as_str()); + let mut txn = self.db.orm_db().begin().await?; + + entity::users::ActiveModel { + username: Set(new_user.credentials.username.clone()), + password: Set(hashed_password.clone()), + ..Default::default() + } + .save(&mut txn) + .await?; + + entity::users_groups::ActiveModel { + user_id: Set(entity::users::Entity::find() + .filter(entity::users::Column::Username.eq(new_user.credentials.username.as_str())) + .one(&mut txn) + .await? + .unwrap() + .id), + group_id: Set(entity::groups::Entity::find() + .filter(entity::groups::Column::Name.eq("users")) + .one(&mut txn) + .await? + .unwrap() + .id), + ..Default::default() + } + .save(&mut txn) + .await?; + txn.commit().await?; + + Ok(()) + } + + pub async fn change_password( + &self, + id: ::Id, + req: &ChangePassword, + ) -> anyhow::Result<()> { + let hashed_password = password_auth::generate_hash(req.new_password.as_str()); + + use entity::users; + + let mut user = users::Entity::find_by_id(id) + .one(self.db.orm_db()) + .await? + .ok_or(anyhow::anyhow!("User not found"))? + .into_active_model(); + user.password = Set(hashed_password.clone()); + + entity::users::Entity::update(user) + .exec(self.db.orm_db()) + .await?; + + Ok(()) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Sqlx(#[from] sea_orm::DbErr), + + #[error(transparent)] + TaskJoin(#[from] task::JoinError), +} + +#[async_trait] +impl AuthnBackend for Backend { + type User = User; + type Credentials = Credentials; + type Error = Error; + + async fn authenticate( + &self, + creds: Self::Credentials, + ) -> Result, Self::Error> { + let user = entity::users::Entity::find() + .filter(entity::users::Column::Username.eq(creds.username)) + .one(self.db.orm_db()) + .await?; + task::spawn_blocking(|| { + // We're using password-based authentication--this works by comparing our form + // input with an argon2 password hash. + Ok(user + .filter(|user| verify_password(creds.password, &user.password).is_ok()) + .map(|user| User { + db_user: user.clone(), + tokens: vec![user.username.clone()], + })) + }) + .await? + } + + async fn get_user(&self, user_id: &UserId) -> Result, Self::Error> { + let mut user = entity::users::Entity::find() + .filter(entity::users::Column::Id.eq(*user_id)) + .one(self.db.orm_db()) + .await?; + + if let Some(u) = &mut user { + let mut user = User { + db_user: u.clone(), + tokens: vec![], + }; + // username is a token + user.tokens.push(u.username.clone()); + Ok(Some(user)) + } else { + Ok(None) + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, FromQueryResult)] +pub struct Permission { + pub name: String, +} + +impl From<&str> for Permission { + fn from(name: &str) -> Self { + Permission { + name: name.to_string(), + } + } +} + +#[async_trait] +impl AuthzBackend for Backend { + type Permission = Permission; + + async fn get_group_permissions( + &self, + _user: &Self::User, + ) -> Result, Self::Error> { + let permissions = entity::users::Entity::find() + .column_as(entity::permissions::Column::Name, "name") + .join( + JoinType::LeftJoin, + entity::users::Relation::UsersGroups.def(), + ) + .join( + JoinType::LeftJoin, + entity::users_groups::Relation::Groups.def(), + ) + .join( + JoinType::LeftJoin, + entity::groups::Relation::GroupsPermissions.def(), + ) + .join( + JoinType::LeftJoin, + entity::groups_permissions::Relation::Permissions.def(), + ) + .into_model::() + .all(self.db.orm_db()) + .await?; + + Ok(permissions.into_iter().collect()) + } +} + +// We use a type alias for convenience. +// +// Note that we've supplied our concrete backend here. +pub type AuthSession = axum_login::AuthSession; diff --git a/easytier/src/common/mod.rs b/easytier/src/common/mod.rs index ecd68ec..dd9a72e 100644 --- a/easytier/src/common/mod.rs +++ b/easytier/src/common/mod.rs @@ -55,7 +55,6 @@ pub fn join_joinset_background( } future::poll_fn(|cx| { - tracing::debug!("try join joinset tasks"); let Some(js) = js.upgrade() else { return std::task::Poll::Ready(()); }; diff --git a/easytier/src/proto/common.rs b/easytier/src/proto/common.rs index d389ed5..38d9596 100644 --- a/easytier/src/proto/common.rs +++ b/easytier/src/proto/common.rs @@ -17,6 +17,12 @@ impl From for uuid::Uuid { } } +impl From for Uuid { + fn from(value: String) -> Self { + uuid::Uuid::parse_str(&value).unwrap().into() + } +} + impl Display for Uuid { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", uuid::Uuid::from(self.clone())) diff --git a/easytier/src/proto/web.proto b/easytier/src/proto/web.proto index 9976c3f..c3d8459 100644 --- a/easytier/src/proto/web.proto +++ b/easytier/src/proto/web.proto @@ -36,6 +36,12 @@ message HeartbeatRequest { common.UUID machine_id = 1; common.UUID inst_id = 2; string user_token = 3; + + string easytier_version = 4; + string report_time = 5; + string hostname = 6; + + repeated common.UUID running_network_instances = 7; } message HeartbeatResponse { @@ -53,10 +59,12 @@ message ValidateConfigResponse { } message RunNetworkInstanceRequest { - string config = 1; + common.UUID inst_id = 1; + string config = 2; } message RunNetworkInstanceResponse { + common.UUID inst_id = 1; } message RetainNetworkInstanceRequest { diff --git a/easytier/src/tunnel/packet_def.rs b/easytier/src/tunnel/packet_def.rs index e9c4f00..7e93c03 100644 --- a/easytier/src/tunnel/packet_def.rs +++ b/easytier/src/tunnel/packet_def.rs @@ -547,7 +547,7 @@ impl ZCPacket { ZCPacketType::NIC => unreachable!(), }; - tracing::debug!(?self.packet_type, ?target_packet_type, ?new_offset, "convert zc packet type"); + tracing::trace!(?self.packet_type, ?target_packet_type, ?new_offset, "convert zc packet type"); if new_offset == INVALID_OFFSET { // copy peer manager header and payload to new buffer diff --git a/easytier/src/web_client/controller.rs b/easytier/src/web_client/controller.rs index fa1c0d1..d3ce15b 100644 --- a/easytier/src/web_client/controller.rs +++ b/easytier/src/web_client/controller.rs @@ -101,8 +101,14 @@ impl WebClientService for Controller { req: RunNetworkInstanceRequest, ) -> Result { let cfg = TomlConfigLoader::new_from_str(&req.config)?; + let id = cfg.get_id(); + if let Some(inst_id) = req.inst_id { + cfg.set_id(inst_id.into()); + } self.run_network_instance(cfg)?; - Ok(RunNetworkInstanceResponse {}) + Ok(RunNetworkInstanceResponse { + inst_id: Some(id.into()), + }) } async fn retain_network_instance( diff --git a/easytier/src/web_client/session.rs b/easytier/src/web_client/session.rs index 5706f18..493a537 100644 --- a/easytier/src/web_client/session.rs +++ b/easytier/src/web_client/session.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::sync::{Arc, Weak}; use tokio::{ sync::{broadcast, Mutex}, @@ -7,7 +7,7 @@ use tokio::{ }; use crate::{ - common::get_machine_id, + common::{constants::EASYTIER_VERSION, get_machine_id}, proto::{ rpc_impl::bidirect::BidirectRpcManager, rpc_types::controller::BaseController, @@ -47,7 +47,8 @@ impl Session { .register(WebClientServiceServer::new(controller.clone()), ""); let mut tasks: JoinSet<()> = JoinSet::new(); - let heartbeat_ctx = Self::heartbeat_routine(&rpc_mgr, controller.token(), &mut tasks); + let heartbeat_ctx = + Self::heartbeat_routine(&rpc_mgr, Arc::downgrade(&controller), &mut tasks); Session { rpc_mgr, @@ -59,7 +60,7 @@ impl Session { fn heartbeat_routine( rpc_mgr: &BidirectRpcManager, - token: String, + controller: Weak, tasks: &mut JoinSet<()>, ) -> HeartbeatCtx { let (tx, _rx1) = broadcast::channel(2); @@ -71,7 +72,8 @@ impl Session { let mid = get_machine_id(); let inst_id = uuid::Uuid::new_v4(); - let token = token; + let token = controller.upgrade().unwrap().token(); + let hostname = gethostname::gethostname().to_string_lossy().to_string(); let ctx_clone = ctx.clone(); let mut tick = interval(std::time::Duration::from_secs(1)); @@ -79,13 +81,29 @@ impl Session { .rpc_client() .scoped_client::>(1, 1, "".to_string()); tasks.spawn(async move { - let req = HeartbeatRequest { - machine_id: Some(mid.into()), - inst_id: Some(inst_id.into()), - user_token: token.to_string(), - }; loop { tick.tick().await; + + let Some(controller) = controller.upgrade() else { + break; + }; + + let req = HeartbeatRequest { + machine_id: Some(mid.into()), + inst_id: Some(inst_id.into()), + user_token: token.to_string(), + + easytier_version: EASYTIER_VERSION.to_string(), + hostname: hostname.clone(), + report_time: chrono::Local::now().to_string(), + + running_network_instances: controller + .list_network_instance_ids() + .into_iter() + .map(Into::into) + .collect(), + }; + match client .heartbeat(BaseController::default(), req.clone()) .await