From 2051b972f16e1916de6af0a114785755308c7b51 Mon Sep 17 00:00:00 2001 From: aOK Date: Tue, 11 Jun 2024 21:48:59 +0300 Subject: [PATCH] first commit --- 597cc37e-54eb-4b64-ba07-3cfca59fc881.svg | 1 + Cargo.lock | 4242 +++++++++++++++++ Cargo.toml | 201 + README.md | 0 configs/server.json | 136 + configs/server.toml | 397 ++ extra.txt | 87 + src/args.rs | 8 + src/binary/command.rs | 104 + src/binary/handlers/mod.rs | 8 + .../create_personal_access_token_handler.rs | 24 + .../delete_personal_access_token_handler.rs | 23 + .../get_personal_access_tokens_handler.rs | 23 + ...ogin_with_personal_access_token_handler.rs | 25 + .../handlers/personal_access_tokens/mod.rs | 4 + .../handlers/system/get_client_handler.rs | 27 + .../handlers/system/get_clients_handler.rs | 21 + src/binary/handlers/system/get_me_handler.rs | 27 + .../handlers/system/get_stats_handler.rs | 21 + src/binary/handlers/system/mod.rs | 5 + src/binary/handlers/system/ping_handler.rs | 16 + .../handlers/users/change_password_handler.rs | 27 + .../handlers/users/create_user_handler.rs | 28 + .../handlers/users/delete_user_handler.rs | 20 + src/binary/handlers/users/get_user_handler.rs | 21 + .../handlers/users/get_users_handler.rs | 21 + .../handlers/users/login_user_handler.rs | 24 + .../handlers/users/logout_user_handler.rs | 21 + src/binary/handlers/users/mod.rs | 9 + .../users/update_permissions_handler.rs | 22 + .../handlers/users/update_user_handler.rs | 27 + src/binary/mapper.rs | 266 ++ src/binary/mod.rs | 4 + src/binary/sender.rs | 10 + src/build.rs | 16 + src/channels/commands/clean_messages.rs | 176 + .../commands/clean_personal_access_tokens.rs | 149 + src/channels/commands/mod.rs | 3 + src/channels/commands/save_messages.rs | 98 + src/channels/handler.rs | 32 + src/channels/mod.rs | 3 + src/channels/server_command.rs | 24 + src/configs/config_provider.rs | 266 ++ src/configs/defaults.rs | 286 ++ src/configs/displays.rs | 287 ++ src/configs/http.rs | 129 + src/configs/mod.rs | 13 + src/configs/mqtt.rs | 47 + src/configs/quic.rs | 29 + src/configs/resource_quota.rs | 183 + src/configs/server.rs | 64 + src/configs/system.rs | 166 + src/configs/tcp.rs | 15 + src/configs/validators.rs | 145 + src/http/axum_http/diagnostics.rs | 42 + src/http/axum_http/http_server.rs | 182 + src/http/axum_http/jwt/cleaner.rs | 33 + src/http/axum_http/jwt/json_web_token.rs | 37 + src/http/axum_http/jwt/jwt_manager.rs | 269 ++ src/http/axum_http/jwt/middleware.rs | 69 + src/http/axum_http/jwt/mod.rs | 6 + src/http/axum_http/jwt/refresh_token.rs | 73 + src/http/axum_http/jwt/storage.rs | 193 + src/http/axum_http/metrics.rs | 26 + src/http/axum_http/mod.rs | 9 + src/http/axum_http/system.rs | 82 + src/http/axum_http/testserver.rs | 50 + src/http/axum_http/users.rs | 203 + src/http/error.rs | 147 + src/http/mapper.rs | 264 + src/http/mod.rs | 5 + src/http/shared.rs | 38 + src/http/xitcav_http/diagnostics.rs | 50 + src/http/xitcav_http/error.rs | 71 + src/http/xitcav_http/http_server.rs | 264 + src/http/xitcav_http/jwt/middlewarex.rs | 81 + src/http/xitcav_http/jwt/mod.rs | 1 + src/http/xitcav_http/metrics.rs | 22 + src/http/xitcav_http/mod.rs | 7 + src/http/xitcav_http/request_limits.rs | 327 ++ src/http/xitcav_http/system.rs | 133 + src/http/xitcav_http/users.rs | 231 + src/iggy/mod.rs | 0 src/infrastructure/cache/buffer.rs | 113 + src/infrastructure/cache/memory_tracker.rs | 101 + src/infrastructure/cache/mod.rs | 2 + src/infrastructure/clients/client_manager.rs | 126 + src/infrastructure/clients/mod.rs | 1 + src/infrastructure/diagnostics/metrics.rs | 68 + src/infrastructure/diagnostics/mod.rs | 1 + src/infrastructure/error.rs | 332 ++ src/infrastructure/mod.rs | 11 + src/infrastructure/persistence/mod.rs | 1 + src/infrastructure/persistence/persister.rs | 75 + .../personal_access_tokens/mod.rs | 2 + .../personal_access_token.rs | 79 + .../personal_access_tokens/storage.rs | 210 + src/infrastructure/session.rs | 65 + src/infrastructure/storage.rs | 189 + src/infrastructure/systems/clients.rs | 98 + src/infrastructure/systems/info.rs | 179 + src/infrastructure/systems/mod.rs | 7 + .../systems/personal_access_token.rs | 127 + src/infrastructure/systems/stats.rs | 70 + src/infrastructure/systems/storage.rs | 91 + src/infrastructure/systems/system.rs | 198 + src/infrastructure/systems/users.rs | 297 ++ src/infrastructure/users/mod.rs | 4 + src/infrastructure/users/permissioner.rs | 75 + .../users/permissioner_rules/mod.rs | 2 + .../users/permissioner_rules/system.rs | 26 + .../users/permissioner_rules/users.rs | 52 + src/infrastructure/users/storage.rs | 193 + src/infrastructure/users/user.rs | 99 + src/infrastructure/utils/crypto.rs | 9 + src/infrastructure/utils/file.rs | 41 + src/infrastructure/utils/hash.rs | 20 + src/infrastructure/utils/mod.rs | 4 + src/infrastructure/utils/random_id.rs | 10 + src/lib.rs | 14 + src/logging/mod.rs | 253 + src/main.rs | 288 ++ src/mod.rs | 252 + src/models/binary/binary_client.rs | 27 + src/models/binary/mapper.rs | 287 ++ src/models/binary/mod.rs | 21 + src/models/binary/personal_access_tokens.rs | 58 + src/models/binary/system.rs | 56 + src/models/binary/users.rs | 87 + src/models/bytes_serializable.rs | 13 + src/models/client.rs | 126 + src/models/client_info.rs | 61 + src/models/command.rs | 708 +++ src/models/header.rs | 896 ++++ src/models/http/client.rs | 249 + src/models/http/config.rs | 17 + src/models/http/mod.rs | 11 + src/models/http/personal_access_tokens.rs | 51 + src/models/http/system.rs | 46 + src/models/http/users.rs | 102 + src/models/identifier.rs | 268 ++ src/models/identity_info.rs | 38 + src/models/mod.rs | 20 + src/models/permissions.rs | 411 ++ src/models/personal_access_token.rs | 22 + .../create_personal_access_token.rs | 138 + .../delete_personal_access_token.rs | 111 + .../get_personal_access_tokens.rs | 66 + .../login_with_personal_access_token.rs | 103 + src/models/personal_access_tokens/mod.rs | 4 + src/models/sizeable.rs | 4 + src/models/stats.rs | 61 + src/models/system/get_client.rs | 87 + src/models/system/get_clients.rs | 66 + src/models/system/get_me.rs | 66 + src/models/system/get_stats.rs | 66 + src/models/system/mod.rs | 5 + src/models/system/ping.rs | 67 + src/models/tcp/client.rs | 325 ++ src/models/tcp/config.rs | 26 + src/models/tcp/mod.rs | 11 + src/models/tcp/personal_access_tokens.rs | 42 + src/models/tcp/system.rs | 35 + src/models/tcp/users.rs | 55 + src/models/user_info.rs | 50 + src/models/user_status.rs | 54 + src/models/users/change_password.rs | 164 + src/models/users/create_user.rs | 257 + src/models/users/defaults.rs | 10 + src/models/users/delete_user.rs | 78 + src/models/users/get_user.rs | 78 + src/models/users/get_users.rs | 68 + src/models/users/login_user.rs | 148 + src/models/users/logout_user.rs | 66 + src/models/users/mod.rs | 10 + src/models/users/update_permissions.rs | 158 + src/models/users/update_user.rs | 186 + src/models/validatable.rs | 4 + src/mqtt/mod.rs | 1 + src/mqtt/mqtt_server.rs | 0 src/quic/listener.rs | 127 + src/quic/mod.rs | 3 + src/quic/quic_sender.rs | 54 + src/quic/quic_server.rs | 78 + src/server_error.rs | 67 + src/tcp/connection_handler.rs | 383 ++ src/tcp/mod.rs | 5 + src/tcp/sender.rs | 60 + src/tcp/tcp_listener.rs | 91 + src/tcp/tcp_sender.rs | 34 + src/tcp/tcp_server.rs | 97 + src/utils/byte_size.rs | 243 + src/utils/checksum.rs | 3 + src/utils/crypto.rs | 95 + src/utils/duration.rs | 139 + src/utils/mod.rs | 6 + src/utils/text.rs | 34 + src/utils/timestamp.rs | 71 + src/webtransport/mod.rs | 0 199 files changed, 22566 insertions(+) create mode 100644 597cc37e-54eb-4b64-ba07-3cfca59fc881.svg create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 configs/server.json create mode 100644 configs/server.toml create mode 100644 extra.txt create mode 100644 src/args.rs create mode 100644 src/binary/command.rs create mode 100644 src/binary/handlers/mod.rs create mode 100644 src/binary/handlers/personal_access_tokens/create_personal_access_token_handler.rs create mode 100644 src/binary/handlers/personal_access_tokens/delete_personal_access_token_handler.rs create mode 100644 src/binary/handlers/personal_access_tokens/get_personal_access_tokens_handler.rs create mode 100644 src/binary/handlers/personal_access_tokens/login_with_personal_access_token_handler.rs create mode 100644 src/binary/handlers/personal_access_tokens/mod.rs create mode 100644 src/binary/handlers/system/get_client_handler.rs create mode 100644 src/binary/handlers/system/get_clients_handler.rs create mode 100644 src/binary/handlers/system/get_me_handler.rs create mode 100644 src/binary/handlers/system/get_stats_handler.rs create mode 100644 src/binary/handlers/system/mod.rs create mode 100644 src/binary/handlers/system/ping_handler.rs create mode 100644 src/binary/handlers/users/change_password_handler.rs create mode 100644 src/binary/handlers/users/create_user_handler.rs create mode 100644 src/binary/handlers/users/delete_user_handler.rs create mode 100644 src/binary/handlers/users/get_user_handler.rs create mode 100644 src/binary/handlers/users/get_users_handler.rs create mode 100644 src/binary/handlers/users/login_user_handler.rs create mode 100644 src/binary/handlers/users/logout_user_handler.rs create mode 100644 src/binary/handlers/users/mod.rs create mode 100644 src/binary/handlers/users/update_permissions_handler.rs create mode 100644 src/binary/handlers/users/update_user_handler.rs create mode 100644 src/binary/mapper.rs create mode 100644 src/binary/mod.rs create mode 100644 src/binary/sender.rs create mode 100644 src/build.rs create mode 100644 src/channels/commands/clean_messages.rs create mode 100644 src/channels/commands/clean_personal_access_tokens.rs create mode 100644 src/channels/commands/mod.rs create mode 100644 src/channels/commands/save_messages.rs create mode 100644 src/channels/handler.rs create mode 100644 src/channels/mod.rs create mode 100644 src/channels/server_command.rs create mode 100644 src/configs/config_provider.rs create mode 100644 src/configs/defaults.rs create mode 100644 src/configs/displays.rs create mode 100644 src/configs/http.rs create mode 100644 src/configs/mod.rs create mode 100644 src/configs/mqtt.rs create mode 100644 src/configs/quic.rs create mode 100644 src/configs/resource_quota.rs create mode 100644 src/configs/server.rs create mode 100644 src/configs/system.rs create mode 100644 src/configs/tcp.rs create mode 100644 src/configs/validators.rs create mode 100644 src/http/axum_http/diagnostics.rs create mode 100644 src/http/axum_http/http_server.rs create mode 100644 src/http/axum_http/jwt/cleaner.rs create mode 100644 src/http/axum_http/jwt/json_web_token.rs create mode 100644 src/http/axum_http/jwt/jwt_manager.rs create mode 100644 src/http/axum_http/jwt/middleware.rs create mode 100644 src/http/axum_http/jwt/mod.rs create mode 100644 src/http/axum_http/jwt/refresh_token.rs create mode 100644 src/http/axum_http/jwt/storage.rs create mode 100644 src/http/axum_http/metrics.rs create mode 100644 src/http/axum_http/mod.rs create mode 100644 src/http/axum_http/system.rs create mode 100644 src/http/axum_http/testserver.rs create mode 100644 src/http/axum_http/users.rs create mode 100644 src/http/error.rs create mode 100644 src/http/mapper.rs create mode 100644 src/http/mod.rs create mode 100644 src/http/shared.rs create mode 100644 src/http/xitcav_http/diagnostics.rs create mode 100644 src/http/xitcav_http/error.rs create mode 100644 src/http/xitcav_http/http_server.rs create mode 100644 src/http/xitcav_http/jwt/middlewarex.rs create mode 100644 src/http/xitcav_http/jwt/mod.rs create mode 100644 src/http/xitcav_http/metrics.rs create mode 100644 src/http/xitcav_http/mod.rs create mode 100644 src/http/xitcav_http/request_limits.rs create mode 100644 src/http/xitcav_http/system.rs create mode 100644 src/http/xitcav_http/users.rs create mode 100644 src/iggy/mod.rs create mode 100644 src/infrastructure/cache/buffer.rs create mode 100644 src/infrastructure/cache/memory_tracker.rs create mode 100644 src/infrastructure/cache/mod.rs create mode 100644 src/infrastructure/clients/client_manager.rs create mode 100644 src/infrastructure/clients/mod.rs create mode 100644 src/infrastructure/diagnostics/metrics.rs create mode 100644 src/infrastructure/diagnostics/mod.rs create mode 100644 src/infrastructure/error.rs create mode 100644 src/infrastructure/mod.rs create mode 100644 src/infrastructure/persistence/mod.rs create mode 100644 src/infrastructure/persistence/persister.rs create mode 100644 src/infrastructure/personal_access_tokens/mod.rs create mode 100644 src/infrastructure/personal_access_tokens/personal_access_token.rs create mode 100644 src/infrastructure/personal_access_tokens/storage.rs create mode 100644 src/infrastructure/session.rs create mode 100644 src/infrastructure/storage.rs create mode 100644 src/infrastructure/systems/clients.rs create mode 100644 src/infrastructure/systems/info.rs create mode 100644 src/infrastructure/systems/mod.rs create mode 100644 src/infrastructure/systems/personal_access_token.rs create mode 100644 src/infrastructure/systems/stats.rs create mode 100644 src/infrastructure/systems/storage.rs create mode 100644 src/infrastructure/systems/system.rs create mode 100644 src/infrastructure/systems/users.rs create mode 100644 src/infrastructure/users/mod.rs create mode 100644 src/infrastructure/users/permissioner.rs create mode 100644 src/infrastructure/users/permissioner_rules/mod.rs create mode 100644 src/infrastructure/users/permissioner_rules/system.rs create mode 100644 src/infrastructure/users/permissioner_rules/users.rs create mode 100644 src/infrastructure/users/storage.rs create mode 100644 src/infrastructure/users/user.rs create mode 100644 src/infrastructure/utils/crypto.rs create mode 100644 src/infrastructure/utils/file.rs create mode 100644 src/infrastructure/utils/hash.rs create mode 100644 src/infrastructure/utils/mod.rs create mode 100644 src/infrastructure/utils/random_id.rs create mode 100644 src/lib.rs create mode 100644 src/logging/mod.rs create mode 100644 src/main.rs create mode 100644 src/mod.rs create mode 100644 src/models/binary/binary_client.rs create mode 100644 src/models/binary/mapper.rs create mode 100644 src/models/binary/mod.rs create mode 100644 src/models/binary/personal_access_tokens.rs create mode 100644 src/models/binary/system.rs create mode 100644 src/models/binary/users.rs create mode 100644 src/models/bytes_serializable.rs create mode 100644 src/models/client.rs create mode 100644 src/models/client_info.rs create mode 100644 src/models/command.rs create mode 100644 src/models/header.rs create mode 100644 src/models/http/client.rs create mode 100644 src/models/http/config.rs create mode 100644 src/models/http/mod.rs create mode 100644 src/models/http/personal_access_tokens.rs create mode 100644 src/models/http/system.rs create mode 100644 src/models/http/users.rs create mode 100644 src/models/identifier.rs create mode 100644 src/models/identity_info.rs create mode 100644 src/models/mod.rs create mode 100644 src/models/permissions.rs create mode 100644 src/models/personal_access_token.rs create mode 100644 src/models/personal_access_tokens/create_personal_access_token.rs create mode 100644 src/models/personal_access_tokens/delete_personal_access_token.rs create mode 100644 src/models/personal_access_tokens/get_personal_access_tokens.rs create mode 100644 src/models/personal_access_tokens/login_with_personal_access_token.rs create mode 100644 src/models/personal_access_tokens/mod.rs create mode 100644 src/models/sizeable.rs create mode 100644 src/models/stats.rs create mode 100644 src/models/system/get_client.rs create mode 100644 src/models/system/get_clients.rs create mode 100644 src/models/system/get_me.rs create mode 100644 src/models/system/get_stats.rs create mode 100644 src/models/system/mod.rs create mode 100644 src/models/system/ping.rs create mode 100644 src/models/tcp/client.rs create mode 100644 src/models/tcp/config.rs create mode 100644 src/models/tcp/mod.rs create mode 100644 src/models/tcp/personal_access_tokens.rs create mode 100644 src/models/tcp/system.rs create mode 100644 src/models/tcp/users.rs create mode 100644 src/models/user_info.rs create mode 100644 src/models/user_status.rs create mode 100644 src/models/users/change_password.rs create mode 100644 src/models/users/create_user.rs create mode 100644 src/models/users/defaults.rs create mode 100644 src/models/users/delete_user.rs create mode 100644 src/models/users/get_user.rs create mode 100644 src/models/users/get_users.rs create mode 100644 src/models/users/login_user.rs create mode 100644 src/models/users/logout_user.rs create mode 100644 src/models/users/mod.rs create mode 100644 src/models/users/update_permissions.rs create mode 100644 src/models/users/update_user.rs create mode 100644 src/models/validatable.rs create mode 100644 src/mqtt/mod.rs create mode 100644 src/mqtt/mqtt_server.rs create mode 100644 src/quic/listener.rs create mode 100644 src/quic/mod.rs create mode 100644 src/quic/quic_sender.rs create mode 100644 src/quic/quic_server.rs create mode 100644 src/server_error.rs create mode 100644 src/tcp/connection_handler.rs create mode 100644 src/tcp/mod.rs create mode 100644 src/tcp/sender.rs create mode 100644 src/tcp/tcp_listener.rs create mode 100644 src/tcp/tcp_sender.rs create mode 100644 src/tcp/tcp_server.rs create mode 100644 src/utils/byte_size.rs create mode 100644 src/utils/checksum.rs create mode 100644 src/utils/crypto.rs create mode 100644 src/utils/duration.rs create mode 100644 src/utils/mod.rs create mode 100644 src/utils/text.rs create mode 100644 src/utils/timestamp.rs create mode 100644 src/webtransport/mod.rs diff --git a/597cc37e-54eb-4b64-ba07-3cfca59fc881.svg b/597cc37e-54eb-4b64-ba07-3cfca59fc881.svg new file mode 100644 index 0000000..c64c525 --- /dev/null +++ b/597cc37e-54eb-4b64-ba07-3cfca59fc881.svg @@ -0,0 +1 @@ +telephone \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..6d9e077 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4242 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" + +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "async-dropper" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d901072ae4dcdca2201b98beb02d31fb4b6b2472fbd0e870b12ec15b8b35b2d2" +dependencies = [ + "async-dropper-derive", + "async-dropper-simple", + "async-trait", + "futures", + "tokio", +] + +[[package]] +name = "async-dropper-derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a35cf17a37761f1c88b8e770b5956820fe84c12854165b6f930c604ea186e47e" +dependencies = [ + "async-trait", + "proc-macro2", + "quote", + "syn 2.0.48", + "tokio", +] + +[[package]] +name = "async-dropper-simple" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7c4748dfe8cd3d625ec68fc424fa80c134319881185866f9e173af9e5d8add8" +dependencies = [ + "async-scoped", + "async-trait", + "futures", + "rustc_version", + "tokio", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-scoped" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4042078ea593edffc452eef14e99fdb2b120caa4ad9618bcdeabc4a023b98740" +dependencies = [ + "futures", + "pin-project", + "tokio", +] + +[[package]] +name = "async-trait" +version = "0.1.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "atomic" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "axum" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1236b4b292f6c4d6dc34604bb5120d85c3fe1d1aa596bd5cc52ca054d13e7b9e" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.1.0", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-server" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ad46c3ec4e12f4a4b6835e173ba21c25e484c9d02b49770bf006ce5367c036" +dependencies = [ + "arc-swap", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.1.0", + "hyper-util", + "pin-project-lite", + "rustls 0.21.10", + "rustls-pemfile 2.0.0", + "tokio", + "tokio-rustls", + "tower", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bcrypt" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d1c9c15093eb224f0baa400f38fcd713fc1391a6f1c389d886beef146d60a3" +dependencies = [ + "base64", + "blowfish", + "getrandom", + "subtle", + "zeroize", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[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 = "blake3" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0231f06152bf547e9c2b5194f247cd97aacf6dcd8b15d8e5ec0663f64580da87" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + +[[package]] +name = "borsh" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58b559fd6448c6e2fd0adb5720cd98a2506594cafa4737ff98c396f3e82f667" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aadb5b6ccbd078890f6d7003694e33816e6b784358f18e15e7e6d9f065a57cd" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.48", + "syn_derive", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "byte-unit" +version = "5.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ac19bdf0b2665407c39d82dbc937e951e7e2001609f0fb32edd0af45a2d63e" +dependencies = [ + "rust_decimal", + "serde", + "utf8-width", +] + +[[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 = "bytecount" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" + +[[package]] +name = "bytemuck" +version = "1.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "694c8807f2ae16faecc43dc17d74b3eb042482789fd0eb64b39a2e04e087053f" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", +] + +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "chrono" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.0", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.0", +] + +[[package]] +name = "clap_derive" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "comfy-table" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c64043d6c7b7a4c58e39e7efccfdea7b93d885a795d0c054a69dbbf4dd52686" +dependencies = [ + "crossterm", + "strum 0.25.0", + "strum_macros 0.25.3", + "unicode-width", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd91cf61412820176e137621345ee43b3f4423e589e7ae4e50d601d93e35ef8" +dependencies = [ + "aes-gcm", + "base64", + "hkdf", + "hmac", + "percent-encoding", + "rand", + "sha2", + "subtle", + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.4.2", + "crossterm_winapi", + "libc", + "parking_lot 0.12.1", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "darling" +version = "0.20.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc5d6b04b3fd0ba9926f945895de7d806260a2d7431ba82e7edaecb043c4c6b8" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e48a959bcd5c761246f5d090ebc2fbf7b9cd527a492b07a67510c108f1e7e3" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 2.0.48", +] + +[[package]] +name = "darling_macro" +version = "0.20.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1545d67a2149e1d93b7e5c7752dce5a7426eb5d1357ddcfd89336b94444f77" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "dev" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "anyhow", + "async-trait", + "axum", + "axum-server", + "base64", + "bcrypt", + "blake3", + "byte-unit", + "bytes", + "cfg-if", + "chrono", + "clap", + "comfy-table", + "crc32fast", + "env_logger", + "figlet-rs", + "figment", + "flume", + "futures", + "humantime", + "iggy", + "jsonwebtoken", + "keepcalm", + "lazy_static", + "libc", + "log", + "moka", + "openssl", + "prometheus-client", + "quinn", + "rcgen", + "regex", + "reqwest", + "reqwest-middleware", + "reqwest-retry", + "ring 0.17.7", + "rmp-serde", + "rumqttc", + "rustls 0.21.10", + "rustls-pemfile 2.0.0", + "serde", + "serde_json", + "serde_with", + "sled", + "strip-ansi-escapes", + "strum 0.26.1", + "sysinfo", + "thiserror", + "tokio", + "tokio-native-tls", + "toml", + "tower", + "tower-http", + "tower-layer", + "tower-service", + "tracing", + "tracing-appender", + "tracing-subscriber", + "ulid", + "uuid", + "vergen", + "xitca-codegen", + "xitca-http", + "xitca-io", + "xitca-router", + "xitca-server", + "xitca-service", + "xitca-web", + "xxhash-rust", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "version_check", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "figlet-rs" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4742a071cd9694fc86f9fa1a08fa3e53d40cc899d7ee532295da2d085639fbc5" + +[[package]] +name = "figment" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6e5bc7bd59d60d0d45a6ccab6cf0f4ce28698fb4e81e750ddf229c9b824026" +dependencies = [ + "atomic", + "pear", + "serde", + "serde_json", + "toml", + "uncased", + "version_check", +] + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin 0.9.8", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "h2" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.11", + "indexmap 2.2.3", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 1.0.0", + "indexmap 2.2.3", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h3" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c8886b9e6e93e7ed93d9433f3779e8d07e3ff96bc67b977d14c7b20c849411" +dependencies = [ + "bytes", + "fastrand", + "futures-util", + "http 1.0.0", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "h3-quinn" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73786bcc0e4c2692ba62c650f7b950ac236e5300c5de3b1d26330555e2322046" +dependencies = [ + "bytes", + "futures", + "h3", + "quinn", + "quinn-proto", + "tokio", + "tokio-util", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0c62115964e08cb8039170eb33c1d0e2388a256930279edca206fff675f82c3" + +[[package]] +name = "hex" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.11", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.0.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" +dependencies = [ + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "pin-project-lite", +] + +[[package]] +name = "http-rate" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5380061124e0cf34062bbf5d4bfc70e433f273c24cd45071c33a461202227e" +dependencies = [ + "http 1.0.0", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.24", + "http 0.2.11", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.2", + "http 1.0.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.28", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "hyper 1.1.0", + "pin-project-lite", + "socket2", + "tokio", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "iggy" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91acb6ffe3e4f3e9bfc3ab735db7c0c07cf167e519054a9ce4d557f0222422e4" +dependencies = [ + "aes-gcm", + "anyhow", + "async-dropper", + "async-trait", + "base64", + "byte-unit", + "bytes", + "chrono", + "clap", + "convert_case", + "crc32fast", + "flume", + "humantime", + "lazy_static", + "openssl", + "quinn", + "regex", + "reqwest", + "reqwest-middleware", + "reqwest-retry", + "rmp-serde", + "rustls 0.21.10", + "serde", + "serde_derive", + "serde_json", + "serde_with", + "thiserror", + "tokio", + "tokio-native-tls", + "tracing", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", + "serde", +] + +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "is-terminal" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "js-sys" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7ea04a7c5c055c175f189b6dc6ba036fd62306b58c66c9f6389036c503a3f4" +dependencies = [ + "base64", + "js-sys", + "pem", + "ring 0.17.7", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "keepcalm" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031ddc7e27bbb011c78958881a3723873608397b8b10e146717fc05cf3364d78" +dependencies = [ + "once_cell", + "parking_lot 0.12.1", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "moka" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1911e88d5831f748a4097a43862d129e3c6fca831eecac9b8db6d01d93c9de2" +dependencies = [ + "async-lock", + "async-trait", + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "futures-util", + "once_cell", + "parking_lot 0.12.1", + "quanta", + "rustc_version", + "skeptic", + "smallvec", + "tagptr", + "thiserror", + "triomphe", + "uuid", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" +dependencies = [ + "bitflags 2.4.2", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "300.2.2+3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bbfad0063610ac26ee79f7484739e2b07555a75c42453b89263830b5c8103bc" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.9", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.4.1", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "pear" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ccca0f6c17acc81df8e242ed473ec144cbf5c98037e69aa6d144780aad103c8" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e22670e8eb757cff11d6c199ca7b987f352f0346e0be4dd23869ec72cb53c77" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "pem" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310" +dependencies = [ + "base64", + "serde", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" + +[[package]] +name = "polyval" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit 0.21.1", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +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.48", + "version_check", + "yansi", +] + +[[package]] +name = "prometheus-client" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f87c10af16e0af74010d2a123d202e8363c04db5acfa91d8747f64a8524da3a" +dependencies = [ + "dtoa", + "itoa", + "parking_lot 0.12.1", + "prometheus-client-derive-encode", +] + +[[package]] +name = "prometheus-client-derive-encode" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[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 = "pulldown-cmark" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" +dependencies = [ + "bitflags 2.4.2", + "memchr", + "unicase", +] + +[[package]] +name = "quanta" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca0b7bac0b97248c40bb77288fc52029cf1459c0461ea1b05ee32ccf011de2c" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + +[[package]] +name = "quinn" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cc2c5017e4b43d5995dcea317bc46c1e09404c0a9664d2908f7f02dfe943d75" +dependencies = [ + "bytes", + "futures-io", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.21.10", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "141bf7dfde2fbc246bfd3fe12f2455aa24b0fbd9af535d8c86c7bd1381ff2b1a" +dependencies = [ + "bytes", + "rand", + "ring 0.16.20", + "rustc-hash", + "rustls 0.21.10", + "rustls-native-certs", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "055b4e778e8feb9f93c4e439f71dc2156ef13360b432b799e179a8c4cdf0b1d7" +dependencies = [ + "bytes", + "libc", + "socket2", + "tracing", + "windows-sys 0.48.0", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +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.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "raw-cpuid" +version = "11.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d86a7c4638d42c44551f4791a20e687dbb4c3de1f33c43dd71e355cd429def1" +dependencies = [ + "bitflags 2.4.2", +] + +[[package]] +name = "rayon" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rcgen" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48406db8ac1f3cbc7dcdb56ec355343817958a356ff430259bb07baf7607e1e1" +dependencies = [ + "pem", + "ring 0.17.7", + "time", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.5", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[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.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.24", + "http 0.2.11", + "http-body 0.4.6", + "hyper 0.14.28", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "reqwest-middleware" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a3e86aa6053e59030e7ce2d2a3b258dd08fc2d337d52f73f6cb480f5858690" +dependencies = [ + "anyhow", + "async-trait", + "http 0.2.11", + "reqwest", + "serde", + "task-local-extensions", + "thiserror", +] + +[[package]] +name = "reqwest-retry" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9af20b65c2ee9746cc575acb6bd28a05ffc0d15e25c992a8f4462d8686aacb4f" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "futures", + "getrandom", + "http 0.2.11", + "hyper 0.14.28", + "parking_lot 0.11.2", + "reqwest", + "reqwest-middleware", + "retry-policies", + "task-local-extensions", + "tokio", + "tracing", + "wasm-timer", +] + +[[package]] +name = "retry-policies" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17dd00bff1d737c40dbcd47d4375281bf4c17933f9eef0a185fc7bacca23ecbd" +dependencies = [ + "anyhow", + "chrono", + "rand", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + +[[package]] +name = "ring" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.48.0", +] + +[[package]] +name = "rkyv" +version = "0.7.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rmp" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bffea85eea980d8a74453e5d02a8d93028f3c34725de143085a844ebe953258a" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "rumqttc" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d8941c6791801b667d52bfe9ff4fc7c968d4f3f9ae8ae7abdaaa1c966feafc8" +dependencies = [ + "bytes", + "flume", + "futures-util", + "log", + "rustls-native-certs", + "rustls-pemfile 1.0.4", + "rustls-webpki 0.101.7", + "thiserror", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "rust_decimal" +version = "1.34.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39449a79f45e8da28c57c341891b69a183044b29518bb8f86dbac9df60bb7df" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +dependencies = [ + "bitflags 2.4.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.21.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +dependencies = [ + "log", + "ring 0.17.7", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" +dependencies = [ + "log", + "ring 0.17.7", + "rustls-pki-types", + "rustls-webpki 0.102.2", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "rustls-pemfile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e4980fa29e4c4b212ffb3db068a564cbf560e51d3944b7c88bd8bf5bec64f4" +dependencies = [ + "base64", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a716eb65e3158e90e17cd93d855216e27bde02745ab842f2cab4a39dba1bacf" + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.7", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +dependencies = [ + "ring 0.17.7", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.7", + "untrusted 0.9.0", +] + +[[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.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "serde_json" +version = "1.0.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.3", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865f9743393e638991566a8b7a479043c2c8da94a33e0a31f18214c9cae0a64d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + +[[package]] +name = "skeptic" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" +dependencies = [ + "bytecount", + "cargo_metadata 0.14.2", + "error-chain", + "glob", + "pulldown-cmark", + "tempfile", + "walkdir", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "sled" +version = "0.34.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" +dependencies = [ + "crc32fast", + "crossbeam-epoch", + "crossbeam-utils", + "fs2", + "fxhash", + "libc", + "log", + "parking_lot 0.11.2", +] + +[[package]] +name = "smallvec" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" +dependencies = [ + "vte", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "strum" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" +dependencies = [ + "strum_macros 0.26.1", +] + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.48", +] + +[[package]] +name = "strum_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.48", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "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.48", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sysinfo" +version = "0.30.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb4f3438c8f6389c864e61221cbc97e9bca98b4daf39a5beb7bea660f528bb2" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "windows", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "task-local-extensions" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba323866e5d033818e3240feeb9f7db2c4296674e4d9e16b97b7bf8f490434e8" +dependencies = [ + "pin-utils", +] + +[[package]] +name = "tempfile" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot 0.12.1", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.10", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap 2.2.3", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ffdf896f8daaabf9b66ba8e77ea1ed5ed0f72821b398aba62352e95062951" +dependencies = [ + "indexmap 2.2.3", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0da193277a4e2c33e59e09b5861580c33dd0a637c3883d0fa74ba40c0374af2e" +dependencies = [ + "bitflags 2.4.2", + "bytes", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "thread_local", + "tracing", + "tracing-core", +] + +[[package]] +name = "triomphe" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ulid" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34778c17965aa2a08913b57e1f34db9b4a63f5de31768b55bf20d2795f921259" +dependencies = [ + "getrandom", + "rand", + "web-time", +] + +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "uuid" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +dependencies = [ + "getrandom", + "rand", + "zerocopy", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vergen" +version = "8.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a78365c3f8ca9dc5428a9f5c6349558c3f9f3eeb65e3fc00b6a981379462947" +dependencies = [ + "anyhow", + "cargo_metadata 0.18.1", + "regex", + "rustc_version", + "rustversion", + "time", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "vte" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" +dependencies = [ + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" + +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee269d72cc29bf77a2c4bc689cc750fb39f5cbd493d2205bbb3f5c7779cf7b0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winnow" +version = "0.5.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5389a154b01683d28c77f8f68f49dea75f0a4da32557a58f68ee51ebba472d29" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "xitca-codegen" +version = "0.1.1" +source = "git+https://github.com/HFQR/xitca-web.git?rev=e27d4fc#e27d4fc4cf669086067328265f133241121a7554" +dependencies = [ + "quote", + "syn 2.0.48", +] + +[[package]] +name = "xitca-http" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89595f7c58a2d61732c69d7510d1b1a84036992ca735eab3a4e346e8c719367" +dependencies = [ + "fnv", + "futures-core", + "futures-util", + "h2 0.4.2", + "h3", + "h3-quinn", + "http 1.0.0", + "httparse", + "httpdate", + "itoa", + "openssl", + "pin-project-lite", + "slab", + "socket2", + "tokio", + "tracing", + "xitca-io", + "xitca-router", + "xitca-service", + "xitca-tls", + "xitca-unsafe-collection", +] + +[[package]] +name = "xitca-io" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e887cc8153538637515e0663704f3492803c5bb48eb7947c80689154d965b7e0" +dependencies = [ + "bytes", + "quinn", + "tokio", + "xitca-unsafe-collection", +] + +[[package]] +name = "xitca-router" +version = "0.2.0" +source = "git+https://github.com/HFQR/xitca-web.git?rev=e27d4fc#e27d4fc4cf669086067328265f133241121a7554" +dependencies = [ + "xitca-unsafe-collection", +] + +[[package]] +name = "xitca-server" +version = "0.1.0" +source = "git+https://github.com/HFQR/xitca-web.git?rev=e27d4fc#e27d4fc4cf669086067328265f133241121a7554" +dependencies = [ + "socket2", + "tokio", + "tracing", + "xitca-io", + "xitca-service", + "xitca-unsafe-collection", +] + +[[package]] +name = "xitca-service" +version = "0.1.0" +source = "git+https://github.com/HFQR/xitca-web.git?rev=e27d4fc#e27d4fc4cf669086067328265f133241121a7554" + +[[package]] +name = "xitca-tls" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890decaabc4ed2367ce5c9d4ed6b8ee007ee8a23546626ad9cd87ee435dcdf" +dependencies = [ + "rustls 0.22.2", + "xitca-io", +] + +[[package]] +name = "xitca-unsafe-collection" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c38c5b92c72ba986bb2c2f4fc40ec56e841194773c02278f3c8d4c9733807270" +dependencies = [ + "bytes", +] + +[[package]] +name = "xitca-web" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3725ac5d5885e967ab99dddb7dcb1f765176c6912867e1e700257208573e4622" +dependencies = [ + "cookie", + "futures-core", + "http-body 1.0.0", + "http-rate", + "pin-project-lite", + "serde", + "serde_json", + "tokio", + "tower-layer", + "tower-service", + "xitca-codegen", + "xitca-http", + "xitca-server", + "xitca-service", + "xitca-unsafe-collection", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53be06678ed9e83edb1745eb72efc0bbcd7b5c3c35711a860906aed827a13d61" + +[[package]] +name = "yansi" +version = "1.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377" + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "zerocopy" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" + +[[patch.unused]] +name = "xitca-http" +version = "0.3.0" +source = "git+https://github.com/HFQR/xitca-web.git?rev=e27d4fc#e27d4fc4cf669086067328265f133241121a7554" + +[[patch.unused]] +name = "xitca-web" +version = "0.3.0" +source = "git+https://github.com/HFQR/xitca-web.git?rev=e27d4fc#e27d4fc4cf669086067328265f133241121a7554" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5e11150 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,201 @@ +[package] +name = "dev" +version = "0.1.0" +edition = "2021" +build = "src/build.rs" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "dev" +crate-type = ["staticlib", "cdylib", "rlib"] + +[dependencies] +# Main +tokio = { version = "1", features = ["full"] } +tokio-native-tls = "0.3.1" +quinn = "0.10.*" +rumqttc = "0.23.0" +iggy = { version = "0.1.2" } +keepcalm = "0.3.5" +async-trait = "0.1.*" +toml = "0.8.*" +strum = { version = "0.26.1", features = ["derive"] } + +axum = { version = "0.7.2" } +axum-server = { version = "0.6.0", features = ["tls-rustls", "tokio-rustls"] } +xitca-http = { version = "0.2.2", features = [ + "http1", + "http2", + "http3", + "openssl", + "rustls", +] } +xitca-web = { version = "0.2.2", features = [ + "codegen", + "cookie", + "json", + "rate-limit", + "tower-http-compat", +] } +xitca-router = { version = "0.2" } +xitca-codegen = { version = "0.1.1" } +xitca-service = { version = "0.1" } +xitca-server = { version = "0.1" } +xitca-io = { version = "0.1" } +# http-rate = "0.1.1" +# xitca-web = "0.2" +# xitca-router = "0.2" +# xitca-http = "0.2" +# xitca-web = { package = "xitca-web", git = "https://github.com/HFQR/xitca-web.git", branch="main" , features = ["codegen", "cookie", "json"]} +# xitca-http = { package = "xitca-http", git = "https://github.com/HFQR/xitca-web.git", branch="main" , features = ["http1","http2", "router", "http3", "openssl", "rustls"]} +# xitca-codegen = { git = "https://github.com/HFQR/xitca-web.git", branch="main" } +# xitca-router = { git = "https://github.com/HFQR/xitca-web.git", branch="main" } + +# xitca-router = { version = "0.2", optional = true } +# xitca-http = { version = "0.1", default-features = false, features = ["http1","http2", "router", "http3", "openssl", "rustls"] } +# xitca-server = { version = "0.1", default-features = false, features = ["http3"] } +# xitca-service = { version = "0.1", default-features = false} +# xitca-service = { git = "https://github.com/HFQR/xitca-web.git", branch="main" } +# xitca-unsafe-collection = { version = "0.1", default-features = false} +# xitca-codegen = { version = "0.1", default-features = false} +lazy_static = "1.4.0" +sysinfo = "0.30.0" + +openssl = "0.10.44" +rustls = { version = "0.21.10" } +rustls-pemfile = "2.0.0" + +# Databases +sled = "0.34.7" + +# Logging / Tracing +# tracing = { version = "0.1.*" } +tracing-appender = "0.2.2" +# tracing-subscriber = { version = "0.3.18", features = ["fmt"] } +tracing = { version = "0.1.40", default-features = false } +tracing-subscriber = { version = "0.3.16", default-features = false, features = [ + "env-filter", + "fmt", + "ansi", +] } +env_logger = "0.10.0" +log = "~0" +prometheus-client = "0.22.0" +tower = { version = "0.4.13" } +tower-http = { version = "0.5.0", features = [ + "add-extension", + "cors", + "trace", + "limit", +] } +tower-layer = "0.3.2" +tower-service = "0.3.2" +ulid = "1.1.0" +uuid = { version = "1.5.0", features = ["v4", "fast-rng", "zerocopy"] } +xxhash-rust = { version = "0.8.*", features = ["xxh32"] } + +# Security +jsonwebtoken = "9.0.0" +ring = "0.17.*" +bcrypt = "0.15.0" +blake3 = "1.5.0" +aes-gcm = "0.10.3" +# rustls-pemfile = "1.0.1" + +# Errors +thiserror = "1.0.*" +anyhow = "1.0.*" + +# cli +clap = { version = "4.4.7", features = ["derive"] } + +# Json Serialization and deserialization +serde = { version = "1", features = ["derive"] } +serde_json = "1" +rmp-serde = "1.1.2" +serde_with = { version = "3.4.0", features = ["base64"] } + +# Fonts +figlet-rs = "0.1.5" +figment = { version = "0.10.*", features = ["json", "toml", "env"] } + +# Miscelleneous +strip-ansi-escapes = "0.2.0" +# sysinfo = "0.29.*" +reqwest = { version = "0.11.*", features = ["json"] } +reqwest-middleware = "0.2.*" +reqwest-retry = "0.3.0" +cfg-if = "1" +humantime = "2.1.0" +base64 = "0.21.5" +regex = "1.10.2" +byte-unit = { version = "5.1.2", default-features = false, features = [ + "serde", + "byte", +] } +bytes = "1.5.0" +chrono = { version = "0.4.31" } +comfy-table = { version = "7.1.0", optional = true } +crc32fast = "1.3.2" + +# =============================== +flume = "0.11.0" +futures = "0.3.30" +moka = { version = "0.12.1", features = ["future"] } +rcgen = "0.12.0" + +[dev-dependencies] +libc = "0.2.147" + +[build-dependencies] +vergen = { version = "8.2.*", features = [ + "build", + "cargo", + "git", + "gitcl", + "rustc", +] } +# [features] +# axum = ["dep:axum"] +# xitca-web = ["dep:xitca-web"] + +# # cargo run -F axum --bin tcptls +# # run cargo run --no-default-features --bin tcptls to turn it off +# default = ["axum"] + +# [features] +# xitca = ["dep:xitca-web"] +# axum = ["dep:axum"] + +# # cargo run -F axum --bin tcptls +# # cargo run -F xitca-web --bin tcptls +# # run cargo run --no-default-features --bin tcptls to turn it off +# default = ["xitca"] + +[profile.release] +opt-level = "z" +lto = "fat" +debug = 0 +strip = true +codegen-units = 1 + +# [profile.release] +# opt-level = 3 +# lto = true +# codegen-units = 1 +# panic = "abort" +# strip = "symbols" + +[patch.crates-io] +xitca-http = { git = "https://github.com/HFQR/xitca-web.git", rev = "e27d4fc" } +xitca-router = { git = "https://github.com/HFQR/xitca-web.git", rev = "e27d4fc" } +xitca-web = { git = "https://github.com/HFQR/xitca-web.git", rev = "e27d4fc" } +xitca-service = { git = "https://github.com/HFQR/xitca-web.git", rev = "e27d4fc" } +xitca-server = { git = "https://github.com/HFQR/xitca-web.git", rev = "e27d4fc" } +xitca-codegen = { git = "https://github.com/HFQR/xitca-web.git", rev = "e27d4fc" } + +[profile.dev] +debug = "line-tables-only" +opt-level = 1 +panic = "abort" diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/configs/server.json b/configs/server.json new file mode 100644 index 0000000..588a474 --- /dev/null +++ b/configs/server.json @@ -0,0 +1,136 @@ +{ + "http": { + // "enabled": true, + "variants": { + "axum_enabled": true, + "xitca_enabled": true + }, + "address": "0.0.0.0:3000", + "cors": { + "enabled": true, + "allowed_methods": ["GET", "POST", "PUT", "DELETE"], + "allowed_origins": ["*"], + "allowed_headers": ["content-type"], + "exposed_headers": [], + "allow_credentials": false, + "allow_private_network": false + }, + "jwt": { + "algorithm": "HS256", + "issuer": "iggy.rs", + "audience": "iggy.rs", + "valid_issuers": ["iggy.rs"], + "valid_audiences": ["iggy.rs"], + "access_token_expiry": "1h", + "refresh_token_expiry": "1d", + "clock_skew": "5s", + "not_before": "0s", + "encoding_secret": "top_secret$iggy.rs$_jwt_HS256_key#!", + "decoding_secret": "top_secret$iggy.rs$_jwt_HS256_key#!", + "use_base64_secret": false + }, + "metrics": { + "enabled": true, + "endpoint": "/metrics" + }, + "tls": { + "enabled": false, + "cert_file": "certs/nigig_cert.pem", + "key_file": "certs/nigig_key.pem" + } + }, + "tcp": { + "enabled": true, + "address": "0.0.0.0:8090", + "tls": { + "enabled": false, + "certificate": "certs/iggy.pfx", + "password": "iggy123" + } + }, + "quic": { + "enabled": true, + "address": "0.0.0.0:8080", + "max_concurrent_bidi_streams": 10000, + "datagram_send_buffer_size": "100KB", + "initial_mtu": "8KB", + "send_window": "100KB", + "receive_window": "100KB", + "keep_alive_interval": "5s", + "max_idle_timeout": "10s", + "certificate": { + "self_signed": true, + "cert_file": "certs/nigig_cert.pem", + "key_file": "certs/nigig_key.pem" + } + }, + "message_cleaner": { + "enabled": true, + "interval": "1m" + }, + "message_saver": { + "enabled": true, + "enforce_fsync": true, + "interval": "30s" + }, + "personal_access_token": { + "max_tokens_per_user": 100, + "cleaner": { + "enabled": true, + "interval": "1m" + } + }, + "system": { + "path": "local_data", + "database": { + "path": "database" + }, + "runtime": { + "path": "runtime" + }, + "logging": { + "path": "logs", + "level": "info", + "max_size": "512MB", + "retention": "7 days" + }, + "cache": { + "enabled": true, + "size": "4 GB" + }, + "retention_policy": { + "message_expiry": "disabled", + "max_topic_size": "10 GB" + }, + "encryption": { + "enabled": false, + "key": "" + }, + "compression": { + "allow_override": false, + "default_algorithm": "none" + }, + "stream": { + "path": "streams" + }, + "topic": { + "path": "topics" + }, + "partition": { + "path": "partitions", + "enforce_fsync": false, + "validate_checksum": false, + "messages_required_to_save": 10000 + }, + "segment": { + "size": "1GB", + "cache_indexes": true, + "cache_time_indexes": true + }, + "message_deduplication": { + "enabled": false, + "max_entries": 1000, + "expiry": "1m" + } + } +} diff --git a/configs/server.toml b/configs/server.toml new file mode 100644 index 0000000..c97f747 --- /dev/null +++ b/configs/server.toml @@ -0,0 +1,397 @@ +# HTTP server configuration +[http] +# Determines if the HTTP server is active. +# `true` enables the server, allowing it to handle HTTP requests. +# `false` disables the server, preventing it from handling HTTP requests. +enabled = true + +# Specifies the network address and port for the HTTP server. +# The format is "HOST:PORT". For example, "0.0.0.0:3000" listens on all network interfaces on port 3000. +address = ["0.0.0.0:3000", "127.0.0.1:3001"] + +[http.variants] +axum_enabled = true +xitca_enabled = true + +# Configuration for Cross-Origin Resource Sharing (CORS). +[http.cors] +# Controls whether CORS is enabled for the HTTP server. +# `true` allows handling cross-origin requests with specified rules. +# `false` blocks cross-origin requests, enhancing security. +enabled = true + +# Specifies which HTTP methods are allowed when CORS is enabled. +# For example, ["GET", "POST"] would allow only GET and POST requests. +allowed_methods = ["GET", "POST", "PUT", "DELETE"] + +# Defines which origins are permitted to make cross-origin requests. +# An asterisk "*" allows all origins. Specific domains can be listed to restrict access. +allowed_origins = ["*"] + +# Lists allowed headers that can be used in CORS requests. +# For example, ["content-type"] permits only the content-type header. +allowed_headers = ["content-type"] + +# Headers that browsers are allowed to access in CORS responses. +# An empty array means no additional headers are exposed to browsers. +exposed_headers = [] + +# Determines if credentials like cookies or HTTP auth can be included in CORS requests. +# `true` allows credentials to be included, useful for authenticated sessions. +# `false` prevents credentials, enhancing privacy and security. +allow_credentials = false + +# Allows or blocks requests from private networks in CORS. +# `true` permits requests from private networks. +# `false` disallows such requests, providing additional security. +allow_private_network = false + +# JWT (JSON Web Token) configuration for HTTP. +[http.jwt] +# Specifies the algorithm used for signing JWTs. +# For example, "HS256" indicates HMAC with SHA-256. +algorithm = "HS256" + +# The issuer of the JWT, typically a URL or an identifier of the issuing entity. +issuer = "iggy.rs" + +# Intended audience for the JWT, usually the recipient or system intended to process the token. +audience = "iggy.rs" + +# Lists valid issuers for JWT validation to ensure tokens are from trusted sources. +valid_issuers = ["iggy.rs"] + +# Lists valid audiences for JWT validation to confirm tokens are for the intended recipient. +valid_audiences = ["iggy.rs"] + +# Expiry time for access tokens. +access_token_expiry = "1h" + +# Expiry time for refresh tokens. +refresh_token_expiry = "1d" + +# Tolerance for timing discrepancies during token validation. +clock_skew = "5s" + +# Time before which the token should not be considered valid. +not_before = "0s" + +# Secret key for encoding JWTs. +encoding_secret = "top_secret$iggy.rs$_jwt_HS256_key#!" + +# Secret key for decoding JWTs. +decoding_secret = "top_secret$iggy.rs$_jwt_HS256_key#!" + +# Indicates if the secret key is base64 encoded. +# `true` means the secret is base64 encoded. +# `false` means the secret is in plain text. +use_base64_secret = false + +# Metrics configuration for HTTP. +[http.metrics] +# Enable or disable the metrics endpoint. +# `true` makes metrics available at the specified endpoint. +# `false` disables metrics collection. +enabled = true + +# Specifies the endpoint for accessing metrics, e.g., "/metrics". +endpoint = "/metrics" + +# TLS (Transport Layer Security) configuration for HTTP. +[http.tls] +# Controls the use of TLS for encrypted HTTP connections. +# `true` enables TLS, enhancing security. +# `false` disables TLS, which may be appropriate in secure internal networks. +enabled = false + +# Path to the TLS certificate file. +cert_file = "certs/nigig_cert.pem" + +# Path to the TLS key file. +key_file = "certs/nigig_key.pem" + +# TCP server configuration. +[tcp] +# Determines if the TCP server is active. +# `true` enables the TCP server for handling TCP connections. +# `false` disables it, preventing any TCP communication. +enabled = true + +# Defines the network address and port for the TCP server. +# For example, "0.0.0.0:8090" listens on all network interfaces on port 8090. +address = "0.0.0.0:8090" + +# TLS configuration for the TCP server. +[tcp.tls] +# Enables or disables TLS for TCP connections. +# `true` secures TCP connections with TLS. +# `false` leaves TCP connections unencrypted. +enabled = false + +# Path to the TLS certificate for TCP. +certificate = "certs/iggy.pfx" + +# Password for the TLS certificate, required for accessing the private key. +password = "iggy123" + +# QUIC protocol configuration. +[quic] +# Controls whether the QUIC server is enabled. +# `true` enables QUIC for fast, secure connections. +# `false` disables QUIC, possibly for compatibility or simplicity. +enabled = true + +# Network address and port for the QUIC server. +# For example, "0.0.0.0:8080" binds to all interfaces on port 8080. +address = "0.0.0.0:8080" + +# Maximum number of simultaneous bidirectional streams in QUIC. +max_concurrent_bidi_streams = 10_000 + +# Size of the buffer for sending datagrams in QUIC. +datagram_send_buffer_size = "100KB" + +# Initial Maximum Transmission Unit (MTU) for QUIC connections. +initial_mtu = "8KB" + +# Size of the sending window in QUIC, controlling data flow. +send_window = "100KB" + +# Size of the receiving window in QUIC, controlling data flow. +receive_window = "100KB" + +# Interval for sending keep-alive messages in QUIC. +keep_alive_interval = "5s" + +# Maximum idle time before a QUIC connection is closed. +max_idle_timeout = "10s" + +# QUIC certificate configuration. +[quic.certificate] +# Indicates whether the QUIC certificate is self-signed. +# `true` for self-signed certificates, often used in internal or testing environments. +# `false` for certificates issued by a certificate authority, common in production. +self_signed = true + +# Path to the QUIC TLS certificate file. +cert_file = "certs/nigig_cert.pem" + +# Path to the QUIC TLS key file. +key_file = "certs/nigig_key.pem" + +# MQTT configuration. +[mqtt] +# Controls whether the MQTT server is enabled. +# `true` enables MQTT for fast, secure connections. +# `false` disables MQTT, possibly for compatibility or simplicity. +enabled = true + +# Network address and port for the MQTT server. +# For example, "0.0.0.0:8080" binds to all interfaces on port 8080. +broker_address = "0.0.0.0" + +port = 4000 + +# Username credentials MQTT. +username = "mqtt" + +# Password credentials in MQTT. +password = "mqtt" + +# Size of the receiving window in MQTT, controlling data flow. +receive_window = "100KB" + +# Interval for sending keep-alive messages in MQTT. +keep_alive_interval = "5s" + +# Maximum idle time before a MQTT connection is closed. +max_idle_timeout = "10s" + +# MQTT certificate configuration. +[mqtt.certificate] +# Indicates whether the MQTT certificate is self-signed. +# `true` for self-signed certificates, often used in internal or testing environments. +# `false` for certificates issued by a certificate authority, common in production. +self_signed = true + +# Path to the MQTT TLS certificate file. +cert_file = "certs/nigig_cert.pem" + +# Path to the MQTT TLS key file. +key_file = "certs/nigig_key.pem" + +# Message cleaner configuration. +[message_cleaner] +# Enables or disables the background process for deleting expired messages. +# `true` activates the message cleaner. +# `false` turns it off, messages will not be auto-deleted based on expiry. +enabled = true + +# Interval for running the message cleaner. +interval = "1m" + +# Message saver configuration. +[message_saver] +# Enables or disables the background process for saving buffered data to disk. +# `true` ensures data is periodically written to disk. +# `false` turns off automatic saving, relying on other triggers for data persistence. +enabled = true + +# Controls whether data saving is synchronous (enforce fsync) or asynchronous. +# `true` for synchronous saving, ensuring data integrity at the cost of performance. +# `false` for asynchronous saving, improving performance but with delayed data writing. +enforce_fsync = true + +# Interval for running the message saver. +interval = "30s" + +# Personal access token configuration. +[personal_access_token] +# Sets the maximum number of active tokens allowed per user. +max_tokens_per_user = 100 + +# Personal access token cleaner configuration. +[personal_access_token.cleaner] +# Enables or disables the token cleaner process. +# `true` activates periodic token cleaning. +# `false` disables it, tokens remain active until manually revoked or expired. +enabled = true + +# Interval for running the token cleaner. +interval = "1m" + +# System configuration. +[system] +# Base path for system data storage. +path = "local_data" + +# Database configuration. +[system.database] +# Path for storing database files. +# Specifies the directory where database files are stored, relative to `system.path`. +path = "database" + +# Runtime configuration. +[system.runtime] +# Path for storing runtime data. +# Specifies the directory where any runtime data is stored, relative to `system.path`. +path = "runtime" + +# Logging configuration. +[system.logging] +# Path for storing log files. +path = "logs" + +# Level of logging detail. Options: "debug", "info", "warn", "error". +level = "trace" + +# Maximum size of the log files before rotation. +max_size = "512 MB" + +# Time to retain log files before deletion. +retention = "7 days" + +# Cache configuration. +[system.cache] +# Enables or disables the system cache. +# `true` activates caching for frequently accessed data. +# `false` disables caching, data is always read from the source. +enabled = true + +# Maximum size of the cache, e.g. "4GB". +size = "4GB" + +# Data retention policy configuration. +[system.retention_policy] +# Configures the message expiry setting. +# "disabled" means messages are kept indefinitely. +# A time value in human-readable format determines the lifespan of messages. +# Example: `message_expiry = "2 days 4 hours 15 minutes"` means messages will expire after that duration. +message_expiry = "disabled" + +# Maximum size of a topic, e.g., "10 GB". +max_topic_size = "10 GB" + +# Encryption configuration +[system.encryption] +# Determines whether server-side data encryption is enabled (boolean). +# `true` enables encryption for stored data using AES-256-GCM. +# `false` means data is stored without encryption. +enabled = false + +# The encryption key used when encryption is enabled (string). +# Should be a 32 bytes length key, provided as a base64 encoded string. +# This key is required and used only if encryption is enabled. +key = "" + +# Compression configuration +[system.compression] +# Allows overriding the default compression algorithm per data segment (boolean). +# `true` permits different compression algorithms for individual segments. +# `false` means all data segments use the default compression algorithm. +allow_override = false + +# The default compression algorithm used for data storage (string). +# "none" indicates no compression, other values can specify different algorithms. +default_algorithm = "none" + +# Stream configuration +[system.stream] +# Path for storing stream-related data (string). +# Specifies the directory where stream data is stored, relative to `system.path`. +path = "streams" + +# Topic configuration +[system.topic] +# Path for storing topic-related data (string). +# Specifies the directory where topic data is stored, relative to `stream.path`. +path = "topics" + +# Partition configuration +[system.partition] +# Path for storing partition-related data (string). +# Specifies the directory where partition data is stored, relative to `topic.path`. +path = "partitions" + +# Determines whether to enforce file synchronization on partition updates (boolean). +# `true` ensures immediate writing of data to disk for durability. +# `false` allows the OS to manage write operations, which can improve performance. +enforce_fsync = false + +# Enables checksum validation for data integrity (boolean). +# `true` activates CRC checks when loading data, guarding against corruption. +# `false` skips these checks for faster loading at the risk of undetected corruption. +validate_checksum = false + +# The threshold of buffered messages before triggering a save to disk (integer). +# Specifies how many messages accumulate before persisting to storage. +# Adjusting this can balance between write performance and data durability. +messages_required_to_save = 10_000 + +# Segment configuration +[system.segment] +# Defines the soft limit for the size of a storage segment. +# When a segment reaches this size, a new segment is created for subsequent data. +# Example: if `size` is set "1GB", the actual segment size may be 1GB + the size of remaining messages in received batch. +size = "1GB" + +# Controls whether to cache indexes for segment access (boolean). +# `true` keeps indexes in memory, speeding up data retrieval. +# `false` reads indexes from disk, which can conserve memory at the cost of access speed. +cache_indexes = true + +# Determines whether to cache time-based indexes for segments (boolean). +# `true` allows faster timestamp-based data retrieval by keeping indexes in memory. +# `false` conserves memory by reading time indexes from disk, which may slow down access. +cache_time_indexes = true + +# Message deduplication configuration +[system.message_deduplication] +# Controls whether message deduplication is enabled (boolean). +# `true` activates deduplication, ignoring messages with duplicate IDs. +# `false` treats each message as unique, even if IDs are duplicated. +enabled = false +# Maximum number of ID entries in the deduplication cache (u64). +max_entries = 1000 +# Maximum age of ID entries in the deduplication cache in human-readable format. +expiry = "1m" diff --git a/extra.txt b/extra.txt new file mode 100644 index 0000000..56dda24 --- /dev/null +++ b/extra.txt @@ -0,0 +1,87 @@ +RUST_LOG=debug cargo run -p tcptls 8080 +RUSTFLAGS="-Z threads=8" cargo +nightly build --release + +time RUSTFLAGS="-Z threads=8" cargo +nightly build --release +Finished release [optimized] target(s) in 23m 26s +real 23m26.801s +user 32m11.223s +sys 4m19.326s +sysctl -n machdep.cpu.brand_string + +hyperfine --runs 1 'RUSTFLAGS="-Z threads=8" cargo +nightly build --release' + +time cargo build --release +Finished release [optimized] target(s) in 43m 39s +real 43m37.079s +user 39m46.355s +sys 5m10.400s + +hyperfine --runs 1 'cargo build --release' + +RUST_LOG=debug cargo watch -q -c -w src/ -w .cargo/ -x "run -p tcptls 8080" + +echo -n -e "\x08\x00\x00\x00\x01\x00\x00\x00\" | nc 127.0.0.1 8090 + +for i in {1..100}; do echo '{"method":"isPrime","number":'$i'}' | nc localhost 8090; sleep 0.25; done; +for i in {1..10}; do curl http://0.0.0.0:3000; sleep 0.25; done; + +for i in {1..1000}; do curl http://0.0.0.0:3000; done; +for i in {1..100}; do + curl -X GET http://localhost:8080/ping & +done + + + +for i in {1..10}; do curl http://0.0.0.0:8080; sleep 0.25; done; +echo PING | nc localhost 8090 +for i in {1..10} +do + printf '= %.0s' {1..$i} + sleep $1s +done +curl -i -X GET -H "Origin: http://0.0.0.0:3001" http://0.0.0.0:3001 +curl -H "Origin: http://localhost:3000" -H "Access-Control-Request-Method: GET" -H "Access-Control-Request-Headers: X-Requested-With" -X OPTIONS --verbose http://localhost:3001/ + + +echo '{"method":"isPrime","number":42}' | nc localhost 8090 +{"method":"isPrime","prime":false} + +$ echo '{"method":"isPrime","number":13}' | nc localhost 8080 +{"method":"isPrime","prime":true} + +$ echo '{"method":"isPrime","number":13.43}' | nc localhost 8080 +{"method":"","prime":false} + +$ echo '{"method":"invalidMethod","number":13}' | nc localhost 8080 +{"method":"","prime":false} +``` +echo -e "GET /version HTTP/1.1\r\nHost: 192.168.64.12\r\n\r\n" | nc 192.168.64.12 1884 +nc 192.168.64.12 1884 +nc 127.0.0.1 8080 + +$ nano ~/Library/LaunchAgents/com.example.nsurlsessiond-monitor.plist +$ launchctl load ~/Library/LaunchAgents/com.example.nsurlsessiond-monitor.plist +$ launchctl unload ~/Library/LaunchAgents/com.example.nsurlsessiond-monitor.plist + +Connection Tracking: +Enable connection tracking in the iptables: +sudo modprobe nf_conntrack + +Rate Limiting: +Use the hashlimit module to rate limit incoming connections: +sudo iptables -A INPUT -p tcp --syn --dport 8090 -m conntrack --ctstate NEW -m hashlimit --hashlimit 50/s --hashlimit-burst 100 --hashlimit-mode srcip --hashlimit-name conn_limit -j ACCEPT +sudo iptables -A INPUT -p tcp --syn --dport 8090 -j DROP + +limit the number of concurrent connections from a single IP address +sudo iptables -A INPUT -p tcp --syn --dport 8090 -m connlimit --connlimit-above 10 --connlimit-mask 32 -j DROP + +$ ulimit -n +256 +$ ulimit -n + +$ sysctl kern.num_taskthreads +kern.num_taskthreads: 4096 +lsof -p PID + +sudo launchctl unload /Library/LaunchDaemons/com.canonical.multipassd.plist +sudo launchctl load -w /Library/LaunchDaemons/com.canonical.multipassd.plist diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..eb5b0e0 --- /dev/null +++ b/src/args.rs @@ -0,0 +1,8 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +pub struct Args { + #[arg(short, long, default_value = "file")] + pub config_provider: String, +} diff --git a/src/binary/command.rs b/src/binary/command.rs new file mode 100644 index 0000000..7b09490 --- /dev/null +++ b/src/binary/command.rs @@ -0,0 +1,104 @@ +// use crate::binary::handlers::consumer_groups::{ +// create_consumer_group_handler, delete_consumer_group_handler, get_consumer_group_handler, +// get_consumer_groups_handler, join_consumer_group_handler, leave_consumer_group_handler, +// }; +// use crate::binary::handlers::consumer_offsets::*; +// use crate::binary::handlers::messages::*; +// use crate::binary::handlers::partitions::*; +use crate::binary::handlers::personal_access_tokens::{ + create_personal_access_token_handler, delete_personal_access_token_handler, + get_personal_access_tokens_handler, login_with_personal_access_token_handler, +}; +// use crate::binary::handlers::streams::*; +use crate::binary::handlers::system::*; +// use crate::binary::handlers::topics::*; +use crate::binary::handlers::users::{ + change_password_handler, create_user_handler, delete_user_handler, get_user_handler, + get_users_handler, login_user_handler, logout_user_handler, update_permissions_handler, + update_user_handler, +}; +use crate::binary::sender::Sender; +use crate::infrastructure::error::Error; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::SharedSystem; +use crate::models::command::Command; +use tracing::debug; + +pub async fn handle( + command: &Command, + sender: &mut dyn Sender, + session: &Session, + system: SharedSystem, +) -> Result<(), Error> { + let result = try_handle(command, sender, session, &system).await; + if result.is_ok() { + debug!("Command was handled successfully, session: {session}.",); + return Ok(()); + } + + let error = result.err().unwrap(); + debug!("Command was not handled successfully, session: {session}, error: {error}.",); + sender.send_error_response(error).await?; + Ok(()) +} + +async fn try_handle( + command: &Command, + sender: &mut dyn Sender, + session: &Session, + system: &SharedSystem, +) -> Result<(), Error> { + debug!("Handling command '{command}', session: {session}..."); + match command { + Command::Ping(command) => ping_handler::handle(command, sender, session).await, + Command::GetStats(command) => { + get_stats_handler::handle(command, sender, session, system).await + } + Command::GetMe(command) => get_me_handler::handle(command, sender, session, system).await, + Command::GetClient(command) => { + get_client_handler::handle(command, sender, session, system).await + } + Command::GetClients(command) => { + get_clients_handler::handle(command, sender, session, system).await + } + Command::GetUser(command) => { + get_user_handler::handle(command, sender, session, system).await + } + Command::GetUsers(command) => { + get_users_handler::handle(command, sender, session, system).await + } + Command::CreateUser(command) => { + create_user_handler::handle(command, sender, session, system).await + } + Command::DeleteUser(command) => { + delete_user_handler::handle(command, sender, session, system).await + } + Command::UpdateUser(command) => { + update_user_handler::handle(command, sender, session, system).await + } + Command::UpdatePermissions(command) => { + update_permissions_handler::handle(command, sender, session, system).await + } + Command::ChangePassword(command) => { + change_password_handler::handle(command, sender, session, system).await + } + Command::LoginUser(command) => { + login_user_handler::handle(command, sender, session, system).await + } + Command::LogoutUser(command) => { + logout_user_handler::handle(command, sender, session, system).await + } + Command::GetPersonalAccessTokens(command) => { + get_personal_access_tokens_handler::handle(command, sender, session, system).await + } + Command::CreatePersonalAccessToken(command) => { + create_personal_access_token_handler::handle(command, sender, session, system).await + } + Command::DeletePersonalAccessToken(command) => { + delete_personal_access_token_handler::handle(command, sender, session, system).await + } + Command::LoginWithPersonalAccessToken(command) => { + login_with_personal_access_token_handler::handle(command, sender, session, system).await + } + } +} diff --git a/src/binary/handlers/mod.rs b/src/binary/handlers/mod.rs new file mode 100644 index 0000000..af88567 --- /dev/null +++ b/src/binary/handlers/mod.rs @@ -0,0 +1,8 @@ +// pub mod consumer_groups; +// pub mod consumer_offsets; +// pub mod messages; +// pub mod partitions; +pub mod personal_access_tokens; +// pub mod streams; +pub mod system; +pub mod users; diff --git a/src/binary/handlers/personal_access_tokens/create_personal_access_token_handler.rs b/src/binary/handlers/personal_access_tokens/create_personal_access_token_handler.rs new file mode 100644 index 0000000..b665f88 --- /dev/null +++ b/src/binary/handlers/personal_access_tokens/create_personal_access_token_handler.rs @@ -0,0 +1,24 @@ +use crate::binary::mapper; +use crate::binary::sender::Sender; +use crate::infrastructure::error::Error; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::SharedSystem; +use crate::models::personal_access_tokens::create_personal_access_token::CreatePersonalAccessToken; +use anyhow::Result; +use tracing::debug; + +pub async fn handle( + command: &CreatePersonalAccessToken, + sender: &mut dyn Sender, + session: &Session, + system: &SharedSystem, +) -> Result<(), Error> { + debug!("session: {session}, command: {command}"); + let system = system.read(); + let token = system + .create_personal_access_token(session, &command.name, command.expiry) + .await?; + let bytes = mapper::map_raw_pat(&token); + sender.send_ok_response(bytes.as_slice()).await?; + Ok(()) +} diff --git a/src/binary/handlers/personal_access_tokens/delete_personal_access_token_handler.rs b/src/binary/handlers/personal_access_tokens/delete_personal_access_token_handler.rs new file mode 100644 index 0000000..8284156 --- /dev/null +++ b/src/binary/handlers/personal_access_tokens/delete_personal_access_token_handler.rs @@ -0,0 +1,23 @@ +use crate::binary::sender::Sender; +use crate::infrastructure::error::Error; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::SharedSystem; +use crate::models::personal_access_tokens::delete_personal_access_token::DeletePersonalAccessToken; +// use crate::models::personal_access_tokens::create_personal_access_token::DeletePersonalAccessToken; +use anyhow::Result; +use tracing::debug; + +pub async fn handle( + command: &DeletePersonalAccessToken, + sender: &mut dyn Sender, + session: &Session, + system: &SharedSystem, +) -> Result<(), Error> { + debug!("session: {session}, command: {command}"); + let system = system.read(); + system + .delete_personal_access_token(session, &command.name) + .await?; + sender.send_empty_ok_response().await?; + Ok(()) +} diff --git a/src/binary/handlers/personal_access_tokens/get_personal_access_tokens_handler.rs b/src/binary/handlers/personal_access_tokens/get_personal_access_tokens_handler.rs new file mode 100644 index 0000000..d6874c3 --- /dev/null +++ b/src/binary/handlers/personal_access_tokens/get_personal_access_tokens_handler.rs @@ -0,0 +1,23 @@ +use crate::binary::mapper; +use crate::binary::sender::Sender; +use crate::infrastructure::error::Error; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::SharedSystem; +use crate::models::personal_access_tokens::get_personal_access_tokens::GetPersonalAccessTokens; +use tracing::log::debug; + +pub async fn handle( + command: &GetPersonalAccessTokens, + sender: &mut dyn Sender, + session: &Session, + system: &SharedSystem, +) -> Result<(), Error> { + debug!("session: {session}, command: {command}"); + let system = system.read(); + let personal_access_tokens = system.get_personal_access_tokens(session).await?; + let personal_access_tokens = mapper::map_personal_access_tokens(&personal_access_tokens); + sender + .send_ok_response(personal_access_tokens.as_slice()) + .await?; + Ok(()) +} diff --git a/src/binary/handlers/personal_access_tokens/login_with_personal_access_token_handler.rs b/src/binary/handlers/personal_access_tokens/login_with_personal_access_token_handler.rs new file mode 100644 index 0000000..7069683 --- /dev/null +++ b/src/binary/handlers/personal_access_tokens/login_with_personal_access_token_handler.rs @@ -0,0 +1,25 @@ +use crate::binary::mapper; +use crate::binary::sender::Sender; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::SharedSystem; +use crate::models::personal_access_tokens::login_with_personal_access_token::LoginWithPersonalAccessToken; +// use crate::models::personal_access_tokens::login_with_personal_access_token::LoginWithPersonalAccessToken; +use crate::infrastructure::error::Error; +use anyhow::Result; +use tracing::debug; + +pub async fn handle( + command: &LoginWithPersonalAccessToken, + sender: &mut dyn Sender, + session: &Session, + system: &SharedSystem, +) -> Result<(), Error> { + debug!("session: {session}, command: {command}"); + let system = system.read(); + let user = system + .login_with_personal_access_token(&command.token, Some(session)) + .await?; + let identity_info = mapper::map_identity_info(user.id); + sender.send_ok_response(identity_info.as_slice()).await?; + Ok(()) +} diff --git a/src/binary/handlers/personal_access_tokens/mod.rs b/src/binary/handlers/personal_access_tokens/mod.rs new file mode 100644 index 0000000..1b16e8b --- /dev/null +++ b/src/binary/handlers/personal_access_tokens/mod.rs @@ -0,0 +1,4 @@ +pub mod create_personal_access_token_handler; +pub mod delete_personal_access_token_handler; +pub mod get_personal_access_tokens_handler; +pub mod login_with_personal_access_token_handler; diff --git a/src/binary/handlers/system/get_client_handler.rs b/src/binary/handlers/system/get_client_handler.rs new file mode 100644 index 0000000..7cf9021 --- /dev/null +++ b/src/binary/handlers/system/get_client_handler.rs @@ -0,0 +1,27 @@ +use crate::binary::mapper; +use crate::binary::sender::Sender; +use crate::infrastructure::error::Error; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::SharedSystem; +use crate::models::system::get_client::GetClient; +use tracing::debug; + +pub async fn handle( + command: &GetClient, + sender: &mut dyn Sender, + session: &Session, + system: &SharedSystem, +) -> Result<(), Error> { + debug!("session: {session}, command: {command}"); + let bytes; + { + let system = system.read(); + let client = system.get_client(session, command.client_id).await?; + { + let client = client.read().await; + bytes = mapper::map_client(&client).await; + } + } + sender.send_ok_response(bytes.as_slice()).await?; + Ok(()) +} diff --git a/src/binary/handlers/system/get_clients_handler.rs b/src/binary/handlers/system/get_clients_handler.rs new file mode 100644 index 0000000..c09b2e7 --- /dev/null +++ b/src/binary/handlers/system/get_clients_handler.rs @@ -0,0 +1,21 @@ +use crate::binary::mapper; +use crate::binary::sender::Sender; +use crate::infrastructure::error::Error; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::SharedSystem; +use crate::models::system::get_clients::GetClients; +use tracing::debug; + +pub async fn handle( + command: &GetClients, + sender: &mut dyn Sender, + session: &Session, + system: &SharedSystem, +) -> Result<(), Error> { + debug!("session: {session}, command: {command}"); + let system = system.read(); + let clients = system.get_clients(session).await?; + let clients = mapper::map_clients(&clients).await; + sender.send_ok_response(clients.as_slice()).await?; + Ok(()) +} diff --git a/src/binary/handlers/system/get_me_handler.rs b/src/binary/handlers/system/get_me_handler.rs new file mode 100644 index 0000000..c8f471e --- /dev/null +++ b/src/binary/handlers/system/get_me_handler.rs @@ -0,0 +1,27 @@ +use crate::binary::mapper; +use crate::binary::sender::Sender; +use crate::infrastructure::error::Error; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::SharedSystem; +use crate::models::system::get_me::GetMe; +use tracing::debug; + +pub async fn handle( + command: &GetMe, + sender: &mut dyn Sender, + session: &Session, + system: &SharedSystem, +) -> Result<(), Error> { + debug!("session: {session}, command: {command}"); + let bytes; + { + let system = system.read(); + let client = system.get_client(session, session.client_id).await?; + { + let client = client.read().await; + bytes = mapper::map_client(&client).await; + } + } + sender.send_ok_response(bytes.as_slice()).await?; + Ok(()) +} diff --git a/src/binary/handlers/system/get_stats_handler.rs b/src/binary/handlers/system/get_stats_handler.rs new file mode 100644 index 0000000..e642035 --- /dev/null +++ b/src/binary/handlers/system/get_stats_handler.rs @@ -0,0 +1,21 @@ +use crate::binary::mapper; +use crate::binary::sender::Sender; +use crate::infrastructure::error::Error; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::SharedSystem; +use crate::models::system::get_stats::GetStats; +use tracing::debug; + +pub async fn handle( + command: &GetStats, + sender: &mut dyn Sender, + session: &Session, + system: &SharedSystem, +) -> Result<(), Error> { + debug!("session: {session}, command: {command}"); + let system = system.read(); + let stats = system.get_stats(session).await?; + let bytes = mapper::map_stats(&stats); + sender.send_ok_response(bytes.as_slice()).await?; + Ok(()) +} diff --git a/src/binary/handlers/system/mod.rs b/src/binary/handlers/system/mod.rs new file mode 100644 index 0000000..c3996fd --- /dev/null +++ b/src/binary/handlers/system/mod.rs @@ -0,0 +1,5 @@ +pub mod get_client_handler; +pub mod get_clients_handler; +pub mod get_me_handler; +pub mod get_stats_handler; +pub mod ping_handler; diff --git a/src/binary/handlers/system/ping_handler.rs b/src/binary/handlers/system/ping_handler.rs new file mode 100644 index 0000000..374981f --- /dev/null +++ b/src/binary/handlers/system/ping_handler.rs @@ -0,0 +1,16 @@ +use crate::binary::sender::Sender; +use crate::infrastructure::error::Error; +use crate::infrastructure::session::Session; +use crate::models::system::ping::Ping; +use anyhow::Result; +use tracing::debug; + +pub async fn handle( + command: &Ping, + sender: &mut dyn Sender, + session: &Session, +) -> Result<(), Error> { + debug!("session: {session}, command: {command}"); + sender.send_empty_ok_response().await?; + Ok(()) +} diff --git a/src/binary/handlers/users/change_password_handler.rs b/src/binary/handlers/users/change_password_handler.rs new file mode 100644 index 0000000..b255113 --- /dev/null +++ b/src/binary/handlers/users/change_password_handler.rs @@ -0,0 +1,27 @@ +use crate::binary::sender::Sender; +use crate::infrastructure::error::Error; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::SharedSystem; +use crate::models::users::change_password::ChangePassword; +use anyhow::Result; +use tracing::debug; + +pub async fn handle( + command: &ChangePassword, + sender: &mut dyn Sender, + session: &Session, + system: &SharedSystem, +) -> Result<(), Error> { + debug!("session: {session}, command: {command}"); + let system = system.read(); + system + .change_password( + session, + &command.user_id, + &command.current_password, + &command.new_password, + ) + .await?; + sender.send_empty_ok_response().await?; + Ok(()) +} diff --git a/src/binary/handlers/users/create_user_handler.rs b/src/binary/handlers/users/create_user_handler.rs new file mode 100644 index 0000000..2fbca81 --- /dev/null +++ b/src/binary/handlers/users/create_user_handler.rs @@ -0,0 +1,28 @@ +use crate::binary::sender::Sender; +use crate::infrastructure::error::Error; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::SharedSystem; +use crate::models::users::create_user::CreateUser; +use anyhow::Result; +use tracing::debug; + +pub async fn handle( + command: &CreateUser, + sender: &mut dyn Sender, + session: &Session, + system: &SharedSystem, +) -> Result<(), Error> { + debug!("session: {session}, command: {command}"); + let mut system = system.write(); + system + .create_user( + session, + &command.username, + &command.password, + command.status, + command.permissions.clone(), + ) + .await?; + sender.send_empty_ok_response().await?; + Ok(()) +} diff --git a/src/binary/handlers/users/delete_user_handler.rs b/src/binary/handlers/users/delete_user_handler.rs new file mode 100644 index 0000000..2bb065d --- /dev/null +++ b/src/binary/handlers/users/delete_user_handler.rs @@ -0,0 +1,20 @@ +use crate::binary::sender::Sender; +use crate::infrastructure::error::Error; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::SharedSystem; +use crate::models::users::delete_user::DeleteUser; +use anyhow::Result; +use tracing::debug; + +pub async fn handle( + command: &DeleteUser, + sender: &mut dyn Sender, + session: &Session, + system: &SharedSystem, +) -> Result<(), Error> { + debug!("session: {session}, command: {command}"); + let mut system = system.write(); + system.delete_user(session, &command.user_id).await?; + sender.send_empty_ok_response().await?; + Ok(()) +} diff --git a/src/binary/handlers/users/get_user_handler.rs b/src/binary/handlers/users/get_user_handler.rs new file mode 100644 index 0000000..9100426 --- /dev/null +++ b/src/binary/handlers/users/get_user_handler.rs @@ -0,0 +1,21 @@ +use crate::binary::mapper; +use crate::binary::sender::Sender; +use crate::infrastructure::error::Error; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::SharedSystem; +use crate::models::users::get_user::GetUser; +use tracing::log::debug; + +pub async fn handle( + command: &GetUser, + sender: &mut dyn Sender, + session: &Session, + system: &SharedSystem, +) -> Result<(), Error> { + debug!("session: {session}, command: {command}"); + let system = system.read(); + let user = system.find_user(session, &command.user_id).await?; + let bytes = mapper::map_user(&user); + sender.send_ok_response(bytes.as_slice()).await?; + Ok(()) +} diff --git a/src/binary/handlers/users/get_users_handler.rs b/src/binary/handlers/users/get_users_handler.rs new file mode 100644 index 0000000..2ec8415 --- /dev/null +++ b/src/binary/handlers/users/get_users_handler.rs @@ -0,0 +1,21 @@ +use crate::binary::mapper; +use crate::binary::sender::Sender; +use crate::infrastructure::error::Error; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::SharedSystem; +use crate::models::users::get_users::GetUsers; +use tracing::log::debug; + +pub async fn handle( + command: &GetUsers, + sender: &mut dyn Sender, + session: &Session, + system: &SharedSystem, +) -> Result<(), Error> { + debug!("session: {session}, command: {command}"); + let system = system.read(); + let users = system.get_users(session).await?; + let users = mapper::map_users(&users); + sender.send_ok_response(users.as_slice()).await?; + Ok(()) +} diff --git a/src/binary/handlers/users/login_user_handler.rs b/src/binary/handlers/users/login_user_handler.rs new file mode 100644 index 0000000..77a7c2a --- /dev/null +++ b/src/binary/handlers/users/login_user_handler.rs @@ -0,0 +1,24 @@ +use crate::binary::mapper; +use crate::binary::sender::Sender; +use crate::infrastructure::error::Error; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::SharedSystem; +use crate::models::users::login_user::LoginUser; +use anyhow::Result; +use tracing::debug; + +pub async fn handle( + command: &LoginUser, + sender: &mut dyn Sender, + session: &Session, + system: &SharedSystem, +) -> Result<(), Error> { + debug!("session: {session}, command: {command}"); + let system = system.read(); + let user = system + .login_user(&command.username, &command.password, Some(session)) + .await?; + let identity_info = mapper::map_identity_info(user.id); + sender.send_ok_response(identity_info.as_slice()).await?; + Ok(()) +} diff --git a/src/binary/handlers/users/logout_user_handler.rs b/src/binary/handlers/users/logout_user_handler.rs new file mode 100644 index 0000000..31c7a90 --- /dev/null +++ b/src/binary/handlers/users/logout_user_handler.rs @@ -0,0 +1,21 @@ +use crate::binary::sender::Sender; +use crate::infrastructure::error::Error; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::SharedSystem; +use crate::models::users::logout_user::LogoutUser; +use anyhow::Result; +use tracing::debug; + +pub async fn handle( + command: &LogoutUser, + sender: &mut dyn Sender, + session: &Session, + system: &SharedSystem, +) -> Result<(), Error> { + debug!("session: {session}, command: {command}"); + let system = system.read(); + system.logout_user(session).await?; + session.clear_user_id(); + sender.send_empty_ok_response().await?; + Ok(()) +} diff --git a/src/binary/handlers/users/mod.rs b/src/binary/handlers/users/mod.rs new file mode 100644 index 0000000..00a6082 --- /dev/null +++ b/src/binary/handlers/users/mod.rs @@ -0,0 +1,9 @@ +pub mod change_password_handler; +pub mod create_user_handler; +pub mod delete_user_handler; +pub mod get_user_handler; +pub mod get_users_handler; +pub mod login_user_handler; +pub mod logout_user_handler; +pub mod update_permissions_handler; +pub mod update_user_handler; diff --git a/src/binary/handlers/users/update_permissions_handler.rs b/src/binary/handlers/users/update_permissions_handler.rs new file mode 100644 index 0000000..713d5d1 --- /dev/null +++ b/src/binary/handlers/users/update_permissions_handler.rs @@ -0,0 +1,22 @@ +use crate::binary::sender::Sender; +use crate::infrastructure::error::Error; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::SharedSystem; +use crate::models::users::update_permissions::UpdatePermissions; +use anyhow::Result; +use tracing::debug; + +pub async fn handle( + command: &UpdatePermissions, + sender: &mut dyn Sender, + session: &Session, + system: &SharedSystem, +) -> Result<(), Error> { + debug!("session: {session}, command: {command}"); + let mut system = system.write(); + system + .update_permissions(session, &command.user_id, command.permissions.clone()) + .await?; + sender.send_empty_ok_response().await?; + Ok(()) +} diff --git a/src/binary/handlers/users/update_user_handler.rs b/src/binary/handlers/users/update_user_handler.rs new file mode 100644 index 0000000..770aa55 --- /dev/null +++ b/src/binary/handlers/users/update_user_handler.rs @@ -0,0 +1,27 @@ +use crate::binary::sender::Sender; +use crate::infrastructure::error::Error; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::SharedSystem; +use crate::models::users::update_user::UpdateUser; +use anyhow::Result; +use tracing::debug; + +pub async fn handle( + command: &UpdateUser, + sender: &mut dyn Sender, + session: &Session, + system: &SharedSystem, +) -> Result<(), Error> { + debug!("session: {session}, command: {command}"); + let system = system.read(); + system + .update_user( + session, + &command.user_id, + command.username.clone(), + command.status, + ) + .await?; + sender.send_empty_ok_response().await?; + Ok(()) +} diff --git a/src/binary/mapper.rs b/src/binary/mapper.rs new file mode 100644 index 0000000..d401863 --- /dev/null +++ b/src/binary/mapper.rs @@ -0,0 +1,266 @@ +use crate::infrastructure::clients::client_manager::{Client, Transport}; +// use crate::streaming::models::messages::PolledMessages; +// use crate::streaming::partitions::partition::Partition; +use crate::infrastructure::personal_access_tokens::personal_access_token::PersonalAccessToken; +use crate::infrastructure::users::user::User; +// use crate::streaming::streams::stream::Stream; +// use crate::streaming::topics::consumer_group::ConsumerGroup; +// use crate::streaming::topics::topic::Topic; +// use crate::infrastructure::models::users::user::User; +use crate::models::bytes_serializable::BytesSerializable; +use bytes::BufMut; +// use iggy::models::consumer_offset_info::ConsumerOffsetInfo; +use crate::models::stats::Stats; +use crate::models::user_info::UserId; +use std::sync::Arc; +use tokio::sync::RwLock; + +pub fn map_stats(stats: &Stats) -> Vec { + let mut bytes = Vec::with_capacity(104); + bytes.put_u32_le(stats.process_id); + bytes.put_f32_le(stats.cpu_usage); + bytes.put_u64_le(stats.memory_usage.as_bytes_u64()); + bytes.put_u64_le(stats.total_memory.as_bytes_u64()); + bytes.put_u64_le(stats.available_memory.as_bytes_u64()); + bytes.put_u64_le(stats.run_time); + bytes.put_u64_le(stats.start_time); + bytes.put_u64_le(stats.read_bytes.as_bytes_u64()); + bytes.put_u64_le(stats.written_bytes.as_bytes_u64()); + // bytes.put_u64_le(stats.messages_size_bytes); + // bytes.put_u32_le(stats.streams_count); + // bytes.put_u32_le(stats.topics_count); + // bytes.put_u32_le(stats.partitions_count); + // bytes.put_u32_le(stats.segments_count); + // bytes.put_u64_le(stats.messages_count); + bytes.put_u32_le(stats.clients_count); + // bytes.put_u32_le(stats.consumer_groups_count); + bytes.put_u32_le(stats.hostname.len() as u32); + bytes.extend(stats.hostname.as_bytes()); + bytes.put_u32_le(stats.os_name.len() as u32); + bytes.extend(stats.os_name.as_bytes()); + bytes.put_u32_le(stats.os_version.len() as u32); + bytes.extend(stats.os_version.as_bytes()); + bytes.put_u32_le(stats.kernel_version.len() as u32); + bytes.extend(stats.kernel_version.as_bytes()); + bytes +} + +// pub fn map_consumer_offset(offset: &ConsumerOffsetInfo) -> Vec { +// let mut bytes = Vec::with_capacity(20); +// bytes.put_u32_le(offset.partition_id); +// bytes.put_u64_le(offset.current_offset); +// bytes.put_u64_le(offset.stored_offset); +// bytes +// } + +pub async fn map_client(client: &Client) -> Vec { + let mut bytes = Vec::new(); + extend_client(client, &mut bytes); + // for consumer_group in &client.consumer_groups { + // bytes.put_u32_le(consumer_group.stream_id); + // bytes.put_u32_le(consumer_group.topic_id); + // bytes.put_u32_le(consumer_group.consumer_group_id); + // } + bytes +} + +pub async fn map_clients(clients: &[Arc>]) -> Vec { + let mut bytes = Vec::new(); + for client in clients { + let client = client.read().await; + extend_client(&client, &mut bytes); + } + bytes +} + +pub fn map_user(user: &User) -> Vec { + let mut bytes = Vec::new(); + extend_user(user, &mut bytes); + if let Some(permissions) = &user.permissions { + bytes.put_u8(1); + let permissions = permissions.as_bytes(); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u32_le(permissions.len() as u32); + bytes.extend(permissions); + } else { + bytes.put_u32_le(0); + } + bytes +} + +pub fn map_users(users: &[User]) -> Vec { + let mut bytes = Vec::new(); + for user in users { + extend_user(user, &mut bytes); + } + bytes +} + +pub fn map_identity_info(user_id: UserId) -> Vec { + let mut bytes = Vec::with_capacity(4); + bytes.put_u32_le(user_id); + bytes +} + +pub fn map_raw_pat(token: &str) -> Vec { + let mut bytes = Vec::with_capacity(1 + token.len()); + bytes.put_u8(token.len() as u8); + bytes.extend(token.as_bytes()); + bytes +} + +pub fn map_personal_access_tokens(personal_access_tokens: &[PersonalAccessToken]) -> Vec { + let mut bytes = Vec::new(); + for personal_access_token in personal_access_tokens { + extend_pat(personal_access_token, &mut bytes); + } + bytes +} + +// pub fn map_polled_messages(polled_messages: &PolledMessages) -> Vec { +// let messages_count = polled_messages.messages.len() as u32; +// let messages_size = polled_messages +// .messages +// .iter() +// .map(|message| message.get_size_bytes()) +// .sum::(); + +// let mut bytes = Vec::with_capacity(20 + messages_size as usize); +// bytes.put_u32_le(polled_messages.partition_id); +// bytes.put_u64_le(polled_messages.current_offset); +// bytes.put_u32_le(messages_count); +// for message in polled_messages.messages.iter() { +// message.extend(&mut bytes); +// } + +// bytes +// } + +// pub async fn map_stream(stream: &Stream) -> Vec { +// let mut bytes = Vec::new(); +// extend_stream(stream, &mut bytes).await; +// for topic in stream.get_topics() { +// extend_topic(topic, &mut bytes).await; +// } +// bytes +// } + +// pub async fn map_streams(streams: &[&Stream]) -> Vec { +// let mut bytes = Vec::new(); +// for stream in streams { +// extend_stream(stream, &mut bytes).await; +// } +// bytes +// } + +// pub async fn map_topics(topics: &[&Topic]) -> Vec { +// let mut bytes = Vec::new(); +// for topic in topics { +// extend_topic(topic, &mut bytes).await; +// } +// bytes +// } + +// pub async fn map_topic(topic: &Topic) -> Vec { +// let mut bytes = Vec::new(); +// extend_topic(topic, &mut bytes).await; +// for partition in topic.get_partitions() { +// let partition = partition.read().await; +// extend_partition(&partition, &mut bytes); +// } +// bytes +// } + +// pub async fn map_consumer_group(consumer_group: &ConsumerGroup) -> Vec { +// let mut bytes = Vec::new(); +// extend_consumer_group(consumer_group, &mut bytes); +// let members = consumer_group.get_members(); +// for member in members { +// let member = member.read().await; +// bytes.put_u32_le(member.id); +// let partitions = member.get_partitions(); +// bytes.put_u32_le(partitions.len() as u32); +// for partition in partitions { +// bytes.put_u32_le(partition); +// } +// } +// bytes +// } + +// pub async fn map_consumer_groups(consumer_groups: &[&RwLock]) -> Vec { +// let mut bytes = Vec::new(); +// for consumer_group in consumer_groups { +// let consumer_group = consumer_group.read().await; +// extend_consumer_group(&consumer_group, &mut bytes); +// } +// bytes +// } + +// async fn extend_stream(stream: &Stream, bytes: &mut Vec) { +// bytes.put_u32_le(stream.stream_id); +// bytes.put_u64_le(stream.created_at); +// bytes.put_u32_le(stream.get_topics().len() as u32); +// bytes.put_u64_le(stream.get_size_bytes().await); +// bytes.put_u64_le(stream.get_messages_count().await); +// bytes.put_u8(stream.name.len() as u8); +// bytes.extend(stream.name.as_bytes()); +// } + +// async fn extend_topic(topic: &Topic, bytes: &mut Vec) { +// bytes.put_u32_le(topic.topic_id); +// bytes.put_u64_le(topic.created_at); +// bytes.put_u32_le(topic.get_partitions().len() as u32); +// match topic.message_expiry { +// Some(message_expiry) => bytes.put_u32_le(message_expiry), +// None => bytes.put_u32_le(0), +// }; +// bytes.put_u64_le(topic.get_size_bytes().await); +// bytes.put_u64_le(topic.get_messages_count().await); +// bytes.put_u8(topic.name.len() as u8); +// bytes.extend(topic.name.as_bytes()); +// } + +// fn extend_partition(partition: &Partition, bytes: &mut Vec) { +// bytes.put_u32_le(partition.partition_id); +// bytes.put_u64_le(partition.created_at); +// bytes.put_u32_le(partition.get_segments().len() as u32); +// bytes.put_u64_le(partition.current_offset); +// bytes.put_u64_le(partition.get_size_bytes()); +// bytes.put_u64_le(partition.get_messages_count()); +// } + +// fn extend_consumer_group(consumer_group: &ConsumerGroup, bytes: &mut Vec) { +// bytes.put_u32_le(consumer_group.consumer_group_id); +// bytes.put_u32_le(consumer_group.partitions_count); +// bytes.put_u32_le(consumer_group.get_members().len() as u32); +// bytes.put_u8(consumer_group.name.len() as u8); +// bytes.extend(consumer_group.name.as_bytes()); +// } + +fn extend_client(client: &Client, bytes: &mut Vec) { + bytes.put_u32_le(client.client_id); + bytes.put_u32_le(client.user_id.unwrap_or(0)); + let transport: u8 = match client.transport { + Transport::Tcp => 1, + Transport::Quic => 2, + }; + bytes.put_u8(transport); + let address = client.address.to_string(); + bytes.put_u32_le(address.len() as u32); + bytes.extend(address.as_bytes()); + // bytes.put_u32_le(client.consumer_groups.len() as u32); +} + +fn extend_user(user: &User, bytes: &mut Vec) { + bytes.put_u32_le(user.id); + bytes.put_u64_le(user.created_at); + bytes.put_u8(user.status.as_code()); + bytes.put_u8(user.username.len() as u8); + bytes.extend(user.username.as_bytes()); +} + +fn extend_pat(personal_access_token: &PersonalAccessToken, bytes: &mut Vec) { + bytes.put_u8(personal_access_token.name.len() as u8); + bytes.extend(personal_access_token.name.as_bytes()); + bytes.put_u64_le(personal_access_token.expiry.unwrap_or(0)); +} diff --git a/src/binary/mod.rs b/src/binary/mod.rs new file mode 100644 index 0000000..a53b99b --- /dev/null +++ b/src/binary/mod.rs @@ -0,0 +1,4 @@ +pub mod command; +pub mod handlers; +pub mod mapper; +pub mod sender; diff --git a/src/binary/sender.rs b/src/binary/sender.rs new file mode 100644 index 0000000..adc1739 --- /dev/null +++ b/src/binary/sender.rs @@ -0,0 +1,10 @@ +use crate::infrastructure::error::Error; +use async_trait::async_trait; + +#[async_trait] +pub trait Sender: Sync + Send { + async fn read(&mut self, buffer: &mut [u8]) -> Result; + async fn send_empty_ok_response(&mut self) -> Result<(), Error>; + async fn send_ok_response(&mut self, payload: &[u8]) -> Result<(), Error>; + async fn send_error_response(&mut self, error: Error) -> Result<(), Error>; +} diff --git a/src/build.rs b/src/build.rs new file mode 100644 index 0000000..ba0890a --- /dev/null +++ b/src/build.rs @@ -0,0 +1,16 @@ +use std::error; +use vergen::EmitBuilder; + +fn main() -> Result<(), Box> { + if option_env!("DEV_CI_BUILD") == Some("true") { + EmitBuilder::builder() + .all_build() + .all_cargo() + .all_git() + .all_rustc() + .emit()?; + } else { + println!("cargo:info=Skipping build script because CI environment variable DEV_CI_BUILD is not set to 'true'"); + } + Ok(()) +} diff --git a/src/channels/commands/clean_messages.rs b/src/channels/commands/clean_messages.rs new file mode 100644 index 0000000..bbebe68 --- /dev/null +++ b/src/channels/commands/clean_messages.rs @@ -0,0 +1,176 @@ +use crate::streaming::systems::system::SharedSystem; +use crate::streaming::topics::topic::Topic; +use crate::{channels::server_command::ServerCommand, configs::server::MessageCleanerConfig}; +use async_trait::async_trait; +use flume::Sender; +use iggy::error::IggyError; +use iggy::utils::duration::IggyDuration; +use iggy::utils::timestamp::IggyTimestamp; +use tokio::time; +use tracing::{error, info}; + +struct DeletedSegments { + pub segments_count: u32, + pub messages_count: u64, +} + +pub struct MessagesCleaner { + enabled: bool, + interval: IggyDuration, + sender: Sender, +} + +#[derive(Debug, Default, Clone)] +pub struct CleanMessagesCommand; + +#[derive(Debug, Default, Clone)] +pub struct CleanMessagesExecutor; + +impl MessagesCleaner { + pub fn new(config: &MessageCleanerConfig, sender: Sender) -> Self { + Self { + enabled: config.enabled, + interval: config.interval, + sender, + } + } + + pub fn start(&self) { + if !self.enabled { + info!("Message cleaner is disabled."); + return; + } + + let interval = self.interval; + let sender = self.sender.clone(); + info!( + "Message cleaner is enabled, expired messages will be deleted every: {:?}.", + interval + ); + + tokio::spawn(async move { + let mut interval_timer = time::interval(interval.get_duration()); + loop { + interval_timer.tick().await; + sender.send(CleanMessagesCommand).unwrap_or_else(|err| { + error!("Failed to send CleanMessagesCommand. Error: {}", err); + }); + } + }); + } +} + +#[async_trait] +impl ServerCommand for CleanMessagesExecutor { + async fn execute(&mut self, system: &SharedSystem, _command: CleanMessagesCommand) { + let now = IggyTimestamp::now().to_micros(); + let system_read = system.read(); + let streams = system_read.get_streams(); + for stream in streams { + let topics = stream.get_topics(); + for topic in topics { + let deleted_segments = delete_expired_segments(topic, now).await; + if let Ok(Some(deleted_segments)) = deleted_segments { + info!( + "Deleted {} segments and {} messages for stream ID: {}, topic ID: {}", + deleted_segments.segments_count, + deleted_segments.messages_count, + topic.stream_id, + topic.topic_id + ); + + system + .write() + .metrics + .decrement_segments(deleted_segments.segments_count); + system + .write() + .metrics + .decrement_messages(deleted_segments.messages_count); + } + } + } + } + + fn start_command_sender( + &mut self, + _system: SharedSystem, + config: &crate::configs::server::ServerConfig, + sender: Sender, + ) { + let messages_cleaner = MessagesCleaner::new(&config.message_cleaner, sender); + messages_cleaner.start(); + } + + fn start_command_consumer( + mut self, + system: SharedSystem, + _config: &crate::configs::server::ServerConfig, + receiver: flume::Receiver, + ) { + tokio::spawn(async move { + let system = system.clone(); + while let Ok(command) = receiver.recv_async().await { + self.execute(&system, command).await; + } + info!("Messages cleaner receiver stopped."); + }); + } +} + +async fn delete_expired_segments( + topic: &Topic, + now: u64, +) -> Result, IggyError> { + let expired_segments = topic + .get_expired_segments_start_offsets_per_partition(now) + .await; + if expired_segments.is_empty() { + info!( + "No expired segments found for stream ID: {}, topic ID: {}", + topic.stream_id, topic.topic_id + ); + return Ok(None); + } + + info!( + "Found {} expired segments for stream ID: {}, topic ID: {}, deleting...", + expired_segments.len(), + topic.stream_id, + topic.topic_id + ); + + let mut segments_count = 0; + let mut messages_count = 0; + for (partition_id, start_offsets) in &expired_segments { + match topic.get_partition(*partition_id) { + Ok(partition) => { + let mut partition = partition.write().await; + let mut last_end_offset = 0; + for start_offset in start_offsets { + let deleted_segment = partition.delete_segment(*start_offset).await?; + last_end_offset = deleted_segment.end_offset; + segments_count += 1; + messages_count += deleted_segment.messages_count; + } + + if partition.get_segments().is_empty() { + let start_offset = last_end_offset + 1; + partition.add_persisted_segment(start_offset).await?; + } + } + Err(error) => { + error!( + "Partition with ID: {} not found for stream ID: {}, topic ID: {}. Error: {}", + partition_id, topic.stream_id, topic.topic_id, error + ); + continue; + } + } + } + + Ok(Some(DeletedSegments { + segments_count, + messages_count, + })) +} diff --git a/src/channels/commands/clean_personal_access_tokens.rs b/src/channels/commands/clean_personal_access_tokens.rs new file mode 100644 index 0000000..8fd9da6 --- /dev/null +++ b/src/channels/commands/clean_personal_access_tokens.rs @@ -0,0 +1,149 @@ +use crate::{ + channels::server_command::ServerCommand, configs::server::PersonalAccessTokenCleanerConfig, + infrastructure::systems::system::SharedSystem, utils::duration::IggyDuration, +}; +// use crate::configs::server::PersonalAccessTokenCleanerConfig; +// use crate::streaming::systems::system::SharedSystem; +use async_trait::async_trait; +use flume::Sender; +// use iggy::utils::duration::IggyDuration; +use crate::utils::timestamp::NigigTimeStamp; +use tokio::time; +use tracing::{debug, error, info}; + +pub struct PersonalAccessTokenCleaner { + enabled: bool, + interval: IggyDuration, + sender: Sender, +} + +#[derive(Debug, Default, Clone)] +pub struct CleanPersonalAccessTokensCommand; + +#[derive(Debug, Default, Clone)] +pub struct CleanPersonalAccessTokensExecutor; + +impl PersonalAccessTokenCleaner { + pub fn new( + config: &PersonalAccessTokenCleanerConfig, + sender: Sender, + ) -> Self { + Self { + enabled: config.enabled, + interval: config.interval, + sender, + } + } + + pub fn start(&self) { + if !self.enabled { + info!("Personal access token cleaner is disabled."); + return; + } + + let interval = self.interval; + let sender = self.sender.clone(); + info!( + "Personal access token cleaner is enabled, expired tokens will be deleted every: {:?}.", + interval + ); + + tokio::spawn(async move { + let mut interval_timer = time::interval(interval.get_duration()); + loop { + interval_timer.tick().await; + sender + .send(CleanPersonalAccessTokensCommand) + .unwrap_or_else(|error| { + error!( + "Failed to send CleanPersonalAccessTokensCommand. Error: {}", + error + ); + }); + } + }); + } +} + +#[async_trait] +impl ServerCommand for CleanPersonalAccessTokensExecutor { + async fn execute(&mut self, system: &SharedSystem, _command: CleanPersonalAccessTokensCommand) { + let system = system.read(); + let tokens = system.storage.personal_access_token.load_all().await; + if tokens.is_err() { + error!("Failed to load personal access tokens: {:?}", tokens); + return; + } + + let tokens = tokens.unwrap(); + if tokens.is_empty() { + debug!("No personal access tokens to delete."); + return; + } + + let now = NigigTimeStamp::now().to_micros(); + let expired_tokens = tokens + .into_iter() + .filter(|token| token.is_expired(now)) + .collect::>(); + + if expired_tokens.is_empty() { + debug!("No expired personal access tokens to delete."); + return; + } + + let expired_tokens_count = expired_tokens.len(); + let mut deleted_tokens_count = 0; + debug!("Found {expired_tokens_count} expired personal access tokens."); + for token in expired_tokens { + let result = system + .storage + .personal_access_token + .delete_for_user(token.user_id, &token.name) + .await; + if result.is_err() { + error!( + "Failed to delete personal access token: {} for user with ID: {}. Error: {:?}", + token.name, + token.user_id, + result.err().unwrap() + ); + continue; + } + + deleted_tokens_count += 1; + debug!( + "Deleted personal access token: {} for user with ID: {}.", + token.name, token.user_id + ); + } + + info!("Deleted {deleted_tokens_count} expired personal access tokens."); + } + + fn start_command_sender( + &mut self, + _system: SharedSystem, + config: &crate::configs::server::ServerConfig, + sender: Sender, + ) { + let personal_access_token_cleaner = + PersonalAccessTokenCleaner::new(&config.personal_access_token.cleaner, sender); + personal_access_token_cleaner.start(); + } + + fn start_command_consumer( + mut self, + system: SharedSystem, + _config: &crate::configs::server::ServerConfig, + receiver: flume::Receiver, + ) { + tokio::spawn(async move { + let system = system.clone(); + while let Ok(command) = receiver.recv_async().await { + self.execute(&system, command).await; + } + info!("Personal access token cleaner receiver stopped."); + }); + } +} diff --git a/src/channels/commands/mod.rs b/src/channels/commands/mod.rs new file mode 100644 index 0000000..52304f5 --- /dev/null +++ b/src/channels/commands/mod.rs @@ -0,0 +1,3 @@ +// pub mod clean_messages; +pub mod clean_personal_access_tokens; +// pub mod save_messages; diff --git a/src/channels/commands/save_messages.rs b/src/channels/commands/save_messages.rs new file mode 100644 index 0000000..846b38e --- /dev/null +++ b/src/channels/commands/save_messages.rs @@ -0,0 +1,98 @@ +use crate::channels::server_command::ServerCommand; +use crate::configs::server::MessageSaverConfig; +use crate::configs::server::ServerConfig; +use crate::streaming::systems::system::SharedSystem; +use async_trait::async_trait; +use flume::{Receiver, Sender}; +use iggy::utils::duration::IggyDuration; +use tokio::time; +use tracing::{error, info, warn}; + +pub struct MessagesSaver { + enforce_fsync: bool, + interval: IggyDuration, + sender: Sender, +} + +#[derive(Debug, Default, Clone)] +pub struct SaveMessagesCommand { + pub enforce_fsync: bool, +} + +#[derive(Debug, Default, Clone)] +pub struct SaveMessagesExecutor; + +impl MessagesSaver { + pub fn new(config: &MessageSaverConfig, sender: Sender) -> Self { + Self { + enforce_fsync: config.enforce_fsync, + interval: config.interval, + sender, + } + } + + pub fn start(&self) { + if !self.enforce_fsync { + info!("Message saver is disabled."); + return; + } + + let enforce_fsync = self.enforce_fsync; + let interval = self.interval; + let sender = self.sender.clone(); + info!( + "Message saver is enabled, buffered messages will be automatically saved every: {:?}, enforce fsync: {:?}.", + interval, enforce_fsync + ); + + tokio::spawn(async move { + let mut interval_timer = time::interval(interval.get_duration()); + loop { + interval_timer.tick().await; + let command = SaveMessagesCommand { enforce_fsync }; + sender.send(command).unwrap_or_else(|error| { + error!("Failed to send SaveMessagesCommand. Error: {}", error); + }); + } + }); + } +} + +#[async_trait] +impl ServerCommand for SaveMessagesExecutor { + async fn execute(&mut self, system: &SharedSystem, _command: SaveMessagesCommand) { + system + .read() + .persist_messages() + .await + .unwrap_or_else(|error| { + error!("Couldn't save buffered messages on disk. Error: {}", error); + }); + info!("Buffered messages saved on disk."); + } + + fn start_command_sender( + &mut self, + _system: SharedSystem, + config: &ServerConfig, + sender: Sender, + ) { + let messages_saver = MessagesSaver::new(&config.message_saver, sender); + messages_saver.start(); + } + + fn start_command_consumer( + mut self, + system: SharedSystem, + _config: &ServerConfig, + receiver: Receiver, + ) { + tokio::spawn(async move { + let system = system.clone(); + while let Ok(command) = receiver.recv_async().await { + self.execute(&system, command).await; + } + warn!("Server command handler stopped receiving commands."); + }); + } +} diff --git a/src/channels/handler.rs b/src/channels/handler.rs new file mode 100644 index 0000000..3dff12c --- /dev/null +++ b/src/channels/handler.rs @@ -0,0 +1,32 @@ +// use super::server_command::ServerCommand; +// use crate::configs::server::ServerConfig; +// use crate::streaming::systems::system::SharedSystem; + +use crate::{configs::server::ServerConfig, infrastructure::systems::system::SharedSystem}; + +use super::server_command::ServerCommand; + +pub struct ServerCommandHandler<'a> { + system: SharedSystem, + config: &'a ServerConfig, +} + +impl<'a> ServerCommandHandler<'a> { + pub fn new(system: SharedSystem, config: &'a ServerConfig) -> Self { + Self { system, config } + } + + pub fn install_handler(&mut self, mut executor: E) -> Self + where + E: ServerCommand + Send + Sync + 'static, + { + let (sender, receiver) = flume::unbounded(); + let system = self.system.clone(); + executor.start_command_sender(system.clone(), self.config, sender); + executor.start_command_consumer(system.clone(), self.config, receiver); + Self { + system, + config: self.config, + } + } +} diff --git a/src/channels/mod.rs b/src/channels/mod.rs new file mode 100644 index 0000000..aece661 --- /dev/null +++ b/src/channels/mod.rs @@ -0,0 +1,3 @@ +pub mod commands; +pub mod handler; +pub mod server_command; diff --git a/src/channels/server_command.rs b/src/channels/server_command.rs new file mode 100644 index 0000000..b5f6556 --- /dev/null +++ b/src/channels/server_command.rs @@ -0,0 +1,24 @@ +// use crate::configs::server::ServerConfig; +// use crate::streaming::systems::system::SharedSystem; +use crate::{configs::server::ServerConfig, infrastructure::systems::system::SharedSystem}; +use async_trait::async_trait; +use flume::{Receiver, Sender}; + +#[async_trait] +pub trait ServerCommand { + async fn execute(&mut self, system: &SharedSystem, command: C); + + fn start_command_sender( + &mut self, + system: SharedSystem, + config: &ServerConfig, + sender: Sender, + ); + + fn start_command_consumer( + self, + system: SharedSystem, + config: &ServerConfig, + receiver: Receiver, + ); +} diff --git a/src/configs/config_provider.rs b/src/configs/config_provider.rs new file mode 100644 index 0000000..e2920e1 --- /dev/null +++ b/src/configs/config_provider.rs @@ -0,0 +1,266 @@ +use crate::configs::server::ServerConfig; +use crate::server_error::ServerError; +use async_trait::async_trait; +use figment::{ + providers::{Format, Json, Toml}, + value::{Dict, Map as FigmentMap, Tag, Value as FigmentValue}, + Error, Figment, Metadata, Profile, Provider, +}; +use std::{env, path::Path}; +use toml::{map::Map, Value as TomlValue}; +use tracing::info; + +const DEFAULT_CONFIG_PROVIDER: &str = "file"; +const DEFAULT_CONFIG_PATH: &str = "configs/server.toml"; +// const DEFAULT_CONFIG_PATH: &str = "configs/server.json"; + +#[async_trait] +pub trait ConfigProvider { + async fn load_config(&self) -> Result; +} + +#[derive(Debug)] +pub struct FileConfigProvider { + path: String, +} + +pub struct CustomEnvProvider { + prefix: String, +} + +impl FileConfigProvider { + pub fn new(path: String) -> Self { + Self { path } + } +} + +impl CustomEnvProvider { + pub fn new(prefix: &str) -> Self { + Self { + prefix: prefix.to_string(), + } + } + + fn walk_toml_table_to_dict(prefix: &str, table: Map, dict: &mut Dict) { + for (key, value) in table { + let new_prefix = if prefix.is_empty() { + key.clone() + } else { + format!("{}.{}", prefix, key) + }; + match value { + TomlValue::Table(inner_table) => { + let mut nested_dict = Dict::new(); + Self::walk_toml_table_to_dict(&new_prefix, inner_table, &mut nested_dict); + dict.insert(key, FigmentValue::from(nested_dict)); + } + _ => { + dict.insert(key, Self::toml_to_figment_value(&value)); + } + } + } + } + + fn insert_overridden_values_from_env( + source: &Dict, + target: &mut Dict, + keys: Vec, + value: FigmentValue, + ) { + if keys.is_empty() { + return; + } + + let mut current_source = source; + let mut current_target = target; + + for i in 0..keys.len() { + let combined_keys = keys[i..].join("_"); + + if current_source.contains_key(&combined_keys) { + current_target.insert(combined_keys, value.clone()); + return; + } + + let key = &keys[i]; + match current_source.get(key) { + Some(FigmentValue::Dict(_, inner_source_dict)) => { + if !current_target.contains_key(key) { + current_target + .insert(key.clone(), FigmentValue::Dict(Tag::Default, Dict::new())); + } + + if let Some(FigmentValue::Dict(_, ref mut actual_inner_target_dict)) = + current_target.get_mut(key) + { + current_source = inner_source_dict; + current_target = actual_inner_target_dict; + } else { + return; + } + } + _ => return, + } + } + } + + fn toml_to_figment_value(toml_value: &TomlValue) -> FigmentValue { + match toml_value { + TomlValue::String(s) => FigmentValue::from(s.clone()), + TomlValue::Integer(i) => FigmentValue::from(*i), + TomlValue::Float(f) => FigmentValue::from(*f), + TomlValue::Boolean(b) => FigmentValue::from(*b), + TomlValue::Array(arr) => { + let vec: Vec = arr.iter().map(Self::toml_to_figment_value).collect(); + FigmentValue::from(vec) + } + TomlValue::Table(tbl) => { + let mut dict = figment::value::Dict::new(); + for (key, value) in tbl.iter() { + dict.insert(key.clone(), Self::toml_to_figment_value(value)); + } + FigmentValue::from(dict) + } + TomlValue::Datetime(_) => todo!("not implemented yet!"), + } + } + + fn try_parse_value(value: &str) -> FigmentValue { + if value == "true" { + return FigmentValue::from(true); + } + if value == "false" { + return FigmentValue::from(false); + } + if let Ok(int_val) = value.parse::() { + return FigmentValue::from(int_val); + } + if let Ok(float_val) = value.parse::() { + return FigmentValue::from(float_val); + } + FigmentValue::from(value) + } +} + +impl Provider for CustomEnvProvider { + fn metadata(&self) -> Metadata { + Metadata::named("nigig-server config") + } + + fn data(&self) -> Result, Error> { + let default_config = toml::to_string(&ServerConfig::default()) + .expect("Cannot serialize default ServerConfig. Something's terribly wrong."); + let toml_value: TomlValue = toml::from_str(&default_config).unwrap(); + let mut source_dict = Dict::new(); + if let TomlValue::Table(table) = toml_value { + Self::walk_toml_table_to_dict("", table, &mut source_dict); + } + + let mut new_dict = Dict::new(); + for (key, value) in env::vars() { + let env_key = key.to_uppercase(); + if !env_key.starts_with(self.prefix.as_str()) { + continue; + } + let keys: Vec = env_key[self.prefix.len()..] + .split('_') + .map(|k| k.to_lowercase()) + .collect(); + let env_var_value = Self::try_parse_value(&value); + info!( + "{} value changed to: {:?} from environment variable", + env_key, value + ); + Self::insert_overridden_values_from_env( + &source_dict, + &mut new_dict, + keys.clone(), + env_var_value.clone(), + ); + } + let mut data = FigmentMap::new(); + data.insert(Profile::default(), new_dict); + + Ok(data) + } +} + +pub fn resolve(config_provider_type: &str) -> Result, ServerError> { + match config_provider_type { + DEFAULT_CONFIG_PROVIDER => { + let path = + env::var("NIGIG_CONFIG_PATH").unwrap_or_else(|_| DEFAULT_CONFIG_PATH.to_string()); + Ok(Box::new(FileConfigProvider::new(path))) + } + _ => Err(ServerError::InvalidConfigurationProvider( + config_provider_type.to_string(), + )), + } +} + +/// This does exactly the same as Figment does internally. +fn file_exists>(path: P) -> bool { + let path = path.as_ref(); + + if path.is_absolute() { + return path.is_file(); + } + + let cwd = match std::env::current_dir() { + Ok(dir) => dir, + Err(_) => return false, + }; + + let mut current_dir = cwd.as_path(); + loop { + let file_path = current_dir.join(path); + if file_path.is_file() { + return true; + } + + current_dir = match current_dir.parent() { + Some(parent) => parent, + None => return false, + }; + } +} + +#[async_trait] +impl ConfigProvider for FileConfigProvider { + async fn load_config(&self) -> Result { + info!("Loading config from path: '{}'...", self.path); + + if !file_exists(&self.path) { + return Err(ServerError::CannotLoadConfiguration(format!( + "Cannot find configuration file at path: '{}'.", + self.path, + ))); + } + + let config_builder = Figment::new(); + let extension = self.path.split('.').last().unwrap_or(""); + let config_builder = match extension { + "json" => config_builder.merge(Json::file(&self.path)), + "toml" => config_builder.merge(Toml::file(&self.path)), + e => { + return Err(ServerError::CannotLoadConfiguration(format!("Cannot load configuration: invalid file extension: {e}, only .json and .toml are supported."))); + } + }; + + let custom_env_provider = CustomEnvProvider::new("NIGIG_"); + let config_result: Result = + config_builder.merge(custom_env_provider).extract(); + + match config_result { + Ok(config) => { + info!("Config loaded from path: '{}'", self.path); + info!("Using Config: {}", config); + Ok(config) + } + Err(figment_error) => Err(ServerError::CannotLoadConfiguration(format!( + "Failed to load configuration: {}", + figment_error + ))), + } + } +} diff --git a/src/configs/defaults.rs b/src/configs/defaults.rs new file mode 100644 index 0000000..bc420ee --- /dev/null +++ b/src/configs/defaults.rs @@ -0,0 +1,286 @@ +use crate::configs::http::{ + HttpConfig, HttpCorsConfig, HttpJwtConfig, HttpMetricsConfig, HttpTlsConfig, +}; +use crate::configs::quic::{QuicCertificateConfig, QuicConfig}; +use crate::configs::server::{ + // MessageCleanerConfig, MessageSaverConfig, + PersonalAccessTokenCleanerConfig, + PersonalAccessTokenConfig, + ServerConfig, +}; +use crate::configs::system::{ + CacheConfig, DatabaseConfig, LoggingConfig, RuntimeConfig, SystemConfig, +}; +use crate::configs::tcp::{TcpConfig, TcpTlsConfig}; +use std::sync::Arc; + +use super::http::HttpVariantConfig; +use super::mqtt::MqttCertificateConfig; +use super::mqtt::MqttConfig; + +impl Default for ServerConfig { + fn default() -> ServerConfig { + ServerConfig { + // message_cleaner: MessageCleanerConfig::default(), + // message_saver: MessageSaverConfig::default(), + personal_access_token: PersonalAccessTokenConfig::default(), + system: Arc::new(SystemConfig::default()), + quic: QuicConfig::default(), + tcp: TcpConfig::default(), + http: HttpConfig::default(), + mqtt: MqttConfig::default(), + } + } +} + +impl Default for QuicConfig { + fn default() -> QuicConfig { + QuicConfig { + enabled: true, + address: "127.0.0.1:8080".to_string(), + max_concurrent_bidi_streams: 10000, + datagram_send_buffer_size: "100KB".parse().unwrap(), + initial_mtu: "10KB".parse().unwrap(), + send_window: "100KB".parse().unwrap(), + receive_window: "100KB".parse().unwrap(), + keep_alive_interval: "5s".parse().unwrap(), + max_idle_timeout: "10s".parse().unwrap(), + certificate: QuicCertificateConfig::default(), + } + } +} + +impl Default for QuicCertificateConfig { + fn default() -> QuicCertificateConfig { + QuicCertificateConfig { + self_signed: true, + cert_file: "certs/iggy_cert.pem".to_string(), + key_file: "certs/iggy_key.pem".to_string(), + } + } +} +impl Default for MqttConfig { + fn default() -> MqttConfig { + MqttConfig { + enabled: true, + broker_address: "127.0.0.1".to_string(), + port: 4000, + username: "mqtt".to_string(), + password: "mqtt".to_string(), + keep_alive_interval: "5s".parse().unwrap(), + max_idle_timeout: "10s".parse().unwrap(), + certificate: MqttCertificateConfig::default(), + } + } +} + +impl Default for MqttCertificateConfig { + fn default() -> MqttCertificateConfig { + MqttCertificateConfig { + self_signed: true, + cert_file: "certs/iggy_cert.pem".to_string(), + key_file: "certs/iggy_key.pem".to_string(), + } + } +} + +impl Default for TcpConfig { + fn default() -> TcpConfig { + TcpConfig { + enabled: true, + address: "127.0.0.1:8090".to_string(), + tls: TcpTlsConfig::default(), + } + } +} + +impl Default for HttpConfig { + fn default() -> HttpConfig { + HttpConfig { + // enabled: true, + variants: HttpVariantConfig::default(), + address: ["127.0.0.1:3000".to_string(), "127.0.0.1:3001".to_string()], + cors: HttpCorsConfig::default(), + jwt: HttpJwtConfig::default(), + metrics: HttpMetricsConfig::default(), + tls: HttpTlsConfig::default(), + } + } +} + +impl Default for HttpJwtConfig { + fn default() -> HttpJwtConfig { + HttpJwtConfig { + algorithm: "HS256".to_string(), + issuer: "iggy".to_string(), + audience: "iggy".to_string(), + valid_issuers: vec!["iggy".to_string()], + valid_audiences: vec!["iggy".to_string()], + access_token_expiry: "1h".parse().unwrap(), + refresh_token_expiry: "1d".parse().unwrap(), + clock_skew: "5s".parse().unwrap(), + not_before: "0s".parse().unwrap(), + encoding_secret: "top_secret$iggy.rs$_jwt_HS256_key#!".to_string(), + decoding_secret: "top_secret$iggy.rs$_jwt_HS256_key#!".to_string(), + use_base64_secret: false, + } + } +} + +// impl Default for MessageCleanerConfig { +// fn default() -> MessageCleanerConfig { +// MessageCleanerConfig { +// enabled: true, +// interval: "1m".parse().unwrap(), +// } +// } +// } + +// impl Default for MessageSaverConfig { +// fn default() -> MessageSaverConfig { +// MessageSaverConfig { +// enabled: true, +// enforce_fsync: true, +// interval: "30s".parse().unwrap(), +// } +// } +// } + +impl Default for PersonalAccessTokenConfig { + fn default() -> PersonalAccessTokenConfig { + PersonalAccessTokenConfig { + max_tokens_per_user: 100, + cleaner: PersonalAccessTokenCleanerConfig::default(), + } + } +} + +impl Default for PersonalAccessTokenCleanerConfig { + fn default() -> PersonalAccessTokenCleanerConfig { + PersonalAccessTokenCleanerConfig { + enabled: true, + interval: "1m".parse().unwrap(), + } + } +} + +impl Default for SystemConfig { + fn default() -> SystemConfig { + SystemConfig { + path: "local_data".to_string(), + database: DatabaseConfig::default(), + runtime: RuntimeConfig::default(), + logging: LoggingConfig::default(), + cache: CacheConfig::default(), + // retention_policy: RetentionPolicyConfig::default(), + // stream: StreamConfig::default(), + // encryption: EncryptionConfig::default(), + // topic: TopicConfig::default(), + // partition: PartitionConfig::default(), + // segment: SegmentConfig::default(), + // compression: CompressionConfig::default(), + // message_deduplication: MessageDeduplicationConfig::default(), + } + } +} + +impl Default for DatabaseConfig { + fn default() -> DatabaseConfig { + DatabaseConfig { + path: "database".to_string(), + } + } +} + +impl Default for RuntimeConfig { + fn default() -> RuntimeConfig { + RuntimeConfig { + path: "runtime".to_string(), + } + } +} + +// impl Default for CompressionConfig { +// fn default() -> Self { +// CompressionConfig { +// allow_override: false, +// default_algorithm: "none".parse().unwrap(), +// } +// } +// } + +impl Default for LoggingConfig { + fn default() -> LoggingConfig { + LoggingConfig { + path: "logs".to_string(), + level: "info".to_string(), + max_size: "200 MB".parse().unwrap(), + retention: "7 days".parse().unwrap(), + } + } +} + +impl Default for CacheConfig { + fn default() -> CacheConfig { + CacheConfig { + enabled: true, + size: "2 GB".parse().unwrap(), + } + } +} + +// impl Default for RetentionPolicyConfig { +// fn default() -> RetentionPolicyConfig { +// RetentionPolicyConfig { +// message_expiry: "0".parse().unwrap(), +// max_topic_size: "10 GB".parse().unwrap(), +// } +// } +// } + +// impl Default for StreamConfig { +// fn default() -> StreamConfig { +// StreamConfig { +// path: "streams".to_string(), +// } +// } +// } + +// impl Default for TopicConfig { +// fn default() -> TopicConfig { +// TopicConfig { +// path: "topics".to_string(), +// } +// } +// } + +// impl Default for PartitionConfig { +// fn default() -> PartitionConfig { +// PartitionConfig { +// path: "partitions".to_string(), +// messages_required_to_save: 1000, +// enforce_fsync: false, +// validate_checksum: false, +// } +// } +// } + +// impl Default for SegmentConfig { +// fn default() -> SegmentConfig { +// SegmentConfig { +// size: "1 GB".parse().unwrap(), +// cache_indexes: true, +// cache_time_indexes: true, +// } +// } +// } + +// impl Default for MessageDeduplicationConfig { +// fn default() -> MessageDeduplicationConfig { +// MessageDeduplicationConfig { +// enabled: false, +// max_entries: 1000, +// expiry: "1m".parse().unwrap(), +// } +// } +// } diff --git a/src/configs/displays.rs b/src/configs/displays.rs new file mode 100644 index 0000000..a7306f3 --- /dev/null +++ b/src/configs/displays.rs @@ -0,0 +1,287 @@ +use crate::configs::quic::{QuicCertificateConfig, QuicConfig}; +use crate::configs::{ + http::{HttpConfig, HttpCorsConfig, HttpJwtConfig, HttpMetricsConfig, HttpTlsConfig}, + resource_quota::MemoryResourceQuota, + server::ServerConfig, + system::{CacheConfig, DatabaseConfig, LoggingConfig, SystemConfig}, + tcp::{TcpConfig, TcpTlsConfig}, +}; +use std::fmt::{Display, Formatter}; + +use super::mqtt::MqttCertificateConfig; +use super::mqtt::MqttConfig; + +impl Display for HttpConfig { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{ variants: {:?}, address: {:?}, cors: {}, jwt: {}, metrics: {}, tls: {} }}", + self.variants, self.address, self.cors, self.jwt, self.metrics, self.tls + ) + } +} + +impl Display for HttpCorsConfig { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{ enabled: {}, allowed_methods: {:?}, allowed_origins: {:?}, allowed_headers: {:?}, exposed_headers: {:?}, allow_credentials: {}, allow_private_network: {} }}", + self.enabled, self.allowed_methods, self.allowed_origins, self.allowed_headers, self.exposed_headers, self.allow_credentials, self.allow_private_network + ) + } +} + +impl Display for HttpJwtConfig { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{ algorithm: {}, audience: {}, expiry: {}, use_base64_secret: {} }}", + self.algorithm, self.audience, self.access_token_expiry, self.use_base64_secret + ) + } +} + +impl Display for HttpMetricsConfig { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{ enabled: {}, endpoint: {} }}", + self.enabled, self.endpoint + ) + } +} + +impl Display for HttpTlsConfig { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{ enabled: {}, cert_file: {}, key_file: {} }}", + self.enabled, self.cert_file, self.key_file + ) + } +} + +impl Display for QuicConfig { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{ enabled: {}, address: {}, max_concurrent_bidi_streams: {}, datagram_send_buffer_size: {}, initial_mtu: {}, send_window: {}, receive_window: {}, keep_alive_interval: {}, max_idle_timeout: {}, certificate: {} }}", + self.enabled, + self.address, + self.max_concurrent_bidi_streams, + self.datagram_send_buffer_size, + self.initial_mtu, + self.send_window, + self.receive_window, + self.keep_alive_interval, + self.max_idle_timeout, + self.certificate + ) + } +} + +impl Display for QuicCertificateConfig { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{ self_signed: {}, cert_file: {}, key_file: {} }}", + self.self_signed, self.cert_file, self.key_file + ) + } +} +impl Display for MqttConfig { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{ enabled: {}, host: {}, port: {}, username: {}, password: {}, keep_alive_interval: {}, max_idle_timeout: {}, certificate: {} }}", + self.enabled, + self.broker_address, + self.port, + self.username, + self.password, + self.keep_alive_interval, + self.max_idle_timeout, + self.certificate + ) + } +} + +impl Display for MqttCertificateConfig { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{ self_signed: {}, cert_file: {}, key_file: {} }}", + self.self_signed, self.cert_file, self.key_file + ) + } +} + +impl Display for MemoryResourceQuota { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + MemoryResourceQuota::Bytes(byte) => write!(f, "{}", byte), + MemoryResourceQuota::Percentage(percentage) => write!(f, "{}%", percentage), + } + } +} + +// impl Display for CompressionConfig { +// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { +// write!( +// f, +// "{{ allowed_override: {}, default_algorithm: {} }}", +// self.allow_override, self.default_algorithm +// ) +// } +// } + +impl Display for ServerConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{ system: {}, quic: {}, tcp: {}, http: {} }}", + self.system, self.quic, self.tcp, self.http + ) + } +} + +// impl Display for MessageCleanerConfig { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// write!( +// f, +// "{{ enabled: {}, interval: {} }}", +// self.enabled, self.interval +// ) +// } +// } + +// impl Display for MessageSaverConfig { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// write!( +// f, +// "{{ enabled: {}, enforce_fsync: {}, interval: {} }}", +// self.enabled, self.enforce_fsync, self.interval +// ) +// } +// } + +impl Display for DatabaseConfig { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{{ path: {} }}", self.path) + } +} + +impl Display for CacheConfig { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{{ enabled: {}, size: {} }}", self.enabled, self.size) + } +} + +// impl Display for RetentionPolicyConfig { +// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { +// write!( +// f, +// "{{ message_expiry: {}, max_topic_size: {} }}", +// self.message_expiry, self.max_topic_size +// ) +// } +// } + +// impl Display for EncryptionConfig { +// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { +// write!(f, "{{ enabled: {} }}", self.enabled) +// } +// } + +// impl Display for StreamConfig { +// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { +// write!(f, "{{ path: {} }}", self.path) +// } +// } + +// impl Display for TopicConfig { +// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { +// write!(f, "{{ path: {} }}", self.path) +// } +// } + +// impl Display for PartitionConfig { +// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { +// write!( +// f, +// "{{ path: {}, messages_required_to_save: {}, enforce_fsync: {}, validate_checksum: {} }}", +// self.path, +// self.messages_required_to_save, +// self.enforce_fsync, +// self.validate_checksum +// ) +// } +// } + +// impl Display for MessageDeduplicationConfig { +// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { +// write!( +// f, +// "{{ enabled: {}, max_entries: {:?}, expiry: {:?} }}", +// self.enabled, self.max_entries, self.expiry +// ) +// } +// } + +// impl Display for SegmentConfig { +// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { +// write!( +// f, +// "{{ size_bytes: {}, cache_indexes: {}, cache_time_indexes: {} }}", +// self.size, self.cache_indexes, self.cache_time_indexes +// ) +// } +// } + +impl Display for LoggingConfig { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{ path: {}, level: {}, max_size: {}, retention: {} }}", + self.path, self.level, self.max_size, self.retention + ) + } +} + +impl Display for TcpConfig { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{ enabled: {}, address: {}, tls: {} }}", + self.enabled, self.address, self.tls + ) + } +} + +impl Display for TcpTlsConfig { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{ enabled: {}, certificate: {} }}", + self.enabled, self.certificate + ) + } +} + +impl Display for SystemConfig { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{ path: {}, database: {}, logging: {}, cache: {} }}", + self.path, + self.database, + self.logging, + self.cache, + // self.stream, + // self.topic, + // self.partition, + // self.segment, + // self.encryption + ) + } +} diff --git a/src/configs/http.rs b/src/configs/http.rs new file mode 100644 index 0000000..21a78b4 --- /dev/null +++ b/src/configs/http.rs @@ -0,0 +1,129 @@ +use crate::infrastructure::error::Error; +use crate::utils::duration::IggyDuration; +use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey}; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use serde_with::DisplayFromStr; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct HttpConfig { + pub variants: HttpVariantConfig, + pub address: [String; 2], + pub cors: HttpCorsConfig, + pub jwt: HttpJwtConfig, + pub metrics: HttpMetricsConfig, + pub tls: HttpTlsConfig, +} + +#[derive(Debug, Deserialize, Serialize, Clone, Default)] +pub struct HttpVariantConfig { + pub axum_enabled: bool, + pub xitca_enabled: bool, +} + +#[derive(Debug, Deserialize, Serialize, Default, Clone)] +pub struct HttpCorsConfig { + pub enabled: bool, + pub allowed_methods: Vec, + pub allowed_origins: Vec, + pub allowed_headers: Vec, + pub exposed_headers: Vec, + pub allow_credentials: bool, + pub allow_private_network: bool, +} + +#[serde_as] +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct HttpJwtConfig { + pub algorithm: String, + pub issuer: String, + pub audience: String, + pub valid_issuers: Vec, + pub valid_audiences: Vec, + #[serde_as(as = "DisplayFromStr")] + pub access_token_expiry: IggyDuration, + #[serde_as(as = "DisplayFromStr")] + pub refresh_token_expiry: IggyDuration, + #[serde_as(as = "DisplayFromStr")] + pub clock_skew: IggyDuration, + #[serde_as(as = "DisplayFromStr")] + pub not_before: IggyDuration, + pub encoding_secret: String, + pub decoding_secret: String, + pub use_base64_secret: bool, +} + +#[derive(Debug, Deserialize, Serialize, Default, Clone)] +pub struct HttpMetricsConfig { + pub enabled: bool, + pub endpoint: String, +} + +#[derive(Debug)] +pub enum JwtSecret { + Default(String), + Base64(String), +} + +#[derive(Debug, Deserialize, Serialize, Default, Clone)] +pub struct HttpTlsConfig { + pub enabled: bool, + pub cert_file: String, + pub key_file: String, +} + +impl HttpJwtConfig { + pub fn get_algorithm(&self) -> Result { + match self.algorithm.as_str() { + "HS256" => Ok(Algorithm::HS256), + "HS384" => Ok(Algorithm::HS384), + "HS512" => Ok(Algorithm::HS512), + "RS256" => Ok(Algorithm::RS256), + "RS384" => Ok(Algorithm::RS384), + "RS512" => Ok(Algorithm::RS512), + _ => Err(Error::InvalidJwtAlgorithm(self.algorithm.clone())), + } + } + + pub fn get_decoding_secret(&self) -> JwtSecret { + self.get_secret(&self.decoding_secret) + } + + pub fn get_encoding_secret(&self) -> JwtSecret { + self.get_secret(&self.encoding_secret) + } + + pub fn get_decoding_key(&self) -> Result { + if self.decoding_secret.is_empty() { + return Err(Error::InvalidJwtSecret); + } + + Ok(match self.get_decoding_secret() { + JwtSecret::Default(ref secret) => DecodingKey::from_secret(secret.as_ref()), + JwtSecret::Base64(ref secret) => { + DecodingKey::from_base64_secret(secret).map_err(|_| Error::InvalidJwtSecret)? + } + }) + } + + pub fn get_encoding_key(&self) -> Result { + if self.encoding_secret.is_empty() { + return Err(Error::InvalidJwtSecret); + } + + Ok(match self.get_encoding_secret() { + JwtSecret::Default(ref secret) => EncodingKey::from_secret(secret.as_ref()), + JwtSecret::Base64(ref secret) => { + EncodingKey::from_base64_secret(secret).map_err(|_| Error::InvalidJwtSecret)? + } + }) + } + + fn get_secret(&self, secret: &str) -> JwtSecret { + if self.use_base64_secret { + JwtSecret::Base64(secret.to_string()) + } else { + JwtSecret::Default(secret.to_string()) + } + } +} diff --git a/src/configs/mod.rs b/src/configs/mod.rs new file mode 100644 index 0000000..af29d66 --- /dev/null +++ b/src/configs/mod.rs @@ -0,0 +1,13 @@ +pub mod server; +pub mod system; + +pub mod http; +pub mod mqtt; +pub mod quic; +pub mod tcp; + +pub mod config_provider; +pub mod defaults; +pub mod displays; +pub mod resource_quota; +pub mod validators; diff --git a/src/configs/mqtt.rs b/src/configs/mqtt.rs new file mode 100644 index 0000000..55ebfc1 --- /dev/null +++ b/src/configs/mqtt.rs @@ -0,0 +1,47 @@ +use crate::utils::duration::IggyDuration; +use byte_unit::Byte; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use serde_with::DisplayFromStr; + +#[serde_as] +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct MqttConfig { + pub enabled: bool, + pub broker_address: String, + pub port: u16, + pub username: String, + pub password: String, + #[serde_as(as = "DisplayFromStr")] + pub keep_alive_interval: IggyDuration, + #[serde_as(as = "DisplayFromStr")] + pub max_idle_timeout: IggyDuration, + pub certificate: MqttCertificateConfig, +} + +// transport: Transport, +// keep_alive: Duration, +// clean_session: bool, +// client_id: String, +// credentials: Option<(String, String)>, +// max_incoming_packet_size: usize, +// max_outgoing_packet_size: usize, +// request_channel_capacity: usize, +// max_request_batch: usize, +// pending_throttle: Duration, +// inflight: u16, +// last_will: Option, +// manual_acks: bool, +// #[default("localhost")] +// mqtt_host: &'static str, +// #[default("")] +// mqtt_user: &'static str, +// #[default("")] +// mqtt_pass: &'static str, + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct MqttCertificateConfig { + pub self_signed: bool, + pub cert_file: String, + pub key_file: String, +} diff --git a/src/configs/quic.rs b/src/configs/quic.rs new file mode 100644 index 0000000..3b5b78b --- /dev/null +++ b/src/configs/quic.rs @@ -0,0 +1,29 @@ +use crate::utils::duration::IggyDuration; +use byte_unit::Byte; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use serde_with::DisplayFromStr; + +#[serde_as] +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct QuicConfig { + pub enabled: bool, + pub address: String, + pub max_concurrent_bidi_streams: u64, + pub datagram_send_buffer_size: Byte, + pub initial_mtu: Byte, + pub send_window: Byte, + pub receive_window: Byte, + #[serde_as(as = "DisplayFromStr")] + pub keep_alive_interval: IggyDuration, + #[serde_as(as = "DisplayFromStr")] + pub max_idle_timeout: IggyDuration, + pub certificate: QuicCertificateConfig, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct QuicCertificateConfig { + pub self_signed: bool, + pub cert_file: String, + pub key_file: String, +} diff --git a/src/configs/resource_quota.rs b/src/configs/resource_quota.rs new file mode 100644 index 0000000..167a9d9 --- /dev/null +++ b/src/configs/resource_quota.rs @@ -0,0 +1,183 @@ +extern crate byte_unit; + +use byte_unit::Byte; +use serde::de::{self, Deserializer, Visitor}; +use serde::{Deserialize, Serialize, Serializer}; +use std::fmt; +use std::str::FromStr; +use sysinfo::System; + +#[derive(Debug, PartialEq, Clone)] +pub enum MemoryResourceQuota { + Bytes(Byte), + Percentage(u8), +} + +impl MemoryResourceQuota { + /// Converts the resource quota into bytes. + /// NOTE: This is a blocking operation and it's slow. Don't use it in the hot path. + pub fn into(self) -> u64 { + match self { + MemoryResourceQuota::Bytes(byte) => byte.as_u64(), + MemoryResourceQuota::Percentage(percentage) => { + let mut sys = System::new_all(); + sys.refresh_all(); + + let total_memory = sys.total_memory(); + (total_memory as f64 * (percentage as f64 / 100.0)) as u64 + } + } + } +} + +impl FromStr for MemoryResourceQuota { + type Err = String; + + fn from_str(s: &str) -> Result { + if s.ends_with('%') { + match s.trim_end_matches('%').parse::() { + Ok(val) => { + if val > 100 { + Err("Percentage cannot be greater than 100".to_string()) + } else { + Ok(MemoryResourceQuota::Percentage(val)) + } + } + Err(_) => Err("Invalid percentage value".to_string()), + } + } else { + match Byte::from_str(s) { + Ok(byte) => Ok(MemoryResourceQuota::Bytes(byte)), + Err(_) => Err("Invalid byte unit".to_string()), + } + } + } +} + +struct ResourceQuotaVisitor; + +impl<'de> Visitor<'de> for ResourceQuotaVisitor { + type Value = MemoryResourceQuota; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a byte unit or a percentage") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + MemoryResourceQuota::from_str(value).map_err(de::Error::custom) + } +} + +impl Serialize for MemoryResourceQuota { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + MemoryResourceQuota::Bytes(byte) => serializer.serialize_str(&byte.to_string()), + MemoryResourceQuota::Percentage(percentage) => { + serializer.serialize_str(&format!("{}%", percentage)) + } + } + } +} + +impl<'de> Deserialize<'de> for MemoryResourceQuota { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(ResourceQuotaVisitor) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn test_parse_percentage() { + let parsed: Result = "25%".parse(); + assert_eq!(parsed, Ok(MemoryResourceQuota::Percentage(25))); + } + + #[test] + fn test_invalid_percentage() { + let parsed: Result = "125%".parse(); + assert_eq!( + parsed, + Err("Percentage cannot be greater than 100".to_string()) + ); + } + + #[test] + fn test_parse_memory() { + let parsed: Result = "4 GB".parse(); + assert_eq!( + parsed, + Ok(MemoryResourceQuota::Bytes(Byte::from_str("4GB").unwrap())) + ); + } + + #[test] + fn test_invalid_memory() { + let parsed: Result = "invalid".parse(); + assert_eq!(parsed, Err("Invalid byte unit".to_string())); + } + + #[test] + fn test_serialize() { + let quota = MemoryResourceQuota::Bytes(Byte::from_str("4GB").unwrap()); + let serialized = serde_json::to_string("a).unwrap(); + assert_eq!(serialized, json!("4000000000").to_string()); + + let quota = MemoryResourceQuota::Percentage(25); + let serialized = serde_json::to_string("a).unwrap(); + assert_eq!(serialized, json!("25%").to_string()); + } + + #[test] + fn test_deserialize_bytes() { + let json_data = "\"4000000000\""; // Corresponds to 4GB + let deserialized: Result = + serde_json::from_str(json_data); + + assert!(deserialized.is_ok()); + let unwrapped = deserialized.unwrap(); + assert_eq!( + unwrapped, + MemoryResourceQuota::Bytes(Byte::from_str("4GB").unwrap()) + ); + } + + #[test] + fn test_deserialize_percentage() { + let json_data = "\"25%\""; + let deserialized: Result = + serde_json::from_str(json_data); + + assert!(deserialized.is_ok()); + let unwrapped = deserialized.unwrap(); + assert_eq!(unwrapped, MemoryResourceQuota::Percentage(25)); + } + + #[test] + fn test_deserialize_invalid_bytes() { + let json_data = "\"invalid\""; + let deserialized: Result = + serde_json::from_str(json_data); + assert!(deserialized.is_err()); + } + + #[test] + fn test_deserialize_invalid_percentage() { + let json_data = "\"125%\""; + let deserialized: Result = + serde_json::from_str(json_data); + assert!(deserialized.is_err()); + } +} diff --git a/src/configs/server.rs b/src/configs/server.rs new file mode 100644 index 0000000..b614597 --- /dev/null +++ b/src/configs/server.rs @@ -0,0 +1,64 @@ +use crate::configs::config_provider::ConfigProvider; +use crate::configs::http::HttpConfig; +use crate::configs::mqtt::MqttConfig; +use crate::configs::quic::QuicConfig; +use crate::configs::system::SystemConfig; +use crate::configs::tcp::TcpConfig; +use crate::models::validatable::Validatable; +use crate::server_error::ServerError; +use crate::utils::duration::IggyDuration; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use serde_with::DisplayFromStr; +use std::sync::Arc; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct ServerConfig { + // pub message_cleaner: MessageCleanerConfig, + // pub message_saver: MessageSaverConfig, + pub personal_access_token: PersonalAccessTokenConfig, + pub system: Arc, + pub quic: QuicConfig, + pub mqtt: MqttConfig, + pub tcp: TcpConfig, + pub http: HttpConfig, +} + +#[serde_as] +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct MessageCleanerConfig { + pub enabled: bool, + #[serde_as(as = "DisplayFromStr")] + pub interval: IggyDuration, +} + +#[serde_as] +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct MessageSaverConfig { + pub enabled: bool, + pub enforce_fsync: bool, + #[serde_as(as = "DisplayFromStr")] + pub interval: IggyDuration, +} + +#[derive(Debug, Deserialize, Serialize, Copy, Clone)] +pub struct PersonalAccessTokenConfig { + pub max_tokens_per_user: u32, + pub cleaner: PersonalAccessTokenCleanerConfig, +} + +#[serde_as] +#[derive(Debug, Deserialize, Serialize, Copy, Clone)] +pub struct PersonalAccessTokenCleanerConfig { + pub enabled: bool, + #[serde_as(as = "DisplayFromStr")] + pub interval: IggyDuration, +} + +impl ServerConfig { + pub async fn load(config_provider: &dyn ConfigProvider) -> Result { + let server_config = config_provider.load_config().await?; + server_config.validate()?; + Ok(server_config) + } +} diff --git a/src/configs/system.rs b/src/configs/system.rs new file mode 100644 index 0000000..8106830 --- /dev/null +++ b/src/configs/system.rs @@ -0,0 +1,166 @@ +use crate::configs::resource_quota::MemoryResourceQuota; +use crate::{ + // compression::compression_algorithm::CompressionAlgorithm, + utils::duration::IggyDuration, +}; +use byte_unit::Byte; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use serde_with::DisplayFromStr; + +#[derive(Debug, Deserialize, Serialize)] +pub struct SystemConfig { + pub path: String, + pub database: DatabaseConfig, + pub runtime: RuntimeConfig, + pub logging: LoggingConfig, + pub cache: CacheConfig, + // pub retention_policy: RetentionPolicyConfig, + // pub stream: StreamConfig, + // pub topic: TopicConfig, + // pub partition: PartitionConfig, + // pub segment: SegmentConfig, + // pub encryption: EncryptionConfig, + // pub compression: CompressionConfig, + // pub message_deduplication: MessageDeduplicationConfig, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct DatabaseConfig { + pub path: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct RuntimeConfig { + pub path: String, +} + +// #[derive(Debug, Deserialize, Serialize)] +// pub struct CompressionConfig { +// pub allow_override: bool, +// pub default_algorithm: CompressionAlgorithm, +// } + +#[serde_as] +#[derive(Debug, Deserialize, Serialize)] +pub struct LoggingConfig { + pub path: String, + pub level: String, + pub max_size: Byte, + #[serde_as(as = "DisplayFromStr")] + pub retention: IggyDuration, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct CacheConfig { + pub enabled: bool, + pub size: MemoryResourceQuota, +} + +// #[serde_as] +// #[derive(Debug, Deserialize, Serialize, Copy, Clone)] +// pub struct RetentionPolicyConfig { +// #[serde_as(as = "DisplayFromStr")] +// pub message_expiry: IggyDuration, +// pub max_topic_size: Byte, +// } + +// #[derive(Debug, Deserialize, Serialize, Default)] +// pub struct EncryptionConfig { +// pub enabled: bool, +// pub key: String, +// } + +// #[derive(Debug, Deserialize, Serialize)] +// pub struct StreamConfig { +// pub path: String, +// } + +// #[derive(Debug, Deserialize, Serialize)] +// pub struct TopicConfig { +// pub path: String, +// } + +// #[derive(Debug, Deserialize, Serialize)] +// pub struct PartitionConfig { +// pub path: String, +// pub messages_required_to_save: u32, +// pub enforce_fsync: bool, +// pub validate_checksum: bool, +// } + +// #[serde_as] +// #[derive(Debug, Deserialize, Serialize)] +// pub struct MessageDeduplicationConfig { +// pub enabled: bool, +// pub max_entries: u64, +// #[serde_as(as = "DisplayFromStr")] +// pub expiry: IggyDuration, +// } + +// #[derive(Debug, Deserialize, Serialize)] +// pub struct SegmentConfig { +// pub size: Byte, +// pub cache_indexes: bool, +// pub cache_time_indexes: bool, +// } + +impl SystemConfig { + pub fn get_system_path(&self) -> String { + self.path.to_string() + } + + pub fn get_database_path(&self) -> String { + format!("{}/{}", self.get_system_path(), self.database.path) + } + + pub fn get_runtime_path(&self) -> String { + format!("{}/{}", self.get_system_path(), self.runtime.path) + } + + // pub fn get_streams_path(&self) -> String { + // format!("{}/{}", self.get_system_path(), self.stream.path) + // } + + // pub fn get_stream_path(&self, stream_id: u32) -> String { + // format!("{}/{}", self.get_streams_path(), stream_id) + // } + + // pub fn get_topics_path(&self, stream_id: u32) -> String { + // format!("{}/{}", self.get_stream_path(stream_id), self.topic.path) + // } + + // pub fn get_topic_path(&self, stream_id: u32, topic_id: u32) -> String { + // format!("{}/{}", self.get_topics_path(stream_id), topic_id) + // } + + // pub fn get_partitions_path(&self, stream_id: u32, topic_id: u32) -> String { + // format!( + // "{}/{}", + // self.get_topic_path(stream_id, topic_id), + // self.partition.path + // ) + // } + + // pub fn get_partition_path(&self, stream_id: u32, topic_id: u32, partition_id: u32) -> String { + // format!( + // "{}/{}", + // self.get_partitions_path(stream_id, topic_id), + // partition_id + // ) + // } + + // pub fn get_segment_path( + // &self, + // stream_id: u32, + // topic_id: u32, + // partition_id: u32, + // start_offset: u64, + // ) -> String { + // format!( + // "{}/{:0>20}", + // self.get_partition_path(stream_id, topic_id, partition_id), + // start_offset + // ) + // } +} diff --git a/src/configs/tcp.rs b/src/configs/tcp.rs new file mode 100644 index 0000000..a7286a5 --- /dev/null +++ b/src/configs/tcp.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct TcpConfig { + pub enabled: bool, + pub address: String, + pub tls: TcpTlsConfig, +} + +#[derive(Debug, Deserialize, Serialize, Default, Clone)] +pub struct TcpTlsConfig { + pub enabled: bool, + pub certificate: String, + pub password: String, +} diff --git a/src/configs/validators.rs b/src/configs/validators.rs new file mode 100644 index 0000000..99e8163 --- /dev/null +++ b/src/configs/validators.rs @@ -0,0 +1,145 @@ +extern crate sysinfo; + +// use super::server::{MessageCleanerConfig, MessageSaverConfig}; +// use super::system::CompressionConfig; +use crate::configs::server::{PersonalAccessTokenConfig, ServerConfig}; +use crate::configs::system::CacheConfig; +use crate::models::validatable::Validatable; +use crate::server_error::ServerError; +// use crate::streaming::segments::segment; +use byte_unit::{Byte, UnitType}; +// use iggy::compression::compression_algorithm::CompressionAlgorithm; +use sysinfo::System; +use tracing::{error, info, warn}; + +impl Validatable for ServerConfig { + fn validate(&self) -> Result<(), ServerError> { + // self.system.segment.validate()?; + self.system.cache.validate()?; + // self.system.retention_policy.validate()?; + // self.system.compression.validate()?; + self.personal_access_token.validate()?; + + Ok(()) + } +} + +// impl Validatable for CompressionConfig { +// fn validate(&self) -> Result<(), ServerError> { +// let compression_alg = &self.default_algorithm; +// if *compression_alg != CompressionAlgorithm::None { +// // TODO(numinex): Change this message once server side compression is fully developed. +// warn!( +// "Server started with server-side compression enabled, using algorithm: {}, this feature is not implemented yet!", +// compression_alg +// ); +// } + +// Ok(()) +// } +// } + +impl Validatable for CacheConfig { + fn validate(&self) -> Result<(), ServerError> { + let limit_bytes = self.size.clone().into(); + let mut sys = System::new_all(); + sys.refresh_all(); + sys.refresh_processes(); + let total_memory = sys.total_memory(); + let free_memory = sys.free_memory(); + let cache_percentage = (limit_bytes as f64 / total_memory as f64) * 100.0; + + let pretty_cache_limit = + Byte::from_u64(limit_bytes).get_appropriate_unit(UnitType::Decimal); + let pretty_total_memory = + Byte::from_u64(total_memory).get_appropriate_unit(UnitType::Decimal); + let pretty_free_memory = + Byte::from_u64(free_memory).get_appropriate_unit(UnitType::Decimal); + + if limit_bytes > total_memory { + return Err(ServerError::CacheConfigValidationFailure(format!( + "Requested cache size exceeds 100% of total memory. Requested: {} ({:.2}% of total memory: {}).", + pretty_cache_limit, cache_percentage, pretty_total_memory + ))); + } + + if limit_bytes > (total_memory as f64 * 0.75) as u64 { + warn!( + "Cache configuration -> cache size exceeds 75% of total memory. Set to: {} ({:.2}% of total memory: {}).", + pretty_cache_limit, cache_percentage, pretty_total_memory + ); + } + + info!( + "Cache configuration -> cache size set to {} ({:.2}% of total memory: {}, free memory: {}).", + pretty_cache_limit, cache_percentage, pretty_total_memory, pretty_free_memory + ); + + Ok(()) + } +} + +// impl Validatable for RetentionPolicyConfig { +// fn validate(&self) -> Result<(), ServerError> { +// // TODO(hubcio): Change this message once topic size based retention policy is fully developed. +// if self.max_topic_size.as_u64() > 0 { +// warn!("Retention policy max_topic_size is not implemented yet!"); +// } + +// Ok(()) +// } +// } + +// impl Validatable for SegmentConfig { +// fn validate(&self) -> Result<(), ServerError> { +// if self.size.as_u64() as u32 > segment::MAX_SIZE_BYTES { +// error!( +// "Segment configuration -> size cannot be greater than: {} bytes.", +// segment::MAX_SIZE_BYTES +// ); +// return Err(ServerError::InvalidConfiguration); +// } + +// Ok(()) +// } +// } + +// impl Validatable for MessageSaverConfig { +// fn validate(&self) -> Result<(), ServerError> { +// if self.enabled && self.interval.is_zero() { +// error!("Message saver interval size cannot be zero, it must be greater than 0."); +// return Err(ServerError::InvalidConfiguration); +// } + +// Ok(()) +// } +// } + +// impl Validatable for MessageCleanerConfig { +// fn validate(&self) -> Result<(), ServerError> { +// if self.enabled && self.interval.is_zero() { +// error!("Message cleaner interval size cannot be zero, it must be greater than 0."); +// return Err(ServerError::InvalidConfiguration); +// } + +// Ok(()) +// } +// } + +impl Validatable for PersonalAccessTokenConfig { + fn validate(&self) -> Result<(), ServerError> { + if self.max_tokens_per_user == 0 { + error!("Max tokens per user cannot be zero, it must be greater than 0."); + return Err(ServerError::InvalidConfiguration); + } + + if self.cleaner.enabled && self.cleaner.interval.is_zero() { + error!( + "Personal access token cleaner interval cannot be zero, it must be greater than 0." + ); + return Err(ServerError::InvalidConfiguration); + } + + Ok(()) + } +} diff --git a/src/http/axum_http/diagnostics.rs b/src/http/axum_http/diagnostics.rs new file mode 100644 index 0000000..89403c1 --- /dev/null +++ b/src/http/axum_http/diagnostics.rs @@ -0,0 +1,42 @@ +use crate::http::shared::RequestDetails; +use crate::infrastructure::utils::random_id; +use axum::body::Body; +use axum::{ + extract::ConnectInfo, + http::{Request, StatusCode}, + middleware::Next, + response::Response, +}; +use std::net::SocketAddr; +use tokio::time::Instant; +use tracing::debug; + +pub async fn request_diagnostics( + ConnectInfo(ip_address): ConnectInfo, + mut request: Request, + next: Next, +) -> Result { + let request_id = random_id::get_ulid(); + let path_and_query = request + .uri() + .path_and_query() + .map(|p| p.as_str()) + .unwrap_or("/"); + debug!( + "Processing a request {} {} with ID: {request_id} from client with IP address: {ip_address}...", + request.method(), + path_and_query, + ); + request.extensions_mut().insert(RequestDetails { + request_id, + ip_address, + }); + let now = Instant::now(); + let result = Ok(next.run(request).await); + let elapsed = now.elapsed(); + debug!( + "Processed a request with ID: {request_id} from client with IP address: {ip_address} in {} ms.", + elapsed.as_millis() + ); + result +} diff --git a/src/http/axum_http/http_server.rs b/src/http/axum_http/http_server.rs new file mode 100644 index 0000000..0940153 --- /dev/null +++ b/src/http/axum_http/http_server.rs @@ -0,0 +1,182 @@ +use crate::configs::http::{HttpConfig, HttpCorsConfig}; +use crate::http::axum_http::diagnostics::request_diagnostics; +// use crate::http::diagnostics::request_diagnostics; +use crate::http::axum_http::jwt::jwt_manager::JwtManager; +// use crate::http::metrics::metrics; +use crate::http::shared::AppState; +use crate::infrastructure::systems::system::SharedSystem; +use std::path::PathBuf; +use std::sync::Arc; + +use crate::http::axum_http::jwt::cleaner::start_expired_tokens_cleaner; +use crate::http::axum_http::jwt::middleware::jwt_auth; +use crate::http::axum_http::metrics::metrics; +use crate::http::axum_http::users; +use crate::http::axum_http::*; +use axum::http::Method; +use axum::{middleware, Router}; +use axum_server::tls_rustls::RustlsConfig; +use std::net::SocketAddr; +use tower_http::cors::{AllowOrigin, CorsLayer}; +use tracing::info; + +/// Starts the HTTP API server. +/// Returns the address the server is listening on. +pub async fn start(config: &HttpConfig, system: SharedSystem) -> SocketAddr { + let api_name = if config.tls.enabled { + "HTTP API (TLS)" + } else { + "HTTP API" + }; + + let app_state = build_app_state(&config, system).await; + let mut app = Router::new() + .merge(system::router(app_state.clone(), &config.metrics)) + // .merge(personal_access_tokens::router(app_state.clone())) + .merge(users::router(app_state.clone())) + // .merge(streams::router(app_state.clone())) + // .merge(topics::router(app_state.clone())) + // .merge(consumer_groups::router(app_state.clone())) + // .merge(consumer_offsets::router(app_state.clone())) + // .merge(partitions::router(app_state.clone())) + // .merge(messages::router(app_state.clone())) + .layer(middleware::from_fn_with_state(app_state.clone(), jwt_auth)); + + if config.cors.enabled { + app = app.layer(configure_cors(config.cors.clone())); + } + + if config.metrics.enabled { + app = app.layer(middleware::from_fn_with_state(app_state.clone(), metrics)); + } + + start_expired_tokens_cleaner(app_state.clone()); + app = app.layer(middleware::from_fn(request_diagnostics)); + + // info!("Started {api_name} on: {:?}", config.address[0].clone()); + // let listener = tokio::net::TcpListener::bind(config.address[0].clone()) + // .await + // .unwrap(); + // let address = listener + // .local_addr() + // .expect("Failed to get local address for HTTP server"); + + if !config.tls.enabled { + let listener = tokio::net::TcpListener::bind(config.address[0].clone()) + .await + .unwrap(); + let address = listener + .local_addr() + .expect("Failed to get local address for HTTP server"); + info!("Started {api_name} on: {address}"); + tokio::task::spawn(async move { + if let Err(error) = axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .await + { + tracing::error!("Failed to start {api_name} server, error {}", error); + } + }); + + address + } else { + let tls_config = RustlsConfig::from_pem_file( + PathBuf::from(config.tls.cert_file.clone()), + PathBuf::from(config.tls.key_file.clone()), + ) + .await + .unwrap(); + + let listener = std::net::TcpListener::bind(config.address[0].clone()).unwrap(); + let address = listener + .local_addr() + .expect("Failed to get local address for HTTPS / TLS server"); + + info!("Started {api_name} on: {address}"); + + tokio::task::spawn(async move { + if let Err(error) = axum_server::from_tcp_rustls(listener, tls_config) + .serve(app.into_make_service_with_connect_info::()) + .await + { + tracing::error!("Failed to start {api_name} server, error: {}", error); + } + }); + + address + } +} + +fn configure_cors(config: HttpCorsConfig) -> CorsLayer { + let allowed_origins = match config.allowed_origins { + origins if origins.is_empty() => AllowOrigin::default(), + origins if origins.first().unwrap() == "*" => AllowOrigin::any(), + origins => AllowOrigin::list(origins.iter().map(|s| s.parse().unwrap())), + }; + + let allowed_headers = config + .allowed_headers + .iter() + .map(|s| s.parse().unwrap()) + .collect::>(); + + let exposed_headers = config + .exposed_headers + .iter() + .map(|s| s.parse().unwrap()) + .collect::>(); + + let allowed_methods = config + .allowed_methods + .iter() + .map(|s| match s.to_uppercase().as_str() { + "GET" => Method::GET, + "POST" => Method::POST, + "PUT" => Method::PUT, + "DELETE" => Method::DELETE, + "HEAD" => Method::HEAD, + "OPTIONS" => Method::OPTIONS, + "CONNECT" => Method::CONNECT, + "PATCH" => Method::PATCH, + "TRACE" => Method::TRACE, + _ => panic!("Invalid HTTP method: {}", s), + }) + .collect::>(); + + CorsLayer::new() + .allow_methods(allowed_methods) + .allow_origin(allowed_origins) + .allow_headers(allowed_headers) + .expose_headers(exposed_headers) + .allow_credentials(config.allow_credentials) + .allow_private_network(config.allow_private_network) +} + +pub async fn build_app_state(config: &HttpConfig, system: SharedSystem) -> Arc { + let db; + { + let system_read = system.read(); + db = system_read + .db + .as_ref() + .expect("Database not initialized") + .clone(); + } + + let jwt_manager = JwtManager::from_config(&config.jwt, db); + if let Err(error) = jwt_manager { + panic!("Failed to initialize JWT manager: {}", error); + } + + let jwt_manager = jwt_manager.unwrap(); + if jwt_manager.load_revoked_tokens().await.is_err() { + panic!("Failed to load revoked access tokens"); + } + + Arc::new(AppState { + jwt_manager, + system, + }) +} diff --git a/src/http/axum_http/jwt/cleaner.rs b/src/http/axum_http/jwt/cleaner.rs new file mode 100644 index 0000000..93015c5 --- /dev/null +++ b/src/http/axum_http/jwt/cleaner.rs @@ -0,0 +1,33 @@ +use crate::http::shared::AppState; +use crate::utils::timestamp::NigigTimeStamp; +use std::sync::Arc; +use std::time::Duration; +use tracing::{error, info}; + +pub fn start_expired_tokens_cleaner(app_state: Arc) { + tokio::spawn(async move { + let mut interval_timer = tokio::time::interval(Duration::from_secs(300)); + loop { + interval_timer.tick().await; + info!("Deleting expired tokens..."); + let now = NigigTimeStamp::now().to_secs(); + app_state + .jwt_manager + .delete_expired_revoked_tokens(now) + .await + .unwrap_or_else(|err| { + error!( + "Failed to delete expired revoked access tokens. Error: {}", + err + ); + }); + app_state + .jwt_manager + .delete_expired_refresh_tokens(now) + .await + .unwrap_or_else(|err| { + error!("Failed to delete expired refresh tokens. Error: {}", err); + }); + } + }); +} diff --git a/src/http/axum_http/jwt/json_web_token.rs b/src/http/axum_http/jwt/json_web_token.rs new file mode 100644 index 0000000..12a34e5 --- /dev/null +++ b/src/http/axum_http/jwt/json_web_token.rs @@ -0,0 +1,37 @@ +use crate::models::user_info::UserId; +use serde::{Deserialize, Serialize}; +use std::net::SocketAddr; + +#[derive(Debug, Clone)] +pub struct Identity { + pub token_id: String, + pub token_expiry: u64, + pub user_id: UserId, + pub ip_address: SocketAddr, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct JwtClaims { + pub jti: String, + pub iss: String, + pub aud: String, + pub sub: u32, + pub iat: u64, + pub exp: u64, + pub nbf: u64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RevokedAccessToken { + pub id: String, + pub expiry: u64, +} + +#[derive(Debug)] +pub struct GeneratedTokens { + pub user_id: UserId, + pub access_token: String, + pub access_token_expiry: u64, + pub refresh_token: String, + pub refresh_token_expiry: u64, +} diff --git a/src/http/axum_http/jwt/jwt_manager.rs b/src/http/axum_http/jwt/jwt_manager.rs new file mode 100644 index 0000000..3bf71fb --- /dev/null +++ b/src/http/axum_http/jwt/jwt_manager.rs @@ -0,0 +1,269 @@ +use crate::configs::http::HttpJwtConfig; +use crate::http::axum_http::jwt::json_web_token::{GeneratedTokens, JwtClaims, RevokedAccessToken}; +use crate::http::axum_http::jwt::refresh_token::RefreshToken; +use crate::http::axum_http::jwt::storage::TokenStorage; +use crate::infrastructure::error::Error; +use crate::models::user_info::UserId; +use crate::utils::duration::IggyDuration; +use crate::utils::timestamp::NigigTimeStamp; +use jsonwebtoken::{encode, Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation}; +use sled::Db; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; +use tracing::{debug, error, info}; + +pub struct IssuerOptions { + pub issuer: String, + pub audience: String, + pub access_token_expiry: IggyDuration, + pub refresh_token_expiry: IggyDuration, + pub not_before: IggyDuration, + pub key: EncodingKey, + pub algorithm: Algorithm, +} + +pub struct ValidatorOptions { + pub valid_audiences: Vec, + pub valid_issuers: Vec, + pub clock_skew: IggyDuration, + pub key: DecodingKey, +} + +pub struct JwtManager { + issuer: IssuerOptions, + validator: ValidatorOptions, + tokens_storage: TokenStorage, + revoked_tokens: RwLock>, + validations: HashMap, +} + +impl JwtManager { + pub fn new( + issuer: IssuerOptions, + validator: ValidatorOptions, + db: Arc, + ) -> Result { + let validation = JwtManager::create_validation( + issuer.algorithm, + &validator.valid_issuers, + &validator.valid_audiences, + validator.clock_skew, + ); + + Ok(Self { + validations: vec![(issuer.algorithm, validation)].into_iter().collect(), + issuer, + validator, + tokens_storage: TokenStorage::new(db), + revoked_tokens: RwLock::new(HashMap::new()), + }) + } + + pub fn from_config(config: &HttpJwtConfig, db: Arc) -> Result { + let algorithm = config.get_algorithm()?; + let issuer = IssuerOptions { + issuer: config.issuer.clone(), + audience: config.audience.clone(), + access_token_expiry: config.access_token_expiry, + refresh_token_expiry: config.refresh_token_expiry, + not_before: config.not_before, + key: config.get_encoding_key()?, + algorithm, + }; + let validator = ValidatorOptions { + valid_audiences: config.valid_audiences.clone(), + valid_issuers: config.valid_issuers.clone(), + clock_skew: config.clock_skew, + key: config.get_decoding_key()?, + }; + JwtManager::new(issuer, validator, db) + } + + fn create_validation( + algorithm: Algorithm, + issuers: &[String], + audiences: &[String], + clock_skew: IggyDuration, + ) -> Validation { + let mut validator = Validation::new(algorithm); + validator.set_issuer(issuers); + validator.set_audience(audiences); + validator.leeway = clock_skew.as_secs() as u64; + validator + } + + pub async fn load_revoked_tokens(&self) -> Result<(), Error> { + let revoked_tokens = self.tokens_storage.load_all_revoked_access_tokens()?; + let mut tokens = self.revoked_tokens.write().await; + for token in revoked_tokens { + tokens.insert(token.id, token.expiry); + } + Ok(()) + } + + pub async fn delete_expired_revoked_tokens(&self, now: u64) -> Result<(), Error> { + let mut tokens_to_delete = Vec::new(); + let revoked_tokens = self.revoked_tokens.read().await; + for (id, expiry) in revoked_tokens.iter() { + if expiry < &now { + tokens_to_delete.push(id.to_string()); + } + } + drop(revoked_tokens); + + debug!( + "Found {} expired revoked access tokens to delete.", + tokens_to_delete.len() + ); + if tokens_to_delete.is_empty() { + return Ok(()); + } + + debug!( + "Deleting {} expired revoked access tokens...", + tokens_to_delete.len() + ); + let mut revoked_tokens = self.revoked_tokens.write().await; + for id in tokens_to_delete { + revoked_tokens.remove(&id); + self.tokens_storage.delete_revoked_access_token(&id)?; + debug!("Deleted expired revoked access token with ID: {id}") + } + + Ok(()) + } + + pub async fn delete_expired_refresh_tokens(&self, now: u64) -> Result<(), Error> { + let mut tokens_to_delete = Vec::new(); + let refresh_tokens = self.tokens_storage.load_all_refresh_tokens()?; + for token in refresh_tokens { + if token.is_expired(now) { + tokens_to_delete.push(token.token_hash); + } + } + + debug!( + "Found {} expired refresh tokens to delete.", + tokens_to_delete.len() + ); + if tokens_to_delete.is_empty() { + return Ok(()); + } + + debug!( + "Deleting {} expired refresh tokens...", + tokens_to_delete.len() + ); + for token_hash in tokens_to_delete { + self.tokens_storage.delete_refresh_token(&token_hash)?; + debug!("Deleted expired refresh token with hash: {token_hash}") + } + + Ok(()) + } + + pub fn generate(&self, user_id: UserId) -> Result { + let header = Header::new(self.issuer.algorithm); + let now = NigigTimeStamp::now().to_secs(); + let iat = now; + let exp = iat + self.issuer.access_token_expiry.as_secs() as u64; + let nbf = iat + self.issuer.not_before.as_secs() as u64; + let claims = JwtClaims { + jti: uuid::Uuid::new_v4().to_string(), + sub: user_id, + aud: self.issuer.audience.to_string(), + iss: self.issuer.issuer.to_string(), + iat, + exp, + nbf, + }; + + let access_token = encode::(&header, &claims, &self.issuer.key); + if let Err(err) = access_token { + error!("Cannot generate JWT token. Error: {}", err); + return Err(Error::CannotGenerateJwt); + } + + let (refresh_token, raw_refresh_token) = RefreshToken::new( + user_id, + now, + self.issuer.refresh_token_expiry.as_secs() as u64, + ); + self.tokens_storage.save_refresh_token(&refresh_token)?; + + Ok(GeneratedTokens { + user_id, + access_token: access_token.unwrap(), + refresh_token: raw_refresh_token, + access_token_expiry: exp, + refresh_token_expiry: refresh_token.expiry, + }) + } + + pub fn refresh_token(&self, refresh_token: &str) -> Result { + let now = NigigTimeStamp::now().to_secs(); + if refresh_token.is_empty() { + return Err(Error::InvalidRefreshToken); + } + + let token_hash = RefreshToken::hash_token(refresh_token); + let refresh_token = self.tokens_storage.load_refresh_token(&token_hash); + if refresh_token.is_err() { + return Err(Error::InvalidRefreshToken); + } + + let refresh_token = refresh_token.unwrap(); + self.tokens_storage.delete_refresh_token(&token_hash)?; + if refresh_token.expiry < now { + return Err(Error::RefreshTokenExpired); + } + + self.generate(refresh_token.user_id) + } + + pub fn decode(&self, token: &str, algorithm: Algorithm) -> Result, Error> { + let validation = self.validations.get(&algorithm); + if validation.is_none() { + return Err(Error::InvalidJwtAlgorithm(Self::map_algorithm_to_string( + algorithm, + ))); + } + + let validation = validation.unwrap(); + match jsonwebtoken::decode::(token, &self.validator.key, validation) { + Ok(claims) => Ok(claims), + _ => Err(Error::Unauthenticated), + } + } + + fn map_algorithm_to_string(algorithm: Algorithm) -> String { + match algorithm { + Algorithm::HS256 => "HS256", + Algorithm::HS384 => "HS384", + Algorithm::HS512 => "HS512", + Algorithm::RS256 => "RS256", + Algorithm::RS384 => "RS384", + Algorithm::RS512 => "RS512", + _ => "Unknown", + } + .to_string() + } + + pub async fn revoke_token(&self, token_id: &str, expiry: u64) -> Result<(), Error> { + let mut revoked_tokens = self.revoked_tokens.write().await; + revoked_tokens.insert(token_id.to_string(), expiry); + self.tokens_storage + .save_revoked_access_token(&RevokedAccessToken { + id: token_id.to_string(), + expiry, + })?; + info!("Revoked access token with ID: {token_id}"); + Ok(()) + } + + pub async fn is_token_revoked(&self, token_id: &str) -> bool { + let revoked_tokens = self.revoked_tokens.read().await; + revoked_tokens.contains_key(token_id) + } +} diff --git a/src/http/axum_http/jwt/middleware.rs b/src/http/axum_http/jwt/middleware.rs new file mode 100644 index 0000000..4a9b20a --- /dev/null +++ b/src/http/axum_http/jwt/middleware.rs @@ -0,0 +1,69 @@ +use crate::http::axum_http::jwt::json_web_token::Identity; +use crate::http::shared::{AppState, RequestDetails}; +use axum::body::Body; +use axum::{ + extract::State, + http::{Request, StatusCode}, + middleware::Next, + response::Response, +}; +use std::borrow::Borrow; +use std::sync::Arc; + +const AUTHORIZATION: &str = "authorization"; +const BEARER: &str = "Bearer "; + +const UNAUTHORIZED_PATHS: &[&str] = &[ + "/", + "/metrics", + "/ping", + "/users/login", + "/users/refresh-token", + "/personal-access-tokens/login", +]; +const UNAUTHORIZED: StatusCode = StatusCode::UNAUTHORIZED; + +pub async fn jwt_auth( + State(state): State>, + mut request: Request, + next: Next, +) -> Result { + if UNAUTHORIZED_PATHS.contains(&request.uri().path()) { + return Ok(next.run(request).await); + } + + let bearer = request + .headers() + .get(AUTHORIZATION) + .ok_or(UNAUTHORIZED)? + .to_str() + .map_err(|_| UNAUTHORIZED)?; + + if !bearer.starts_with(BEARER) { + return Err(StatusCode::UNAUTHORIZED); + } + + let jwt_token = &bearer[BEARER.len()..]; + let token_header = jsonwebtoken::decode_header(jwt_token).map_err(|_| UNAUTHORIZED)?; + let jwt_claims = state + .jwt_manager + .decode(jwt_token, token_header.alg) + .map_err(|_| UNAUTHORIZED)?; + if state + .jwt_manager + .is_token_revoked(&jwt_claims.claims.jti) + .await + { + return Err(StatusCode::UNAUTHORIZED); + } + + let request_details = request.extensions().get::().unwrap(); + let identity = Identity { + token_id: jwt_claims.claims.jti, + token_expiry: jwt_claims.claims.exp, + user_id: jwt_claims.claims.sub, + ip_address: request_details.ip_address, + }; + request.extensions_mut().insert(identity); + Ok(next.run(request).await) +} diff --git a/src/http/axum_http/jwt/mod.rs b/src/http/axum_http/jwt/mod.rs new file mode 100644 index 0000000..3bb564c --- /dev/null +++ b/src/http/axum_http/jwt/mod.rs @@ -0,0 +1,6 @@ +pub mod cleaner; +pub mod json_web_token; +pub mod jwt_manager; +pub mod middleware; +pub mod refresh_token; +pub mod storage; diff --git a/src/http/axum_http/jwt/refresh_token.rs b/src/http/axum_http/jwt/refresh_token.rs new file mode 100644 index 0000000..a45fafa --- /dev/null +++ b/src/http/axum_http/jwt/refresh_token.rs @@ -0,0 +1,73 @@ +use crate::infrastructure::utils::hash; +use crate::models::user_info::UserId; +use crate::utils::text::as_base64; +use ring::rand::SecureRandom; +use serde::{Deserialize, Serialize}; + +const REFRESH_TOKEN_SIZE: usize = 50; + +#[derive(Debug, Serialize, Deserialize)] +pub struct RefreshToken { + #[serde(skip)] + pub token_hash: String, + pub user_id: u32, + pub expiry: u64, +} + +impl RefreshToken { + pub fn new(user_id: UserId, now: u64, expiry: u64) -> (Self, String) { + let mut buffer: [u8; REFRESH_TOKEN_SIZE] = [0; REFRESH_TOKEN_SIZE]; + let system_random = ring::rand::SystemRandom::new(); + system_random.fill(&mut buffer).unwrap(); + let token = as_base64(&buffer); + let hash = Self::hash_token(&token); + let expiry = now + expiry; + ( + Self { + token_hash: hash, + user_id, + expiry, + }, + token, + ) + } + + pub fn is_expired(&self, now: u64) -> bool { + now > self.expiry + } + + pub fn hash_token(token: &str) -> String { + hash::calculate_256(token.as_bytes()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::utils::timestamp::NigigTimeStamp; + + #[test] + fn refresh_token_should_be_created_with_random_secure_value_and_hashed_successfully() { + let user_id = 1; + let now = NigigTimeStamp::now().to_secs(); + let expiry = 10; + let (refresh_token, raw_token) = RefreshToken::new(user_id, now, expiry); + assert_eq!(refresh_token.user_id, user_id); + assert_eq!(refresh_token.expiry, now + expiry); + assert!(!raw_token.is_empty()); + assert_ne!(refresh_token.token_hash, raw_token); + assert_eq!( + refresh_token.token_hash, + RefreshToken::hash_token(&raw_token) + ); + } + + #[test] + fn refresh_access_token_should_be_expired_given_passed_expiry() { + let user_id = 1; + let now = NigigTimeStamp::now().to_secs(); + let expiry = 1; + let (refresh_token, _) = RefreshToken::new(user_id, now, expiry); + assert!(refresh_token.is_expired(now + expiry + 1)); + } +} diff --git a/src/http/axum_http/jwt/storage.rs b/src/http/axum_http/jwt/storage.rs new file mode 100644 index 0000000..63700f7 --- /dev/null +++ b/src/http/axum_http/jwt/storage.rs @@ -0,0 +1,193 @@ +use crate::http::axum_http::jwt::json_web_token::RevokedAccessToken; +use crate::http::axum_http::jwt::refresh_token::RefreshToken; +use crate::infrastructure::error::Error; +use anyhow::Context; +use sled::Db; +use std::str::from_utf8; +use std::sync::Arc; +use tracing::{error, info}; + +const REVOKED_ACCESS_TOKENS_KEY_PREFIX: &str = "revoked_access_token"; +const REFRESH_TOKENS_KEY_PREFIX: &str = "refresh_token"; + +#[derive(Debug)] +pub struct TokenStorage { + db: Arc, +} + +impl TokenStorage { + pub fn new(db: Arc) -> Self { + Self { db } + } + + pub fn load_refresh_token(&self, token_hash: &str) -> Result { + let key = Self::get_refresh_token_key(token_hash); + let token_data = self + .db + .get(&key) + .with_context(|| format!("Failed to load refresh token, key: {}", key)); + if let Err(err) = token_data { + return Err(Error::CannotLoadResource(err)); + } + + let token_data = token_data.unwrap(); + if token_data.is_none() { + return Err(Error::ResourceNotFound(key)); + } + + let token_data = token_data.unwrap(); + let token_data = rmp_serde::from_slice::(&token_data) + .with_context(|| format!("Failed to deserialize refresh token, key: {}", key)); + if let Err(err) = token_data { + return Err(Error::CannotDeserializeResource(err)); + } + + let mut token_data = token_data.unwrap(); + token_data.token_hash = token_hash.to_string(); + Ok(token_data) + } + + pub fn load_all_refresh_tokens(&self) -> Result, Error> { + let key = format!("{REFRESH_TOKENS_KEY_PREFIX}:"); + let refresh_tokens: Result, Error> = self + .db + .scan_prefix(&key) + .map(|data| { + let (hash, value) = data + .with_context(|| { + format!( + "Failed to load refresh token, when searching by key: {}", + key + ) + }) + .map_err(Error::CannotLoadResource)?; + + let mut token = rmp_serde::from_slice::(&value) + .with_context(|| { + format!( + "Failed to deserialize refresh token, when searching by key: {}", + key + ) + }) + .map_err(Error::CannotDeserializeResource)?; + + token.token_hash = from_utf8(&hash) + .with_context(|| "Failed to convert hash to UTF-8 string") + .map_err(Error::CannotDeserializeResource)? + .to_string(); + Ok(token) + }) + .collect(); + + let refresh_tokens = refresh_tokens?; + info!("Loaded {} refresh tokens", refresh_tokens.len()); + Ok(refresh_tokens) + } + + pub fn load_all_revoked_access_tokens(&self) -> Result, Error> { + let key = format!("{REVOKED_ACCESS_TOKENS_KEY_PREFIX}:"); + let revoked_tokens: Result, Error> = self + .db + .scan_prefix(&key) + .map(|data| { + let (_, value) = data + .with_context(|| { + format!( + "Failed to load invoked refresh token, when searching by key: {}", + key + ) + }) + .map_err(Error::CannotLoadResource)?; + + let token = rmp_serde::from_slice::(&value) + .with_context(|| { + format!( + "Failed to deserialize revoked access token, when searching by key: {}", + key + ) + }) + .map_err(Error::CannotDeserializeResource)?; + Ok(token) + }) + .collect(); + + let revoked_tokens = revoked_tokens?; + info!("Loaded {} revoked access tokens", revoked_tokens.len()); + Ok(revoked_tokens) + } + + pub fn save_revoked_access_token(&self, token: &RevokedAccessToken) -> Result<(), Error> { + let key = Self::get_revoked_token_key(&token.id); + match rmp_serde::to_vec(&token) + .with_context(|| format!("Failed to serialize revoked access token, key: {}", key)) + { + Ok(data) => { + if let Err(err) = self + .db + .insert(&key, data) + .with_context(|| "Failed to save revoked access token") + { + return Err(Error::CannotSaveResource(err)); + } + } + Err(err) => { + return Err(Error::CannotSerializeResource(err)); + } + } + Ok(()) + } + + pub fn save_refresh_token(&self, token: &RefreshToken) -> Result<(), Error> { + let key = Self::get_refresh_token_key(&token.token_hash); + match rmp_serde::to_vec(&token) + .with_context(|| format!("Failed to serialize refresh token, key: {}", key)) + { + Ok(data) => { + if let Err(err) = self + .db + .insert(&key, data) + .with_context(|| format!("Failed to save refresh token, key: {}", key)) + { + return Err(Error::CannotSaveResource(err)); + } + } + Err(err) => { + return Err(Error::CannotSerializeResource(err)); + } + } + Ok(()) + } + + pub fn delete_revoked_access_token(&self, id: &str) -> Result<(), Error> { + let key = Self::get_revoked_token_key(id); + if let Err(err) = self + .db + .remove(&key) + .with_context(|| format!("Failed to delete revoked access token, key: {}", key)) + { + return Err(Error::CannotDeleteResource(err)); + } + Ok(()) + } + + pub fn delete_refresh_token(&self, token_hash: &str) -> Result<(), Error> { + let key = Self::get_refresh_token_key(token_hash); + if let Err(err) = self + .db + .remove(&key) + .with_context(|| format!("Failed to delete refresh token, key: {}", key)) + { + error!("Cannot delete refresh token. Error: {err}"); + return Err(Error::CannotDeleteResource(err)); + } + Ok(()) + } + + fn get_revoked_token_key(id: &str) -> String { + format!("{REVOKED_ACCESS_TOKENS_KEY_PREFIX}:{id}") + } + + fn get_refresh_token_key(token_hash: &str) -> String { + format!("{REFRESH_TOKENS_KEY_PREFIX}:{token_hash}") + } +} diff --git a/src/http/axum_http/metrics.rs b/src/http/axum_http/metrics.rs new file mode 100644 index 0000000..5da1d88 --- /dev/null +++ b/src/http/axum_http/metrics.rs @@ -0,0 +1,26 @@ +use crate::http::shared::AppState; +use axum::body::Body; +use axum::{ + extract::State, + http::{Request, StatusCode}, + middleware::Next, + response::Response, +}; +use std::sync::Arc; +// use xitca_http::Response; +// use xitca_web::middleware::sync::Next; +// use xitca_web::WebContext; + +pub async fn metrics( + State(state): State>, + request: Request, + next: Next, +) -> Result { + state.system.read().metrics.increment_http_requests(); + Ok(next.run(request).await) +} + +// fn xmetrics(next: &mut Next, ctx: WebContext<'_, AppState>) -> Result, E> { +// ctx.state().system.read().metrics.increment_http_requests(); +// next.call(ctx) +// } diff --git a/src/http/axum_http/mod.rs b/src/http/axum_http/mod.rs new file mode 100644 index 0000000..09cb949 --- /dev/null +++ b/src/http/axum_http/mod.rs @@ -0,0 +1,9 @@ +// pub mod error; +pub mod diagnostics; +pub mod http_server; +pub mod jwt; +pub mod metrics; +// pub mod shared; +pub mod system; +pub mod testserver; +pub mod users; diff --git a/src/http/axum_http/system.rs b/src/http/axum_http/system.rs new file mode 100644 index 0000000..e41dd43 --- /dev/null +++ b/src/http/axum_http/system.rs @@ -0,0 +1,82 @@ +// boilerplate to run in different modes +// cfg_if! { +// if #[cfg(feature = "axum")] { +// use axum::extract::{Path, State}; +use crate::configs::http::HttpMetricsConfig; +use crate::http::axum_http::jwt::json_web_token::Identity; +use crate::http::error::CustomError; +use axum::extract::State; +use axum::routing::get; +use axum::{Extension, Json, Router}; +// use crate::http::mapper; +use crate::http::shared::AppState; +use crate::infrastructure::session::Session; +// use iggy::models::client_info::{ClientInfo, ClientInfoDetails}; +use crate::models::stats::Stats; +use std::sync::Arc; + +const NAME: &str = "Nigiginc HTTP\n"; +const PONG: &str = "pong\n"; + +pub fn router(state: Arc, metrics_config: &HttpMetricsConfig) -> Router { + let mut router = Router::new() + .route("/", get(|| async { NAME })) + .route("/ping", get(|| async { PONG })) + .route("/stats", get(get_stats)); + // .route("/clients", get(get_clients)) + // .route("/clients/:client_id", get(get_client)); + if metrics_config.enabled { + router = router.route(&metrics_config.endpoint, get(get_metrics)); + } + + router.with_state(state) +} +async fn get_metrics(State(state): State>) -> Result { + let system = state.system.read(); + Ok(system.metrics.get_formatted_output()) +} + +async fn get_stats( + State(state): State>, + Extension(identity): Extension, +) -> Result, CustomError> { + let system = state.system.read(); + let stats = system + .get_stats(&Session::stateless(identity.user_id, identity.ip_address)) + .await?; + Ok(Json(stats)) +} + +// async fn get_client( +// State(state): State>, +// Extension(identity): Extension, +// Path(client_id): Path, +// ) -> Result, CustomError> { +// let system = state.system.read(); +// let client = system +// .get_client( +// &Session::stateless(identity.user_id, identity.ip_address), +// client_id, +// ) +// .await?; +// let client = client.read().await; +// let client = mapper::map_client(&client).await; +// Ok(Json(client)) +// } + +// async fn get_clients( +// State(state): State>, +// Extension(identity): Extension, +// ) -> Result>, CustomError> { +// let system = state.system.read(); +// let clients = system +// .get_clients(&Session::stateless(identity.user_id, identity.ip_address)) +// .await?; +// let clients = mapper::map_clients(&clients).await; +// Ok(Json(clients)) +// } + +// } else { + +// } +// } diff --git a/src/http/axum_http/testserver.rs b/src/http/axum_http/testserver.rs new file mode 100644 index 0000000..0934e2b --- /dev/null +++ b/src/http/axum_http/testserver.rs @@ -0,0 +1,50 @@ +use crate::configs::http::HttpConfig; +// use crate::http::jwt::middleware::jwt_auth; +use super::http_server::build_app_state; +use crate::http::axum_http::jwt::middleware::*; +use crate::http::axum_http::system; +use crate::http::axum_http::users; +use crate::infrastructure::systems::system::SharedSystem; +use axum::{middleware, Router}; +use std::net::SocketAddr; + +pub async fn start(config: HttpConfig, system: SharedSystem) -> SocketAddr { + let api_name = if config.tls.enabled { + "HTTP API (TLS)" + } else { + "HTTP API" + }; + + let app_state = build_app_state(&config, system).await; + let app = Router::new() + .merge(system::router(app_state.clone(), &config.metrics)) + // .merge(personal_access_tokens::router(app_state.clone())) + .merge(users::router(app_state.clone())) + // .merge(streams::router(app_state.clone())) + // .merge(topics::router(app_state.clone())) + // .merge(consumer_groups::router(app_state.clone())) + // .merge(consumer_offsets::router(app_state.clone())) + // .merge(partitions::router(app_state.clone())) + // .merge(messages::router(app_state.clone())) + .layer(middleware::from_fn_with_state(app_state.clone(), jwt_auth)); + + tracing::info!("Started {api_name} on: {:?}", config.address[0].clone()); + + let listener = tokio::net::TcpListener::bind(config.address[0].clone()) + .await + .unwrap(); + let address = listener + .local_addr() + .expect("Failed to get local address for HTTP server"); + + tokio::task::spawn(async move { + axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .await + .expect("Failed to start HTTP server"); + }); + + address +} diff --git a/src/http/axum_http/users.rs b/src/http/axum_http/users.rs new file mode 100644 index 0000000..a53fd93 --- /dev/null +++ b/src/http/axum_http/users.rs @@ -0,0 +1,203 @@ +use crate::http::axum_http::jwt::json_web_token::Identity; +use crate::http::error::CustomError; +use crate::http::mapper; +use crate::http::mapper::map_generated_tokens_to_identity_info; +use crate::http::shared::AppState; +use crate::infrastructure::session::Session; +use crate::models::identifier::Identifier; +use crate::models::identity_info::IdentityInfo; +use crate::models::user_info::{UserInfo, UserInfoDetails}; +use crate::models::users::change_password::ChangePassword; +use crate::models::users::create_user::CreateUser; +use crate::models::users::login_user::LoginUser; +use crate::models::users::logout_user::LogoutUser; +use crate::models::users::update_permissions::UpdatePermissions; +use crate::models::users::update_user::UpdateUser; +use crate::models::validatable::Validatable; +use axum::extract::{Path, State}; +use axum::http::StatusCode; +use axum::routing::{get, post, put}; +use axum::{Extension, Json, Router}; +use serde::Deserialize; +use std::sync::Arc; + +pub fn router(state: Arc) -> Router { + Router::new() + .route("/users", get(get_users).post(create_user)) + .route( + "/:user_id", + get(get_user).put(update_user).delete(delete_user), + ) + .route("/:user_id/permissions", put(update_permissions)) + .route("/:user_id/password", put(change_password)) + .route("/login", post(login_user)) + .route("/logout", post(logout_user)) + .route("/refresh-token", post(refresh_token)) + .with_state(state) +} + +async fn get_user( + State(state): State>, + Extension(identity): Extension, + Path(user_id): Path, +) -> Result, CustomError> { + let user_id = Identifier::from_str_value(&user_id)?; + let system = state.system.read(); + let user = system + .find_user( + &Session::stateless(identity.user_id, identity.ip_address), + &user_id, + ) + .await?; + let user = mapper::map_user(&user); + Ok(Json(user)) +} + +async fn get_users( + State(state): State>, + Extension(identity): Extension, +) -> Result>, CustomError> { + let system = state.system.read(); + let users = system + .get_users(&Session::stateless(identity.user_id, identity.ip_address)) + .await?; + let users = mapper::map_users(&users); + Ok(Json(users)) +} + +async fn create_user( + State(state): State>, + Extension(identity): Extension, + Json(command): Json, +) -> Result { + command.validate()?; + let mut system = state.system.write(); + system + .create_user( + &Session::stateless(identity.user_id, identity.ip_address), + &command.username, + &command.password, + command.status, + command.permissions.clone(), + ) + .await?; + Ok(StatusCode::NO_CONTENT) +} + +async fn update_user( + State(state): State>, + Extension(identity): Extension, + Path(user_id): Path, + Json(mut command): Json, +) -> Result { + command.user_id = Identifier::from_str_value(&user_id)?; + command.validate()?; + let system = state.system.read(); + system + .update_user( + &Session::stateless(identity.user_id, identity.ip_address), + &command.user_id, + command.username, + command.status, + ) + .await?; + Ok(StatusCode::NO_CONTENT) +} + +async fn update_permissions( + State(state): State>, + Extension(identity): Extension, + Path(user_id): Path, + Json(mut command): Json, +) -> Result { + command.user_id = Identifier::from_str_value(&user_id)?; + command.validate()?; + let mut system = state.system.write(); + system + .update_permissions( + &Session::stateless(identity.user_id, identity.ip_address), + &command.user_id, + command.permissions, + ) + .await?; + Ok(StatusCode::NO_CONTENT) +} + +async fn change_password( + State(state): State>, + Extension(identity): Extension, + Path(user_id): Path, + Json(mut command): Json, +) -> Result { + command.user_id = Identifier::from_str_value(&user_id)?; + command.validate()?; + let system = state.system.read(); + system + .change_password( + &Session::stateless(identity.user_id, identity.ip_address), + &command.user_id, + &command.current_password, + &command.new_password, + ) + .await?; + Ok(StatusCode::NO_CONTENT) +} + +async fn delete_user( + State(state): State>, + Extension(identity): Extension, + Path(user_id): Path, +) -> Result { + let user_id = Identifier::from_str_value(&user_id)?; + let mut system = state.system.write(); + system + .delete_user( + &Session::stateless(identity.user_id, identity.ip_address), + &user_id, + ) + .await?; + Ok(StatusCode::NO_CONTENT) +} + +async fn login_user( + State(state): State>, + Json(command): Json, +) -> Result, CustomError> { + command.validate()?; + let system = state.system.read(); + let user = system + .login_user(&command.username, &command.password, None) + .await?; + let tokens = state.jwt_manager.generate(user.id)?; + Ok(Json(map_generated_tokens_to_identity_info(tokens))) +} + +async fn logout_user( + State(state): State>, + Extension(identity): Extension, + Json(command): Json, +) -> Result { + command.validate()?; + let system = state.system.read(); + system + .logout_user(&Session::stateless(identity.user_id, identity.ip_address)) + .await?; + state + .jwt_manager + .revoke_token(&identity.token_id, identity.token_expiry) + .await?; + Ok(StatusCode::NO_CONTENT) +} + +async fn refresh_token( + State(state): State>, + Json(command): Json, +) -> Result, CustomError> { + let tokens = state.jwt_manager.refresh_token(&command.refresh_token)?; + Ok(Json(map_generated_tokens_to_identity_info(tokens))) +} + +#[derive(Debug, Deserialize)] +struct RefreshToken { + refresh_token: String, +} diff --git a/src/http/error.rs b/src/http/error.rs new file mode 100644 index 0000000..e19df9e --- /dev/null +++ b/src/http/error.rs @@ -0,0 +1,147 @@ +// use crate::infrastructure::error::Error; +use crate::infrastructure::error::Error as InfraError; + +use serde::Serialize; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum CustomError { + #[error(transparent)] + NigigServerError(#[from] InfraError), +} + +#[derive(Debug, Serialize)] +pub struct ErrorResponse { + pub id: u32, + pub code: String, + pub reason: String, + pub field: Option, +} + +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::Json; + +impl IntoResponse for CustomError { + fn into_response(self) -> Response { + match self { + CustomError::NigigServerError(error) => { + let status_code = match error { + // Error::StreamIdNotFound(_) => StatusCode::NOT_FOUND, + // Error::TopicIdNotFound(_, _) => StatusCode::NOT_FOUND, + // Error::PartitionNotFound(_, _, _) => StatusCode::NOT_FOUND, + // Error::SegmentNotFound => StatusCode::NOT_FOUND, + // Error::ClientNotFound(_) => StatusCode::NOT_FOUND, + // Error::ConsumerGroupIdNotFound(_, _) => StatusCode::NOT_FOUND, + // Error::ConsumerGroupNameNotFound(_, _) => StatusCode::NOT_FOUND, + // Error::ConsumerGroupMemberNotFound(_, _, _) => StatusCode::NOT_FOUND, + // Error::CannotLoadResource(_) => StatusCode::NOT_FOUND, + // Error::ResourceNotFound(_) => StatusCode::NOT_FOUND, + // Error::IoError(_) => StatusCode::INTERNAL_SERVER_ERROR, + // Error::WriteError(_) => StatusCode::INTERNAL_SERVER_ERROR, + // Error::CannotParseInt(_) => StatusCode::INTERNAL_SERVER_ERROR, + // Error::CannotParseSlice(_) => StatusCode::INTERNAL_SERVER_ERROR, + // Error::CannotParseUtf8(_) => StatusCode::INTERNAL_SERVER_ERROR, + InfraError::Unauthenticated => StatusCode::UNAUTHORIZED, + InfraError::Unauthorized => StatusCode::FORBIDDEN, + _ => StatusCode::BAD_REQUEST, + }; + (status_code, Json(ErrorResponse::from_error(error))) + } + } + .into_response() + } +} + +impl ErrorResponse { + pub fn from_error(error: InfraError) -> Self { + ErrorResponse { + id: error.as_code(), + code: error.as_string().to_string(), + reason: error.to_string(), + field: match error { + // Error::StreamIdNotFound(_) => Some("stream_id".to_string()), + // Error::TopicIdNotFound(_, _) => Some("topic_id".to_string()), + // Error::PartitionNotFound(_, _, _) => Some("partition_id".to_string()), + // Error::SegmentNotFound => Some("segment_id".to_string()), + // Error::ClientNotFound(_) => Some("client_id".to_string()), + // Error::InvalidStreamName => Some("name".to_string()), + // Error::StreamNameAlreadyExists(_) => Some("name".to_string()), + // Error::InvalidTopicName => Some("name".to_string()), + // Error::TopicNameAlreadyExists(_, _) => Some("name".to_string()), + // Error::InvalidStreamId => Some("stream_id".to_string()), + // Error::StreamIdAlreadyExists(_) => Some("stream_id".to_string()), + // Error::InvalidTopicId => Some("topic_id".to_string()), + // Error::TopicIdAlreadyExists(_, _) => Some("topic_id".to_string()), + // Error::InvalidOffset(_) => Some("offset".to_string()), + // Error::InvalidConsumerGroupId => Some("consumer_group_id".to_string()), + // Error::ConsumerGroupIdAlreadyExists(_, _) => Some("consumer_group_id".to_string()), + // Error::ConsumerGroupNameAlreadyExists(_, _) => Some("name".to_string()), + InfraError::UserAlreadyExists => Some("username".to_string()), + InfraError::PersonalAccessTokenAlreadyExists(_, _) => Some("name".to_string()), + _ => None, + }, + } + } +} +use std::{convert::Infallible, error, fmt}; +use xitca_web::{error::{Error, Internal}, http::WebResponse, service::Service, WebContext}; + +impl<'r, C> Service> for CustomError { + type Response = WebResponse; + type Error = Infallible; + + async fn call(&self, ctx: WebContext<'r, C>) -> Result { + xitca_web::error::BadRequest.call(ctx).await + } +} +impl From for Error { + fn from(e: CustomError) -> Self { + Error::from_service(e) + } +} +pub async fn error_handler(s: &S, ctx: WebContext<'_, C>) -> Result> +where + S: for<'r> Service, Response = Res, Error = Error>, +{ + s.call(ctx).await.map_err(|e| { + // debug format error info. + println!("{e:?}"); + + // display format error info. + println!("{e}"); + + // utilize std::error::Error trait methods for backtrace and more advanced error info. + let _source = e.source(); + + // // upcast trait and downcast to concrete type again. + // // this offers the ability to regain typed error specific error handling. + // // *. this is a runtime feature and not reinforced at compile time. + // if let Some(e) = (&*e as &dyn error::Error).downcast_ref::() { + // match e { + // XitcaCustomError::NigigServerError => {} + // } + // } + + e + }) +} + +pub async fn catch_panic(service: &S, ctx: WebContext<'_, C>) -> Result> +where + S: for<'r> Service, Response = WebResponse, Error = Error>, +{ + use futures::FutureExt; + std::panic::AssertUnwindSafe(service.call(ctx)) + .catch_unwind() + .await + // Internal is the default blank 500 error response type. we convert it to Error type and + // the default catch all convertor would help up construct a http response. + .map_err(|_| Internal)? +} +// #[error_impl] +// impl CustomError { +// async fn call(&self, ctx: WebContext<'_, C>) -> WebResponse { +// // logic of generating a response from your error. +// } +// } diff --git a/src/http/mapper.rs b/src/http/mapper.rs new file mode 100644 index 0000000..d1eaed2 --- /dev/null +++ b/src/http/mapper.rs @@ -0,0 +1,264 @@ +use crate::models::identity_info::{IdentityInfo, IdentityTokens, TokenInfo}; +use crate::{ + // http::jwt::json_web_token::GeneratedTokens, + infrastructure::users::user::User, +}; +// use crate::streaming::clients::client_manager::Client; +// use crate::systems::personal_access_tokens::personal_access_token::PersonalAccessToken; +// use crate::streaming::streams::stream::Stream; +// use crate::streaming::topics::consumer_group::ConsumerGroup; +// use crate::streaming::topics::topic::Topic; +// use crate::models::users::user::User; + +use crate::models::user_info::{UserInfo, UserInfoDetails}; + +use super::axum_http::jwt::json_web_token::GeneratedTokens; + +// use super::jwt::json_web_token::GeneratedTokens; +// use std::sync::Arc; +// use tokio::sync::RwLock; + +pub fn map_user(user: &User) -> UserInfoDetails { + UserInfoDetails { + id: user.id, + username: user.username.clone(), + created_at: user.created_at, + status: user.status, + permissions: user.permissions.clone(), + } +} +pub fn map_users(users: &[User]) -> Vec { + let mut users_data = Vec::with_capacity(users.len()); + for user in users { + let user = UserInfo { + id: user.id, + username: user.username.clone(), + created_at: user.created_at, + status: user.status, + }; + users_data.push(user); + } + users_data.sort_by(|a, b| a.id.cmp(&b.id)); + users_data +} +pub fn map_generated_tokens_to_identity_info(tokens: GeneratedTokens) -> IdentityInfo { + IdentityInfo { + user_id: tokens.user_id, + tokens: Some({ + IdentityTokens { + access_token: TokenInfo { + token: tokens.access_token, + expiry: tokens.access_token_expiry, + }, + refresh_token: TokenInfo { + token: tokens.refresh_token, + expiry: tokens.refresh_token_expiry, + }, + } + }), + } +} +// pub async fn map_client(client: &Client) -> iggy::models::client_info::ClientInfoDetails { +// let client = iggy::models::client_info::ClientInfoDetails { +// client_id: client.client_id, +// user_id: client.user_id, +// transport: client.transport.to_string(), +// address: client.address.to_string(), +// consumer_groups_count: client.consumer_groups.len() as u32, +// consumer_groups: client +// .consumer_groups +// .iter() +// .map(|consumer_group| ConsumerGroupInfo { +// stream_id: consumer_group.stream_id, +// topic_id: consumer_group.topic_id, +// consumer_group_id: consumer_group.consumer_group_id, +// }) +// .collect(), +// }; +// client +// } + +// pub async fn map_clients( +// clients: &[Arc>], +// ) -> Vec { +// let mut all_clients = Vec::new(); +// for client in clients { +// let client = client.read().await; +// let client = iggy::models::client_info::ClientInfo { +// client_id: client.client_id, +// user_id: client.user_id, +// transport: client.transport.to_string(), +// address: client.address.to_string(), +// consumer_groups_count: client.consumer_groups.len() as u32, +// }; +// all_clients.push(client); +// } + +// all_clients.sort_by(|a, b| a.client_id.cmp(&b.client_id)); +// all_clients +// } +// pub async fn map_stream(stream: &Stream) -> StreamDetails { +// let topics = map_topics(&stream.get_topics()).await; +// let mut stream_details = StreamDetails { +// id: stream.stream_id, +// created_at: stream.created_at, +// name: stream.name.clone(), +// topics_count: topics.len() as u32, +// size_bytes: stream.get_size_bytes().await, +// messages_count: stream.get_messages_count().await, +// topics, +// }; +// stream_details.topics.sort_by(|a, b| a.id.cmp(&b.id)); +// stream_details +// } + +// pub async fn map_streams(streams: &[&Stream]) -> Vec { +// let mut streams_data = Vec::with_capacity(streams.len()); +// for stream in streams { +// let stream = iggy::models::stream::Stream { +// id: stream.stream_id, +// created_at: stream.created_at, +// name: stream.name.clone(), +// size_bytes: stream.get_size_bytes().await, +// topics_count: stream.get_topics().len() as u32, +// messages_count: stream.get_messages_count().await, +// }; +// streams_data.push(stream); +// } + +// streams_data.sort_by(|a, b| a.id.cmp(&b.id)); +// streams_data +// } + +// pub async fn map_topics(topics: &[&Topic]) -> Vec { +// let mut topics_data = Vec::with_capacity(topics.len()); +// for topic in topics { +// let topic = iggy::models::topic::Topic { +// id: topic.topic_id, +// created_at: topic.created_at, +// name: topic.name.clone(), +// size_bytes: topic.get_size_bytes().await, +// partitions_count: topic.get_partitions().len() as u32, +// messages_count: topic.get_messages_count().await, +// message_expiry: topic.message_expiry, +// }; +// topics_data.push(topic); +// } +// topics_data.sort_by(|a, b| a.id.cmp(&b.id)); +// topics_data +// } + +// pub async fn map_topic(topic: &Topic) -> TopicDetails { +// let mut topic_details = TopicDetails { +// id: topic.topic_id, +// created_at: topic.created_at, +// name: topic.name.clone(), +// size_bytes: topic.get_size_bytes().await, +// messages_count: topic.get_messages_count().await, +// partitions_count: topic.get_partitions().len() as u32, +// partitions: Vec::new(), +// message_expiry: topic.message_expiry, +// }; +// for partition in topic.get_partitions() { +// let partition = partition.read().await; +// topic_details +// .partitions +// .push(iggy::models::partition::Partition { +// id: partition.partition_id, +// created_at: partition.created_at, +// segments_count: partition.get_segments().len() as u32, +// current_offset: partition.current_offset, +// size_bytes: partition.get_size_bytes(), +// messages_count: partition.get_messages_count(), +// }); +// } +// topic_details.partitions.sort_by(|a, b| a.id.cmp(&b.id)); +// topic_details +// } + +// pub fn map_users(users: &[User]) -> Vec { +// let mut users_data = Vec::with_capacity(users.len()); +// for user in users { +// let user = UserInfo { +// id: user.id, +// username: user.username.clone(), +// created_at: user.created_at, +// status: user.status, +// }; +// users_data.push(user); +// } +// users_data.sort_by(|a, b| a.id.cmp(&b.id)); +// users_data +// } + +// pub fn map_personal_access_tokens( +// personal_access_tokens: &[PersonalAccessToken], +// ) -> Vec { +// let mut personal_access_tokens_data = Vec::with_capacity(personal_access_tokens.len()); +// for personal_access_token in personal_access_tokens { +// let personal_access_token = PersonalAccessTokenInfo { +// name: personal_access_token.name.clone(), +// expiry: personal_access_token.expiry, +// }; +// personal_access_tokens_data.push(personal_access_token); +// } +// personal_access_tokens_data.sort_by(|a, b| a.name.cmp(&b.name)); +// personal_access_tokens_data +// } + +// pub async fn map_consumer_groups( +// consumer_groups: &[&RwLock], +// ) -> Vec { +// let mut groups = Vec::new(); +// for consumer_group in consumer_groups { +// let consumer_group = consumer_group.read().await; +// let consumer_group = iggy::models::consumer_group::ConsumerGroup { +// id: consumer_group.consumer_group_id, +// name: consumer_group.name.clone(), +// partitions_count: consumer_group.partitions_count, +// members_count: consumer_group.get_members().len() as u32, +// }; +// groups.push(consumer_group); +// } +// groups.sort_by(|a, b| a.id.cmp(&b.id)); +// groups +// } + +// pub async fn map_consumer_group(consumer_group: &ConsumerGroup) -> ConsumerGroupDetails { +// let mut consumer_group_details = ConsumerGroupDetails { +// id: consumer_group.consumer_group_id, +// name: consumer_group.name.clone(), +// partitions_count: consumer_group.partitions_count, +// members_count: consumer_group.get_members().len() as u32, +// members: Vec::new(), +// }; +// let members = consumer_group.get_members(); +// for member in members { +// let member = member.read().await; +// let partitions = member.get_partitions(); +// consumer_group_details.members.push(ConsumerGroupMember { +// id: member.id, +// partitions_count: partitions.len() as u32, +// partitions, +// }); +// } +// consumer_group_details +// } + +// pub fn map_generated_tokens_to_identity_info(tokens: GeneratedTokens) -> IdentityInfo { +// IdentityInfo { +// user_id: tokens.user_id, +// tokens: Some({ +// IdentityTokens { +// access_token: TokenInfo { +// token: tokens.access_token, +// expiry: tokens.access_token_expiry, +// }, +// refresh_token: TokenInfo { +// token: tokens.refresh_token, +// expiry: tokens.refresh_token_expiry, +// }, +// } +// }), +// } +// } diff --git a/src/http/mod.rs b/src/http/mod.rs new file mode 100644 index 0000000..ac10f3b --- /dev/null +++ b/src/http/mod.rs @@ -0,0 +1,5 @@ +pub mod axum_http; +pub mod error; +pub mod mapper; +pub mod shared; +pub mod xitcav_http; diff --git a/src/http/shared.rs b/src/http/shared.rs new file mode 100644 index 0000000..abde9fc --- /dev/null +++ b/src/http/shared.rs @@ -0,0 +1,38 @@ +use crate::http::axum_http::jwt::jwt_manager::JwtManager; +use crate::infrastructure::systems::system::SharedSystem; +// use std::borrow::Borrow; +use std::net::SocketAddr; +use ulid::Ulid; +use xitca_codegen::State; + +// #[derive(State, Clone, Debug, Eq, PartialEq)] +pub struct AppState { + pub jwt_manager: JwtManager, + pub system: SharedSystem, +} + +// impl Borrow for AppState { +// fn borrow(&self) -> &JwtManager { +// &self.jwt_manager +// } +// } + +// impl Borrow for AppState { +// fn borrow(&self) -> &SharedSystem { +// &self.system +// } +// } + +#[derive(State, Clone, Debug, Eq, PartialEq)] +pub struct DuplicateState { + #[borrow] + pub field1: String, + #[borrow] + pub field2: u32, +} + +#[derive(Debug, Copy, Clone)] +pub struct RequestDetails { + pub request_id: Ulid, + pub ip_address: SocketAddr, +} diff --git a/src/http/xitcav_http/diagnostics.rs b/src/http/xitcav_http/diagnostics.rs new file mode 100644 index 0000000..855b045 --- /dev/null +++ b/src/http/xitcav_http/diagnostics.rs @@ -0,0 +1,50 @@ +use crate::http::shared::AppState; +use crate::http::shared::RequestDetails; +use crate::infrastructure::utils::random_id; +use std::borrow::Borrow; +use tokio::time::Instant; +use tracing::debug; +use xitca_web::http::WebResponse; +use xitca_web::middleware::sync::Next; +use xitca_web::WebContext; + +pub fn request_diagnostics( + // ext: &RequestExt<()>, + next: &mut Next, + mut ctx: WebContext<'_, C>, +) -> Result, E> +where + // S: for<'r> Service, Response = WebResponse, Error = Error>, + C: Borrow, // annotate we want to borrow &String from generic C state type. +{ + // ctx.state().borrow().system.read().metrics.increment_http_requests(); + let request_id = random_id::get_ulid(); + let ip_address = *ctx.req().body().socket_addr(); + let path_and_query = ctx + .req() + .uri() + .path_and_query() + .map(|p| p.as_str()) + .unwrap_or("/"); + debug!( + "Processing a request {} {} with ID: {request_id} from client with IP address: {ip_address:?}...", + ctx.req().method(), + path_and_query, + ); + ctx.req_mut().extensions_mut().insert(RequestDetails { + request_id, + ip_address, + }); + let now = Instant::now(); + let result = next.call(ctx); + let elapsed = now.elapsed(); + debug!( + "Processed a request with ID: {request_id} from client with IP address: {ip_address:?} in {} ms.", + elapsed.as_millis() + ); + result + // next.call(ctx).map(|res| { + // tracing::info!("metricx: response status: {}", res.status()); + // res + // }) +} diff --git a/src/http/xitcav_http/error.rs b/src/http/xitcav_http/error.rs new file mode 100644 index 0000000..4cf9a26 --- /dev/null +++ b/src/http/xitcav_http/error.rs @@ -0,0 +1,71 @@ +use crate::infrastructure::error::Error as InfraError; +use std::{convert::Infallible, error, fmt}; +// use thiserror::Error as TError; + +// use xitca_http::http::IntoResponse; +use xitca_web::codegen::error_impl; +use xitca_web::{error::Error, http::WebResponse, service::Service, WebContext}; + +// #[derive(Debug)] +// pub enum XitcaCustomError { +// // NigigServerError(InfraError), +// NigigServerError, +// } +#[derive(Debug, thiserror::Error)] +pub enum XitcaCustomError { + #[error(transparent)] + NigigServerError(#[from] InfraError), + // NigigServerError(#[from] InfraError), +} + +// Error is the main error type xitca-web uses and at some point XitcaCustomError would +// need to be converted to it. +impl From for Error { + fn from(e: XitcaCustomError) -> Self { + Error::from_service(e) + } +} +// #[error_impl] +// impl XitcaCustomError { +// async fn call(&self, ctx: WebContext<'_, C>) -> WebResponse { +// // logic of generating a response from your error. +// } +// } + +// response generator of XitcaCustomError. in this case we generate blank bad request error. +impl<'r, C> Service> for XitcaCustomError { + type Response = WebResponse; + type Error = Infallible; + + async fn call(&self, ctx: WebContext<'r, C>) -> Result { + xitca_web::error::BadRequest.call(ctx).await + } +} + +// a middleware function used for intercept and interact with app handler outputs. +pub async fn error_handler(s: &S, ctx: WebContext<'_, C>) -> Result> +where + S: for<'r> Service, Response = Res, Error = Error>, +{ + s.call(ctx).await.map_err(|e| { + // debug format error info. + println!("{e:?}"); + + // display format error info. + println!("{e}"); + + // utilize std::error::Error trait methods for backtrace and more advanced error info. + let _source = e.source(); + + // // upcast trait and downcast to concrete type again. + // // this offers the ability to regain typed error specific error handling. + // // *. this is a runtime feature and not reinforced at compile time. + // if let Some(e) = (&*e as &dyn error::Error).downcast_ref::() { + // match e { + // XitcaCustomError::NigigServerError => {} + // } + // } + + e + }) +} diff --git a/src/http/xitcav_http/http_server.rs b/src/http/xitcav_http/http_server.rs new file mode 100644 index 0000000..e30268d --- /dev/null +++ b/src/http/xitcav_http/http_server.rs @@ -0,0 +1,264 @@ +use crate::configs::http::{HttpConfig, HttpCorsConfig}; +// use crate::http::diagnostics::request_diagnostics; +use crate::http::axum_http::jwt::jwt_manager::JwtManager; +// use crate::http::metrics::metrics; +use crate::http::error::error_handler; +use crate::http::shared::AppState; +use crate::http::xitcav_http::diagnostics::request_diagnostics; +use crate::http::xitcav_http::jwt::middlewarex::middleware_fn; +use crate::http::xitcav_http::metrics::metricsx; +use crate::http::xitcav_http::request_limits::{connection_limit, request_limit}; +use crate::http::xitcav_http::{system, users}; +use crate::infrastructure::systems::system::SharedSystem; +use std::error::Error; +use std::fs::File; +use std::io::BufReader; +use std::{convert::Infallible, io, sync::Arc}; +use xitca_web::middleware::rate_limit::RateLimit; + +// use axum::http::Method; +// use axum::{middleware, Router}; +use std::net::SocketAddr; +use tower_http::{ + cors::{AllowOrigin, CorsLayer}, + // set_status::SetStatusLayer, +}; +use tracing::info; +use xitca_http::{ + // http, + util::service::{ + route::{get, post, Route}, + router::{Router, RouterError}, + }, +}; +use xitca_web::middleware::Extension; + +use openssl::ssl::{AlpnError, SslAcceptor, SslFiletype, SslMethod}; +use quinn::ServerConfig; +use rustls::{Certificate, PrivateKey}; +use xitca_http::{ + h1, + h2, + h3, + http::{ + const_header_value::TEXT_UTF8, header::CONTENT_TYPE, Method, Request, RequestExt, Response, + Version, + }, + // middleware::tower_http_compat::TowerHttpCompat as CompatMiddleware, + util::middleware::{Logger, SocketConfig}, + HttpServiceBuilder, + ResponseBody, +}; +use xitca_server::ServerFuture; +use xitca_service::{fn_service, middleware, ServiceExt}; +use xitca_web::handler::handler_service; +use xitca_web::middleware::sync::SyncMiddleware; +use xitca_web::middleware::{tower_http_compat::TowerHttpCompat, Group}; +use xitca_web::{App, WebContext}; + +fn configure_cors(config: HttpCorsConfig) -> CorsLayer { + let allowed_origins = match config.allowed_origins { + origins if origins.is_empty() => AllowOrigin::default(), + origins if origins.first().unwrap() == "*" => AllowOrigin::any(), + origins => AllowOrigin::list(origins.iter().map(|s| s.parse().unwrap())), + }; + + let allowed_headers = config + .allowed_headers + .iter() + .map(|s| s.parse().unwrap()) + .collect::>(); + + let exposed_headers = config + .exposed_headers + .iter() + .map(|s| s.parse().unwrap()) + .collect::>(); + + let allowed_methods = config + .allowed_methods + .iter() + .map(|s| match s.to_uppercase().as_str() { + "GET" => Method::GET, + "POST" => Method::POST, + "PUT" => Method::PUT, + "DELETE" => Method::DELETE, + "HEAD" => Method::HEAD, + "OPTIONS" => Method::OPTIONS, + "CONNECT" => Method::CONNECT, + "PATCH" => Method::PATCH, + "TRACE" => Method::TRACE, + _ => panic!("Invalid HTTP method: {}", s), + }) + .collect::>(); + + CorsLayer::new() + .allow_methods(allowed_methods) + .allow_origin(allowed_origins) + .allow_headers(allowed_headers) + .expose_headers(exposed_headers) + .allow_credentials(config.allow_credentials) + .allow_private_network(config.allow_private_network) +} + +pub async fn build_app_state(config: &HttpConfig, system: SharedSystem) -> Arc { + let db; + { + let system_read = system.read(); + db = system_read + .db + .as_ref() + .expect("Database not initialized") + .clone(); + } + + let jwt_manager = JwtManager::from_config(&config.jwt, db); + if let Err(error) = jwt_manager { + panic!("Failed to initialize JWT manager: {}", error); + } + + let jwt_manager = jwt_manager.unwrap(); + if jwt_manager.load_revoked_tokens().await.is_err() { + panic!("Failed to load revoked access tokens"); + } + + Arc::new(AppState { + jwt_manager, + system, + }) +} + +/// Starts the XITCA HTTP API server. +/// Returns the address the server is listening on. +pub async fn startxitca(config: HttpConfig, system: SharedSystem) -> (SocketAddr, ServerFuture) { + let api_name = if config.tls.enabled { + "XITCA HTTP API (TLS)" + } else { + "XITCA HTTP API" + }; + + let app_state: Arc = build_app_state(&config, system).await; + // start_expired_tokens_cleaner(app_state.clone()); + + // construct http3 quic server config + // let hconfig = h3_config(&config).unwrap(); + + info!("Started {api_name} on: {:?}", config.address[1]); + + let listener = std::net::TcpListener::bind(config.address[1].clone()).unwrap(); + let address = listener + .local_addr() + .expect("Failed to get local address for HTTP server"); + + let mut app = App::new(); + app = system::routes(app); + app = users::routes(app); + // app.with_state(app_state); + + let service = app + .with_state(app_state) + // .enclosed_fn(request_limit) + .enclosed_fn(error_handler) + .enclosed(SyncMiddleware::new(request_diagnostics)) + .enclosed(SyncMiddleware::new(metricsx)) + .enclosed(TowerHttpCompat::new(configure_cors(config.cors))) + .enclosed_fn(middleware_fn) + .enclosed(Logger::new()) + // limit client to 60rps based on it's ip address. + .enclosed(RateLimit::per_minute(60)) + // middleware before App::finish have access to http request types. + .finish() + .enclosed(HttpServiceBuilder::new()); + // middleware after http service have access to raw connection types. + // .enclosed_fn(connection_limit); + + let server = xitca_server::Builder::new() + .bind("service_name", "127.0.0.1:8080", service) + .unwrap() + .build(); + // let server = app + // .with_state(app_state) + // .enclosed_fn(error_handler) + // .enclosed(SyncMiddleware::new(request_diagnostics)) + // .enclosed(SyncMiddleware::new(metricsx)) + // .enclosed(TowerHttpCompat::new(configure_cors(config.cors))) + // // .enclosed(TowerHttpCompat::new( + // // tower::ServiceBuilder::new().layer(CorsLayer::very_permissive()), + // // )) + // .enclosed_fn(middleware_fn) + // .enclosed(Logger::new()) + // .serve() + // .listen(listener) + // .unwrap() + // // .bind("127.0.0.1:8080")? + // .run(); + (address, server) +} + +async fn handler_h1( + _: Request>, +) -> Result, Infallible> { + Ok(Response::builder() + .header(CONTENT_TYPE, TEXT_UTF8) + .body("Hello World from Http/1!".into()) + .unwrap()) +} +async fn handler_h3( + _: Request>, +) -> Result, Box> { + Response::builder() + .status(200) + .version(Version::HTTP_3) + .header(CONTENT_TYPE, TEXT_UTF8) + .body("Hello World from Http/3!".into()) + .map_err(Into::into) +} + +fn generate_self_signed_cert( +) -> Result<(Vec, rustls::PrivateKey), Box> { + let certificate = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let certificate_der = certificate.serialize_der().unwrap(); + let private_key = certificate.serialize_private_key_der(); + let private_key = rustls::PrivateKey(private_key); + let cert_chain = vec![rustls::Certificate(certificate_der)]; + Ok((cert_chain, private_key)) +} +fn load_certificates( + cert_file: &str, + key_file: &str, +) -> Result<(Vec, rustls::PrivateKey), Box> { + let mut cert_chain_reader = BufReader::new(File::open(cert_file)?); + let certs = rustls_pemfile::certs(&mut cert_chain_reader) + .map(|x| rustls::Certificate(x.unwrap().to_vec())) + .collect(); + let mut key_reader = BufReader::new(File::open(key_file)?); + let mut keys = rustls_pemfile::rsa_private_keys(&mut key_reader) + .map(|x| rustls::PrivateKey(x.unwrap().secret_pkcs1_der().to_vec())) + .collect::>(); + let key = rustls::PrivateKey(keys.remove(0).0); + Ok((certs, key)) +} +fn h3_config(config: &HttpConfig) -> io::Result { + let (certificate, key) = match config.tls.enabled { + true => generate_self_signed_cert().unwrap(), + false => load_certificates(&config.tls.cert_file, &config.tls.key_file).unwrap(), + }; + // let cert = fs::read("../../../certs/nigig_cert.pem")?; + // let key = fs::read("../../../certs/nigig_key.pem")?; + // let key = rustls_pemfile::pkcs8_private_keys(&mut &*key).remove(0); + // let key = PrivateKey(key); + // let cert = rustls_pemfile::certs(&mut &*cert) + // // .into_iter() + // .map(|res| res.unwrap()) + // .collect(); + + let mut acceptor = rustls::ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(certificate, key) + .unwrap(); + + acceptor.alpn_protocols = vec![b"h3".to_vec()]; + + Ok(ServerConfig::with_crypto(Arc::new(acceptor))) +} diff --git a/src/http/xitcav_http/jwt/middlewarex.rs b/src/http/xitcav_http/jwt/middlewarex.rs new file mode 100644 index 0000000..3ebea33 --- /dev/null +++ b/src/http/xitcav_http/jwt/middlewarex.rs @@ -0,0 +1,81 @@ +use crate::http::axum_http::jwt::json_web_token::Identity; +use crate::http::shared::{AppState, RequestDetails}; + +use std::borrow::Borrow; +use xitca_http::http::StatusCode; +use xitca_service::Service; +use xitca_web::error::Error; +// use xitca_web::handler::state::StateRef; +// use xitca_web::handler::FromRequest; +// use xitca_web::http::WebResponse; +use xitca_web::WebContext; + +const AUTHORIZATION: &str = "authorization"; +pub const BEARER: &str = "Bearer "; +const UNAUTHORIZED: StatusCode = StatusCode::UNAUTHORIZED; + +const UNAUTHORIZED_PATHS: &[&str] = &[ + "/", + "/metrics", + "/ping", + "/users/login", + "/users/refresh-token", + "/personal-access-tokens/login", +]; +pub async fn middleware_fn( + service: &S, + mut ctx: WebContext<'_, C>, +) -> Result> +where + S: for<'r> Service, Response = Res, Error = Error>, + C: Borrow, // annotate we want to borrow &String from generic C state type. +{ + if UNAUTHORIZED_PATHS.contains(&ctx.req().uri().path()) { + return service.call(ctx).await; + } + let bearer = ctx + .req() + .headers() + .get(AUTHORIZATION) + .ok_or(UNAUTHORIZED)? + .to_str() + .map_err(|_| UNAUTHORIZED)?; + + if !bearer.starts_with(BEARER) { + return Err(StatusCode::UNAUTHORIZED.into()); + } + let jwt_token = &bearer[BEARER.len()..]; + let token_header = jsonwebtoken::decode_header(jwt_token).map_err(|_| UNAUTHORIZED)?; + let jwt_claims = ctx + .state() + .borrow() + .jwt_manager + .decode(jwt_token, token_header.alg) + .map_err(|_| UNAUTHORIZED)?; + if ctx + .state() + .borrow() + .jwt_manager + .is_token_revoked(&jwt_claims.claims.jti) + .await + { + return Err(StatusCode::UNAUTHORIZED.into()); + } + let request_details = ctx.req().extensions().get::().unwrap(); + let identity = Identity { + token_id: jwt_claims.claims.jti, + token_expiry: jwt_claims.claims.exp, + user_id: jwt_claims.claims.sub, + ip_address: request_details.ip_address, + }; + ctx.req_mut().extensions_mut().insert(identity); + // // WebContext::state would return &C then we can call Borrow::borrow on it to get &String + // let _appstate = ctx.state().borrow(); + // // or use extractor manually like in function service. + // let _appstate = StateRef::<'_, AppState>::from_request(&ctx).await?; + // // service.call(ctx).await + service.call(ctx).await.map(|res| { + // tracing::info!("middleware_fn: response status: "); + res + }) +} diff --git a/src/http/xitcav_http/jwt/mod.rs b/src/http/xitcav_http/jwt/mod.rs new file mode 100644 index 0000000..0d45001 --- /dev/null +++ b/src/http/xitcav_http/jwt/mod.rs @@ -0,0 +1 @@ +pub mod middlewarex; diff --git a/src/http/xitcav_http/metrics.rs b/src/http/xitcav_http/metrics.rs new file mode 100644 index 0000000..ff1b9d0 --- /dev/null +++ b/src/http/xitcav_http/metrics.rs @@ -0,0 +1,22 @@ +use crate::http::shared::AppState; +use std::borrow::Borrow; +use xitca_web::http::WebResponse; +use xitca_web::middleware::sync::Next; +use xitca_web::WebContext; + +pub fn metricsx(next: &mut Next, ctx: WebContext<'_, C>) -> Result, E> +where + // S: for<'r> Service, Response = WebResponse, Error = Error>, + C: Borrow, // annotate we want to borrow &String from generic C state type. +{ + ctx.state() + .borrow() + .system + .read() + .metrics + .increment_http_requests(); + next.call(ctx).map(|res| { + // tracing::info!("metricx: response status: {}", res.status()); + res + }) +} diff --git a/src/http/xitcav_http/mod.rs b/src/http/xitcav_http/mod.rs new file mode 100644 index 0000000..6b5b7c6 --- /dev/null +++ b/src/http/xitcav_http/mod.rs @@ -0,0 +1,7 @@ +pub mod diagnostics; +pub mod http_server; +pub mod jwt; +pub mod metrics; +pub mod request_limits; +pub mod system; +pub mod users; diff --git a/src/http/xitcav_http/request_limits.rs b/src/http/xitcav_http/request_limits.rs new file mode 100644 index 0000000..9cf8bc3 --- /dev/null +++ b/src/http/xitcav_http/request_limits.rs @@ -0,0 +1,327 @@ +use std::{ + collections::{HashMap, HashSet}, + fmt, + net::{IpAddr, SocketAddr}, + sync::{Arc, Mutex}, + time::{Duration, Instant, SystemTime}, +}; + +use xitca_http::http::{HeaderValue, StatusCode}; +use xitca_io::net::Stream; +use xitca_web::{ + error::Error, + handler::Responder, + http::header::{HeaderMap, HeaderName}, + http::WebResponse, + service::{Service, ServiceExt}, + WebContext, +}; + +const X_FORWARDED_FOR: &str = "x-forwarded-for"; + +pub async fn request_limit( + service: &S, + ctx: WebContext<'_, C>, +) -> Result> +where + S: for<'r> Service, Response = WebResponse, Error = Error>, +{ + let addr = get_client_addr(ctx.req().headers()); + // let addr = *ctx.req().body().socket_addr(); + + // rate limit based on client addr + if check_addr(&addr) { + return StatusCode::TOO_MANY_REQUESTS.respond(ctx).await; + } + + service.call(ctx).await +} + +pub async fn connection_limit(service: &S, conn: Stream) -> Result +where + S: Service, +{ + match &conn { + Stream::Tcp(_, addr) => { + // drop connection on condition. + if check_addr(&addr) { + return Ok(()); + } + + // delay handling on condition. + if check_addr(&addr) { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + } + _ => {} + } + + service.call(conn).await +} + +// arbitrary function for checking client address +// fn check_addr(_: &std::net::SocketAddr) -> bool { +// true +// } +fn check_addr(addr: &SocketAddr) -> bool { + let rate_limiter = Arc::new(Mutex::new(Server::from_token())); + rate_limiter.lock().unwrap().client_connected(*addr); + if rate_limiter.lock().unwrap().client_read(*addr) { + // tracing::info!("Request from {} allowed h", addr); + rate_limiter.lock().unwrap().update(*addr); + false + } else { + tracing::info!("Request from {} blocked due to rate limit", addr); + // rate_limiter.lock().unwrap().update(*addr); + true + } +} + +fn get_client_addr(headers: &HeaderMap) -> SocketAddr { + if let Some(header_value) = headers.get(&HeaderName::from_static(X_FORWARDED_FOR)) { + if let Ok(addr) = header_value.to_str() { + if let Ok(parsed_addr) = addr.parse() { + // tracing::info!("Request from {} allowed", addr); + return parsed_addr; + } + } + } + SocketAddr::new( + std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), + 0, + ) +} + +// Add these constants to define the rate limit and the duration for which a user is banned after exceeding the limit. +// const MESSAGE_RATE_LIMIT: Duration = Duration::from_secs(1); +// const RATE_LIMIT_STRIKE_LIMIT: i32 = 5; +// type CResult = std::result::Result; +const SAFE_MODE: bool = false; +const BAN_LIMIT: Duration = Duration::from_secs(1 * 6); +const MESSAGE_RATE: Duration = Duration::from_secs(1); +const SLOWLORIS_LIMIT: Duration = Duration::from_millis(200); +const STRIKE_LIMIT: usize = 10; +struct Sens(T); + +impl fmt::Display for Sens { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self(inner) = self; + if SAFE_MODE { + "[REDACTED]".fmt(f) + } else { + inner.fmt(f) + } + } +} +struct Client { + // conn: TcpStream, + last_message: SystemTime, + connected_at: SystemTime, + // authed: bool, + addr: SocketAddr, +} +enum Sinner { + Striked(usize), + Banned(SystemTime), +} + +impl Sinner { + fn new() -> Self { + Self::Striked(0) + } + + fn forgive(&mut self) { + *self = Self::Striked(0) + } + + fn strike(&mut self) -> bool { + match self { + Self::Striked(x) => { + if *x >= STRIKE_LIMIT { + *self = Self::Banned(SystemTime::now()); + true + } else { + *x += 1; + false + } + } + Self::Banned(_) => true, + } + } +} +struct Server { + clients: HashMap, + sinners: HashMap, + // token: String, +} + +impl Server { + fn from_token() -> Self { + Self { + clients: HashMap::new(), + sinners: HashMap::new(), + // token, + } + } + fn client_connected(&mut self, author_addr: SocketAddr) { + let now = SystemTime::now(); + + if let Some(sinner) = self.sinners.get_mut(&author_addr.ip()) { + match sinner { + Sinner::Banned(banned_at) => { + let diff = now.duration_since(*banned_at).unwrap_or_else(|err| { + eprintln!("ERROR: ban time check on client connection: the clock might have gone backwards: {err}"); + Duration::ZERO + }); + if diff < BAN_LIMIT { + let secs = (BAN_LIMIT - diff).as_secs_f32(); + // TODO: probably remove this logging, cause banned MFs may still keep connecting and overflow us with logs + println!("INFO: Client {author_addr} tried to connected, but that MF is banned for {secs} secs", author_addr = Sens(author_addr)); + + return; + } else { + sinner.forgive() + } + } + Sinner::Striked(_) => {} + } + } + println!( + "INFO: Client {author_addr} connected", + author_addr = Sens(author_addr) + ); + self.clients.insert( + author_addr.clone(), + Client { + // conn: author, + last_message: now - 2 * MESSAGE_RATE, + connected_at: now, + // authed: false, + addr: author_addr, + }, + ); + } + // fn new_message(&mut self, author_addr: SocketAddr) -> bool { + // if let Some(author) = self.clients.get_mut(&author_addr) { + // let now = SystemTime::now(); + // let diff = now.duration_since(author.last_message).unwrap_or_else(|err| { + // eprintln!("ERROR: message rate check on new message: the clock might have gone backwards: {err}"); + // Duration::from_secs(0) + // }); + + // if diff >= MESSAGE_RATE { + // // No need to increment the strike count if the message rate is allowed + // tracing::info!( + // "Current rate count {}/{} from address {}", + // author.strike_count, + // STRIKE_LIMIT, + // author_addr + // ); + // true + // } else { + // author.strike_count += 1; + // if author.strike_count >= STRIKE_LIMIT { + // println!( + // "INFO: Client {author_addr} got banned", + // author_addr = Sens(author_addr) + // ); + // tracing::info!( + // "Current rate count {}/{} from address {}", + // author.strike_count, + // STRIKE_LIMIT, + // author_addr + // ); + // self.banned_mfs.insert(author_addr.ip().clone(), now); + // self.clients.remove(&author_addr); + // false + // } else { + // // Incremented the strike count, but not yet banned + // tracing::info!( + // "Current rate count {}/{} from address {}", + // author.strike_count, + // STRIKE_LIMIT, + // author_addr + // ); + // false + // } + // } + // } else { + // // The client is not in the clients map + // true + // } + // } + fn strike_ip(&mut self, ip: IpAddr) { + let sinner = self.sinners.entry(ip).or_insert(Sinner::new()); + if sinner.strike() { + println!("INFO: IP {ip} got banned", ip = Sens(ip)); + self.clients.retain(|_token, client| { + let addr: SocketAddr = client.addr.clone(); + if addr.ip() == ip { + return false; + } + true + }); + } + } + fn update(&mut self, author_addr: SocketAddr) { + self.client_read(author_addr); + + // TODO: keep waiting connections in a separate hash map + self.clients.retain(|_, client| { + let addr: SocketAddr = client.addr.clone(); + // if !client.authed { + let now = SystemTime::now(); + let diff = now.duration_since(client.connected_at).unwrap_or_else(|err| { + eprintln!("ERROR: slowloris time limit check: the clock might have gone backwards: {err}"); + SLOWLORIS_LIMIT + }); + if diff >= SLOWLORIS_LIMIT { + // TODO: disconnect everyone from addr.ip() + self.sinners.entry(addr.ip()).or_insert(Sinner::new()).strike(); + return false; + } + // } + true + }); + } + fn client_read(&mut self, sauthor_addr: SocketAddr) -> bool { + if let Some(author) = self.clients.get_mut(&sauthor_addr) { + let author_addr: SocketAddr = author.addr.clone(); + let now = SystemTime::now(); + let diff = now.duration_since(author.last_message).unwrap_or_else(|err| { + eprintln!("ERROR: message rate check on new message: the clock might have gone backwards: {err}"); + Duration::from_secs(0) + }); + if diff < MESSAGE_RATE { + self.strike_ip(author_addr.ip()); + return false; + } + tracing::info!( + "Current rate count {:?}/{:?} from address {}", + diff, + MESSAGE_RATE, + author_addr + ); + self.sinners + .entry(author_addr.ip()) + .or_insert(Sinner::new()) + .forgive(); + author.last_message = now; + // TODO: let the user know that they were banned after this attempt + self.clients.remove(&sauthor_addr); + // TODO: each IP strike must be properly documented in the source code giving the reasoning + // behind it. + self.strike_ip(author_addr.ip()); + // author.authed = true; + println!("INFO: {} authorized!", Sens(author_addr)); + true + } else { + // Handle the case where the client is not in the clients map + false + } + } +} +// fn client(addr: &SocketAddr) -> CResult { +// let mut server = Server::from_token(); +// Ok(server.client_read(*addr)) +// } diff --git a/src/http/xitcav_http/system.rs b/src/http/xitcav_http/system.rs new file mode 100644 index 0000000..b25c893 --- /dev/null +++ b/src/http/xitcav_http/system.rs @@ -0,0 +1,133 @@ +use crate::configs::http::HttpMetricsConfig; +use crate::http::axum_http::jwt::json_web_token::Identity; +use crate::http::error::CustomError; +use std::{convert::Infallible, sync::Arc}; +// use crate::http::mapper; +use crate::http::shared::AppState; +use crate::infrastructure::session::Session; +// use iggy::models::client_info::{ClientInfo, ClientInfoDetails}; +use crate::models::stats::Stats; +use xitca_http::util::service::route::{get, post}; +use xitca_http::{ + h1, h3, + http::{ + const_header_value::TEXT_UTF8, header::CONTENT_TYPE, Request, RequestExt, Response, Version, + }, + ResponseBody, +}; +use xitca_web::handler::extension::ExtensionRef; +use xitca_web::handler::handler_service; +use xitca_web::handler::json::Json; +use xitca_web::handler::state::StateRef; +use xitca_web::NestApp; + +// use super::http_server::error::CustomError; +// use xitca_service::{fn_service, object, Service, ServiceExt}; + +pub const NAME: &str = "Nigiginc HTTP\n"; +pub const PONG: &str = "pong\n"; + +// pub(super) fn routes>( +// app: NestApp, +// // state: Arc, +// metrics_config: &HttpMetricsConfig, +// ) -> NestApp { +// let mut app = app +// .at("/", get(handler_service(|| async { NAME }))) +// .at("/ping", get(handler_service(|| async { PONG }))) +// .at("/stats", get(handler_service(get_stats))); + +// if metrics_config.enabled { +// app = app.at(&metrics_config.endpoint, get(handler_service(get_metrics))); +// } +// app +// } + +// pub fn app() -> NestApp { +// App::new() +// .at( +// "/index", +// handler_service(|_: &WebContext<'_, usize>| async { "Test\n" }), +// ) +// .at("/", get(handler_service(|| async { NAME }))) +// .at("/", get(handler_service(|| async { PONG }))) +// // .at("/stats", get(handler_service(get_stats))) +// .at("/metrics", get(handler_service(get_metrics))) +// } +// pub(super) fn route(app: NestApp) -> NestApp { +// app.at("/", get(handler_service(|| async { NAME }))) +// .at("/ping", get(handler_service(|| async { PONG }))) +// // .at("/stats", get(handler_service(get_stats))) +// .at("/metrics", get(handler_service(get_metrics))) +// } +pub(super) fn routes(app: NestApp>) -> NestApp> { + app.at("/", get(handler_service(|| async { NAME }))) + .at("/ping", get(handler_service(|| async { PONG }))) + .at("/metrics", get(handler_service(get_metrics))) + .at("/stats", get(handler_service(get_stats))) +} +pub async fn get_metrics( + StateRef(state): StateRef<'_, Arc>, + // ExtensionRef(state): ExtensionRef<'_, Arc>>, + // ctx: &WebContext<'_, Arc>>, +) -> Result { + // let system = ctx.state().system.read(); + let system = state.system.read(); + Ok(system.metrics.get_formatted_output()) +} +// pub fn app() -> NestApp { +// App::new().at( +// "/index", +// handler_service(|_: &WebContext<'_, usize>| async { NAME }), +// ) +// } + +pub async fn get_stats( + StateRef(state): StateRef<'_, Arc>, + ExtensionRef(identity): ExtensionRef<'_, Identity>, +) -> Result, CustomError> { + let system = state.system.read(); + let stats = system + .get_stats(&Session::stateless(identity.user_id, identity.ip_address)) + .await?; + Ok(Json(stats)) +} +pub fn router_h3(state: Arc, metrics_config: &HttpMetricsConfig) { + // ) -> Router { + // let mut router = Router::new(); + // .insert( + // "/", + // get(fn_service(handler_h3).enclosed( + // // a show case of nested enclosed middleware + // Group::new() + // .enclosed(HttpServiceBuilder::h3()) + // .enclosed(Logger::default()), + // )), + // ); + // .route("/", get(|| async { NAME })) + // .route("/ping", get(|| async { PONG })); + // .route("/stats", get(get_stats)); + // .route("/clients", get(get_clients)) + // .route("/clients/:client_id", get(get_client)); + // if metrics_config.enabled { + // router = router.route(&metrics_config.endpoint, get(get_metrics)); +} + +async fn handler_h1( + _: Request>, +) -> Result, Infallible> { + Ok(Response::builder() + .header(CONTENT_TYPE, TEXT_UTF8) + .body("Hello World from Http/1!".into()) + .unwrap()) +} +async fn handler_h3( + _: Request>, +) -> Result, Box> { + Response::builder() + .status(200) + .version(Version::HTTP_3) + .header(CONTENT_TYPE, TEXT_UTF8) + .body("Hello World from Http/3!".into()) + .map_err(Into::into) +} diff --git a/src/http/xitcav_http/users.rs b/src/http/xitcav_http/users.rs new file mode 100644 index 0000000..4ab150d --- /dev/null +++ b/src/http/xitcav_http/users.rs @@ -0,0 +1,231 @@ +use std::sync::Arc; + +use crate::http::axum_http::jwt::json_web_token::Identity; +use crate::http::error::CustomError; +use crate::http::mapper; +use crate::http::mapper::map_generated_tokens_to_identity_info; +use crate::http::shared::AppState; +use crate::infrastructure::session::Session; +use crate::models::identifier::Identifier; +use crate::models::identity_info::IdentityInfo; +use crate::models::user_info::{UserInfo, UserInfoDetails}; +use crate::models::users::change_password::ChangePassword; +use crate::models::users::create_user::CreateUser; +use crate::models::users::login_user::LoginUser; +use crate::models::users::logout_user::LogoutUser; +use crate::models::users::update_permissions::UpdatePermissions; +use crate::models::users::update_user::UpdateUser; +use crate::models::validatable::Validatable; +// use axum::extract::{Path, State}; +use xitca_http::http::StatusCode; +// use axum::routing::{get, post, put}; +// use axum::{Extension, Json, Router}; +use serde::Deserialize; +use xitca_web::handler::extension::ExtensionRef; +use xitca_web::handler::handler_service; +use xitca_web::handler::json::Json; +use xitca_web::handler::path::PathRef; +use xitca_web::handler::state::StateRef; +use xitca_web::route::{get, post, put}; +use xitca_web::NestApp; + +// pub fn router(state: Arc) -> Router { +// Router::new() +// .route("/users", get(get_users).post(create_user)) +// .route( +// "/:user_id", +// get(get_user).put(update_user).delete(delete_user), +// ) +// .route("/:user_id/permissions", put(update_permissions)) +// .route("/:user_id/password", put(change_password)) +// .route("/login", post(login_user)) +// .route("/logout", post(logout_user)) +// .route("/refresh-token", post(refresh_token)) +// .with_state(state) +// } +pub(super) fn routes(app: NestApp>) -> NestApp> { + app.at( + "/users", + get(handler_service(get_users)).post(handler_service(create_user)), + ) + .at( + "/:user_id", + get(handler_service(get_user)) + .put(handler_service(update_user)) + .delete(handler_service(delete_user)), + ) + .at( + "/:user_id/permissions", + put(handler_service(update_permissions)), + ) + .at("/:user_id/password", put(handler_service(change_password))) + .at("/login", post(handler_service(login_user))) + .at("/logout", post(handler_service(logout_user))) + .at("/refresh-token", post(handler_service(refresh_token))) +} + +pub async fn get_user( + StateRef(state): StateRef<'_, Arc>, + ExtensionRef(identity): ExtensionRef<'_, Identity>, + PathRef(user_id): PathRef<'_>, +) -> Result, CustomError> { + let user_id = Identifier::from_str_value(&user_id)?; + let system = state.system.read(); + let user = system + .find_user( + &Session::stateless(identity.user_id, identity.ip_address), + &user_id, + ) + .await?; + let user = mapper::map_user(&user); + Ok(Json(user)) +} + +pub async fn get_users( + StateRef(state): StateRef<'_, Arc>, + ExtensionRef(identity): ExtensionRef<'_, Identity>, +) -> Result>, CustomError> { + let system = state.system.read(); + let users = system + .get_users(&Session::stateless(identity.user_id, identity.ip_address)) + .await?; + let users = mapper::map_users(&users); + Ok(Json(users)) +} + +pub async fn create_user( + StateRef(state): StateRef<'_, Arc>, + ExtensionRef(identity): ExtensionRef<'_, Identity>, + Json(command): Json, +) -> Result { + command.validate()?; + let mut system = state.system.write(); + system + .create_user( + &Session::stateless(identity.user_id, identity.ip_address), + &command.username, + &command.password, + command.status, + command.permissions.clone(), + ) + .await?; + Ok(StatusCode::NO_CONTENT) +} + +pub async fn update_user( + StateRef(state): StateRef<'_, Arc>, + ExtensionRef(identity): ExtensionRef<'_, Identity>, + PathRef(user_id): PathRef<'_>, + Json(mut command): Json, +) -> Result { + command.user_id = Identifier::from_str_value(&user_id)?; + command.validate()?; + let system = state.system.read(); + system + .update_user( + &Session::stateless(identity.user_id, identity.ip_address), + &command.user_id, + command.username, + command.status, + ) + .await?; + Ok(StatusCode::NO_CONTENT) +} + +pub async fn update_permissions( + StateRef(state): StateRef<'_, Arc>, + ExtensionRef(identity): ExtensionRef<'_, Identity>, + PathRef(user_id): PathRef<'_>, + Json(mut command): Json, +) -> Result { + command.user_id = Identifier::from_str_value(&user_id)?; + command.validate()?; + let mut system = state.system.write(); + system + .update_permissions( + &Session::stateless(identity.user_id, identity.ip_address), + &command.user_id, + command.permissions, + ) + .await?; + Ok(StatusCode::NO_CONTENT) +} + +pub async fn change_password( + StateRef(state): StateRef<'_, Arc>, + ExtensionRef(identity): ExtensionRef<'_, Identity>, + PathRef(user_id): PathRef<'_>, + Json(mut command): Json, +) -> Result { + command.user_id = Identifier::from_str_value(&user_id)?; + command.validate()?; + let system = state.system.read(); + system + .change_password( + &Session::stateless(identity.user_id, identity.ip_address), + &command.user_id, + &command.current_password, + &command.new_password, + ) + .await?; + Ok(StatusCode::NO_CONTENT) +} + +pub async fn delete_user( + StateRef(state): StateRef<'_, Arc>, + ExtensionRef(identity): ExtensionRef<'_, Identity>, + PathRef(user_id): PathRef<'_>, +) -> Result { + let user_id = Identifier::from_str_value(&user_id)?; + let mut system = state.system.write(); + system + .delete_user( + &Session::stateless(identity.user_id, identity.ip_address), + &user_id, + ) + .await?; + Ok(StatusCode::NO_CONTENT) +} + +pub async fn login_user( + StateRef(state): StateRef<'_, Arc>, + Json(command): Json, +) -> Result, CustomError> { + command.validate()?; + let system = state.system.read(); + let user = system + .login_user(&command.username, &command.password, None) + .await?; + let tokens = state.jwt_manager.generate(user.id)?; + Ok(Json(map_generated_tokens_to_identity_info(tokens))) +} + +pub async fn logout_user( + StateRef(state): StateRef<'_, Arc>, + ExtensionRef(identity): ExtensionRef<'_, Identity>, + Json(command): Json, +) -> Result { + command.validate()?; + let system = state.system.read(); + system + .logout_user(&Session::stateless(identity.user_id, identity.ip_address)) + .await?; + state + .jwt_manager + .revoke_token(&identity.token_id, identity.token_expiry) + .await?; + Ok(StatusCode::NO_CONTENT) +} + +pub async fn refresh_token( + StateRef(state): StateRef<'_, Arc>, + Json(command): Json, +) -> Result, CustomError> { + let tokens = state.jwt_manager.refresh_token(&command.refresh_token)?; + Ok(Json(map_generated_tokens_to_identity_info(tokens))) +} + +#[derive(Debug, Deserialize)] +pub struct RefreshToken { + pub refresh_token: String, +} diff --git a/src/iggy/mod.rs b/src/iggy/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/infrastructure/cache/buffer.rs b/src/infrastructure/cache/buffer.rs new file mode 100644 index 0000000..9636c17 --- /dev/null +++ b/src/infrastructure/cache/buffer.rs @@ -0,0 +1,113 @@ +use super::memory_tracker::CacheMemoryTracker; +use crate::models::sizeable::Sizeable; +use std::collections::VecDeque; +use std::fmt::Debug; +use std::ops::Index; +use std::sync::Arc; + +#[derive(Debug)] +pub struct SmartCache { + current_size: u64, + buffer: VecDeque, + memory_tracker: Arc, +} + +impl SmartCache +where + T: Sizeable + Clone + Debug, +{ + pub fn new() -> Self { + let current_size = 0; + let buffer = VecDeque::new(); + let memory_tracker = CacheMemoryTracker::get_instance().unwrap(); + + Self { + current_size, + buffer, + memory_tracker, + } + } + + // Used only for cache validation tests + #[cfg(test)] + pub fn to_vec(&self) -> Vec { + let mut vec = Vec::with_capacity(self.buffer.len()); + vec.extend(self.buffer.iter().cloned()); + vec + } + + /// Pushes an element to the buffer, and if adding the element would exceed the memory limit, + /// removes the oldest elements until there's enough space for the new element. + /// It's preferred to use `extend` instead of this method. + pub fn push_safe(&mut self, element: T) { + let element_size = element.get_size_bytes() as u64; + + while !self.memory_tracker.will_fit_into_cache(element_size) { + if let Some(oldest_element) = self.buffer.pop_front() { + let oldest_size = oldest_element.get_size_bytes() as u64; + self.memory_tracker.decrement_used_memory(oldest_size); + self.current_size -= oldest_size; + } + } + + self.memory_tracker.increment_used_memory(element_size); + self.current_size += element_size; + self.buffer.push_back(element); + } + + /// Removes the oldest elements until there's enough space for the new element. + pub fn evict_by_size(&mut self, size_to_remove: u64) { + let mut removed_size = 0; + + while let Some(element) = self.buffer.pop_front() { + if removed_size >= size_to_remove { + break; + } + let elem_size = element.get_size_bytes() as u64; + self.memory_tracker.decrement_used_memory(elem_size); + self.current_size -= elem_size; + removed_size += elem_size; + } + } + + pub fn is_empty(&self) -> bool { + self.buffer.is_empty() + } + + pub fn current_size(&self) -> u64 { + self.current_size + } + + /// Extends the buffer with the given elements, and always adding the elements, + /// even if it exceeds the memory limit. + pub fn extend(&mut self, elements: impl IntoIterator) { + let elements = elements.into_iter().map(|element| { + let element_size = element.get_size_bytes() as u64; + self.memory_tracker.increment_used_memory(element_size); + self.current_size += element_size; + element + }); + self.buffer.extend(elements); + } + + pub fn len(&self) -> usize { + self.buffer.len() + } +} + +impl Index for SmartCache +where + T: Sizeable + Clone + Debug, +{ + type Output = T; + + fn index(&self, index: usize) -> &Self::Output { + &self.buffer[index] + } +} + +impl Default for SmartCache { + fn default() -> Self { + Self::new() + } +} diff --git a/src/infrastructure/cache/memory_tracker.rs b/src/infrastructure/cache/memory_tracker.rs new file mode 100644 index 0000000..89beab0 --- /dev/null +++ b/src/infrastructure/cache/memory_tracker.rs @@ -0,0 +1,101 @@ +extern crate sysinfo; + +use crate::configs::resource_quota::MemoryResourceQuota; +use crate::configs::system::CacheConfig; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Once}; +use sysinfo::System; +use tracing::info; + +static ONCE: Once = Once::new(); +static mut INSTANCE: Option> = None; + +#[derive(Debug)] +pub struct CacheMemoryTracker { + used_memory_bytes: AtomicU64, + limit_bytes: u64, +} + +type MessageSize = u64; + +impl CacheMemoryTracker { + pub fn initialize(config: &CacheConfig) -> Option> { + unsafe { + ONCE.call_once(|| { + if config.enabled { + INSTANCE = Some(Arc::new(CacheMemoryTracker::new(config.size.clone()))); + info!("Cache memory tracker initialized"); + } else { + INSTANCE = None; + info!("Cache memory tracker disabled"); + } + }); + INSTANCE.clone() + } + } + + pub fn get_instance() -> Option> { + unsafe { INSTANCE.clone() } + } + + fn new(limit: MemoryResourceQuota) -> Self { + let mut sys = System::new_all(); + sys.refresh_all(); + + let total_memory_bytes = sys.total_memory(); + let free_memory = sys.free_memory(); + let free_memory_percentage = free_memory as f64 / total_memory_bytes as f64 * 100.0; + let used_memory_bytes = AtomicU64::new(0); + let limit_bytes = limit.into(); + + info!( + "Cache memory tracker started, cache: {} bytes, total memory: {} bytes, free memory: {} bytes, free memory percentage: {:.2}%", + limit_bytes, total_memory_bytes, free_memory, free_memory_percentage + ); + + CacheMemoryTracker { + used_memory_bytes, + limit_bytes, + } + } + + pub fn increment_used_memory(&self, message_size: MessageSize) { + let mut current_cache_size_bytes = self.used_memory_bytes.load(Ordering::SeqCst); + loop { + let new_size = current_cache_size_bytes + message_size; + match self.used_memory_bytes.compare_exchange_weak( + current_cache_size_bytes, + new_size, + Ordering::SeqCst, + Ordering::SeqCst, + ) { + Ok(_) => break, + Err(actual_current) => current_cache_size_bytes = actual_current, + } + } + } + + pub fn decrement_used_memory(&self, message_size: MessageSize) { + let mut current_cache_size_bytes = self.used_memory_bytes.load(Ordering::SeqCst); + loop { + let new_size = current_cache_size_bytes - message_size; + match self.used_memory_bytes.compare_exchange_weak( + current_cache_size_bytes, + new_size, + Ordering::SeqCst, + Ordering::SeqCst, + ) { + Ok(_) => return, + Err(actual_current) => current_cache_size_bytes = actual_current, + } + } + } + + pub fn usage_bytes(&self) -> u64 { + self.used_memory_bytes.load(Ordering::SeqCst) + } + + pub fn will_fit_into_cache(&self, requested_size: u64) -> bool { + self.used_memory_bytes.load(Ordering::SeqCst) + requested_size <= self.limit_bytes + } +} diff --git a/src/infrastructure/cache/mod.rs b/src/infrastructure/cache/mod.rs new file mode 100644 index 0000000..074d852 --- /dev/null +++ b/src/infrastructure/cache/mod.rs @@ -0,0 +1,2 @@ +pub mod buffer; +pub mod memory_tracker; diff --git a/src/infrastructure/clients/client_manager.rs b/src/infrastructure/clients/client_manager.rs new file mode 100644 index 0000000..e44f31a --- /dev/null +++ b/src/infrastructure/clients/client_manager.rs @@ -0,0 +1,126 @@ +use crate::infrastructure::error::Error; +use crate::infrastructure::utils::hash; +use crate::models::user_info::UserId; +use std::collections::HashMap; +use std::fmt::{Display, Formatter}; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::sync::RwLock; + +#[derive(Debug, Default)] +pub struct ClientManager { + clients: HashMap>>, +} + +#[derive(Debug)] +pub struct Client { + pub client_id: u32, + pub user_id: Option, + pub address: SocketAddr, + pub transport: Transport, + // pub consumer_groups: Vec, +} + +// #[derive(Debug)] +// pub struct ConsumerGroup { +// pub stream_id: u32, +// pub topic_id: u32, +// pub consumer_group_id: u32, +// } + +#[derive(Debug, Clone, Copy)] +pub enum Transport { + Tcp, + Quic, +} + +impl Display for Transport { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Transport::Tcp => write!(f, "TCP"), + Transport::Quic => write!(f, "QUIC"), + } + } +} + +impl ClientManager { + pub fn add_client(&mut self, address: &SocketAddr, transport: Transport) -> u32 { + let id = hash::calculate_32(address.to_string().as_bytes()); + let client = Client { + client_id: id, + user_id: None, + address: *address, + transport, + // consumer_groups: Vec::new(), + }; + self.clients + .insert(client.client_id, Arc::new(RwLock::new(client))); + id + } + + pub async fn set_user_id(&mut self, client_id: u32, user_id: UserId) -> Result<(), Error> { + let client = self.clients.get(&client_id); + if client.is_none() { + return Err(Error::ClientNotFound(client_id)); + } + + let mut client = client.unwrap().write().await; + client.user_id = Some(user_id); + Ok(()) + } + + pub async fn clear_user_id(&mut self, client_id: u32) -> Result<(), Error> { + let client = self.clients.get(&client_id); + if client.is_none() { + return Err(Error::ClientNotFound(client_id)); + } + + let mut client = client.unwrap().write().await; + client.user_id = None; + Ok(()) + } + + pub fn get_client_by_address( + &self, + address: &SocketAddr, + ) -> Result>, Error> { + let id = hash::calculate_32(address.to_string().as_bytes()); + self.get_client_by_id(id) + } + + pub fn get_client_by_id(&self, client_id: u32) -> Result>, Error> { + let client = self.clients.get(&client_id); + if client.is_none() { + return Err(Error::ClientNotFound(client_id)); + } + + Ok(client.unwrap().clone()) + } + + pub fn get_clients(&self) -> Vec>> { + self.clients.values().cloned().collect() + } + + pub async fn delete_clients_for_user(&mut self, user_id: UserId) -> Result<(), Error> { + let mut clients_to_remove = Vec::new(); + for client in self.clients.values() { + let client = client.read().await; + if let Some(client_user_id) = client.user_id { + if client_user_id == user_id { + clients_to_remove.push(client.client_id); + } + } + } + + for client_id in clients_to_remove { + self.clients.remove(&client_id); + } + + Ok(()) + } + + pub fn delete_client(&mut self, address: &SocketAddr) -> Option>> { + let id = hash::calculate_32(address.to_string().as_bytes()); + self.clients.remove(&id) + } +} diff --git a/src/infrastructure/clients/mod.rs b/src/infrastructure/clients/mod.rs new file mode 100644 index 0000000..25d82df --- /dev/null +++ b/src/infrastructure/clients/mod.rs @@ -0,0 +1 @@ +pub mod client_manager; diff --git a/src/infrastructure/diagnostics/metrics.rs b/src/infrastructure/diagnostics/metrics.rs new file mode 100644 index 0000000..005e296 --- /dev/null +++ b/src/infrastructure/diagnostics/metrics.rs @@ -0,0 +1,68 @@ +use prometheus_client::encoding::text::encode; +use prometheus_client::metrics::counter::Counter; +use prometheus_client::metrics::gauge::Gauge; +use prometheus_client::registry::Registry; +use tracing::error; + +#[derive(Debug)] +pub(crate) struct Metrics { + registry: Registry, + http_requests: Counter, + users: Gauge, + clients: Gauge, +} + +impl Metrics { + pub fn init() -> Self { + let mut metrics = Metrics { + registry: ::default(), + http_requests: Counter::default(), + users: Gauge::default(), + clients: Gauge::default(), + }; + + metrics.register_counter("http_requests", metrics.http_requests.clone()); + metrics.register_gauge("users", metrics.users.clone()); + metrics.register_gauge("clients", metrics.clients.clone()); + + metrics + } + + fn register_counter(&mut self, name: &str, counter: Counter) { + self.registry + .register(name, format!("total count of {name}"), counter) + } + + fn register_gauge(&mut self, name: &str, gauge: Gauge) { + self.registry + .register(name, format!("total count of {name}"), gauge) + } + + pub fn get_formatted_output(&self) -> String { + let mut buffer = String::new(); + if let Err(err) = encode(&mut buffer, &self.registry) { + error!("Failed to encode metrics: {}", err); + } + buffer + } + + pub fn increment_http_requests(&self) { + self.http_requests.inc(); + } + + pub fn increment_users(&self, count: u32) { + self.users.inc_by(count as i64); + } + + pub fn decrement_users(&self, count: u32) { + self.users.dec_by(count as i64); + } + + pub fn increment_clients(&self, count: u32) { + self.clients.inc_by(count as i64); + } + + pub fn decrement_clients(&self, count: u32) { + self.clients.dec_by(count as i64); + } +} diff --git a/src/infrastructure/diagnostics/mod.rs b/src/infrastructure/diagnostics/mod.rs new file mode 100644 index 0000000..e144883 --- /dev/null +++ b/src/infrastructure/diagnostics/mod.rs @@ -0,0 +1 @@ +pub mod metrics; diff --git a/src/infrastructure/error.rs b/src/infrastructure/error.rs new file mode 100644 index 0000000..f373900 --- /dev/null +++ b/src/infrastructure/error.rs @@ -0,0 +1,332 @@ +use quinn::{ConnectionError, ReadError, ReadToEndError, WriteError}; +use std::array::TryFromSliceError; +use std::net::AddrParseError; +use std::num::ParseIntError; +use std::str::Utf8Error; +use thiserror::Error; +use tokio::io; + +use strum::{EnumDiscriminants, FromRepr, IntoStaticStr}; + +#[derive(Debug, Error, EnumDiscriminants, IntoStaticStr)] +#[repr(u32)] +#[strum(serialize_all = "snake_case")] +#[strum_discriminants( + vis(pub(crate)), + derive(FromRepr, IntoStaticStr), + strum(serialize_all = "snake_case") +)] +pub enum Error { + #[error("Error")] + Error = 1, + #[error("Invalid configuration")] + InvalidConfiguration = 2, + #[error("Invalid command")] + InvalidCommand = 3, + #[error("Invalid format")] + InvalidFormat = 4, + #[error("Feature is unavailable")] + FeatureUnavailable = 5, + #[error("Cannot create base directory, Path: {0}")] + CannotCreateBaseDirectory(String) = 10, + #[error("Cannot create runtime directory, Path: {0}")] + CannotCreateRuntimeDirectory(String) = 11, + #[error("Cannot remove runtime directory, Path: {0}")] + CannotRemoveRuntimeDirectory(String) = 12, + #[error("Resource with key: {0} was not found.")] + ResourceNotFound(String) = 20, + #[error("Cannot load resource. Reason: {0:#}")] + CannotLoadResource(#[source] anyhow::Error) = 21, + #[error("Cannot save resource. Reason: {0:#}")] + CannotSaveResource(#[source] anyhow::Error) = 22, + #[error("Cannot delete resource. Reason: {0:#}")] + CannotDeleteResource(#[source] anyhow::Error) = 23, + #[error("Cannot serialize resource. Reason: {0:#}")] + CannotSerializeResource(#[source] anyhow::Error) = 24, + #[error("Cannot deserialize resource. Reason: {0:#}")] + CannotDeserializeResource(#[source] anyhow::Error) = 25, + #[error("Unauthenticated")] + Unauthenticated = 40, + #[error("Unauthorized")] + Unauthorized = 41, + #[error("Invalid credentials")] + InvalidCredentials = 42, + #[error("Invalid username")] + InvalidUsername = 43, + #[error("Invalid password")] + InvalidPassword = 44, + #[error("Invalid user status")] + InvalidUserStatus = 45, + #[error("User already exists")] + UserAlreadyExists = 46, + #[error("User inactive")] + UserInactive = 47, + #[error("Cannot delete user with ID: {0}")] + CannotDeleteUser(u32) = 48, + #[error("Cannot change permissions for user with ID: {0}")] + CannotChangePermissions(u32) = 49, + #[error("Invalid personal access token name")] + InvalidPersonalAccessTokenName = 50, + #[error("Personal access token: {0} for user with ID: {1} already exists")] + PersonalAccessTokenAlreadyExists(String, u32) = 51, + #[error("User with ID: {0} has reached the maximum number of personal access tokens: {1}")] + PersonalAccessTokensLimitReached(u32, u32) = 52, + #[error("Invalid personal access token")] + InvalidPersonalAccessToken = 53, + #[error("Personal access token: {0} for user with ID: {1} has expired.")] + PersonalAccessTokenExpired(String, u32) = 54, + #[error("Not connected")] + NotConnected = 61, + #[error("Request error")] + RequestError(#[from] reqwest::Error) = 62, + #[error("Invalid encryption key")] + InvalidEncryptionKey = 70, + #[error("Cannot encrypt data")] + CannotEncryptData = 71, + #[error("Cannot decrypt data")] + CannotDecryptData = 72, + #[error("Invalid JWT algorithm: {0}")] + InvalidJwtAlgorithm(String) = 73, + #[error("Invalid JWT secret")] + InvalidJwtSecret = 74, + #[error("JWT is missing")] + JwtMissing = 75, + #[error("Cannot generate JWT")] + CannotGenerateJwt = 76, + #[error("Refresh token is missing")] + RefreshTokenMissing = 77, + #[error("Invalid refresh token")] + InvalidRefreshToken = 78, + #[error("Refresh token expired")] + RefreshTokenExpired = 79, + #[error("Client with ID: {0} was not found.")] + ClientNotFound(u32) = 100, + #[error("Invalid client ID")] + InvalidClientId = 101, + #[error("IO error")] + IoError(#[from] std::io::Error) = 200, + #[error("Write error")] + WriteError(#[from] quinn::WriteError) = 201, + #[error("Cannot parse UTF8")] + CannotParseUtf8(#[from] std::str::Utf8Error) = 202, + #[error("Cannot parse integer")] + CannotParseInt(#[from] std::num::ParseIntError) = 203, + #[error("Cannot parse integer")] + CannotParseSlice(#[from] std::array::TryFromSliceError) = 204, + #[error("Cannot parse byte unit")] + CannotParseByteUnit(#[from] byte_unit::ParseError) = 205, + #[error("HTTP response error, status: {0}, body: {1}")] + HttpResponseError(u16, String) = 300, + #[error("Request middleware error")] + RequestMiddlewareError(#[from] reqwest_middleware::Error) = 301, + #[error("Cannot create endpoint")] + CannotCreateEndpoint = 302, + #[error("Cannot parse URL")] + CannotParseUrl = 303, + #[error("Invalid response: {0}")] + InvalidResponse(u32) = 304, + #[error("Empty response")] + EmptyResponse = 305, + #[error("Cannot parse address")] + CannotParseAddress(#[from] std::net::AddrParseError) = 306, + #[error("Read error")] + ReadError(#[from] quinn::ReadError) = 307, + #[error("Connection error")] + ConnectionError(#[from] quinn::ConnectionError) = 308, + #[error("Read to end error")] + ReadToEndError(#[from] quinn::ReadToEndError) = 309, + #[error("Cannot create streams directory, Path: {0}")] + CannotCreateStreamsDirectory(String) = 1000, + #[error("Cannot create stream with ID: {0} directory, Path: {1}")] + CannotCreateStreamDirectory(u32, String) = 1001, + #[error("Failed to create stream info file for stream with ID: {0}")] + CannotCreateStreamInfo(u32) = 1002, + #[error("Failed to update stream info for stream with ID: {0}")] + CannotUpdateStreamInfo(u32) = 1003, + #[error("Failed to open stream info file for stream with ID: {0}")] + CannotOpenStreamInfo(u32) = 1004, + #[error("Failed to read stream info file for stream with ID: {0}")] + CannotReadStreamInfo(u32) = 1005, + #[error("Failed to create stream with ID: {0}")] + CannotCreateStream(u32) = 1006, + #[error("Failed to delete stream with ID: {0}")] + CannotDeleteStream(u32) = 1007, + #[error("Failed to delete stream directory with ID: {0}")] + CannotDeleteStreamDirectory(u32) = 1008, + #[error("Stream with ID: {0} was not found.")] + StreamIdNotFound(u32) = 1009, + #[error("Stream with name: {0} was not found.")] + StreamNameNotFound(String) = 1010, + #[error("Stream with ID: {0} already exists.")] + StreamIdAlreadyExists(u32) = 1011, + #[error("Stream with name: {0} already exists.")] + StreamNameAlreadyExists(String) = 1012, + #[error("Invalid stream name")] + InvalidStreamName = 1013, + #[error("Invalid stream ID")] + InvalidStreamId = 1014, + #[error("Cannot read streams")] + CannotReadStreams = 1015, + #[error("Cannot create topics directory for stream with ID: {0}, Path: {1}")] + CannotCreateTopicsDirectory(u32, String) = 2000, + #[error( + "Failed to create directory for topic with ID: {0} for stream with ID: {1}, Path: {2}" + )] + CannotCreateTopicDirectory(u32, u32, String) = 2001, + #[error("Failed to create topic info file for topic with ID: {0} for stream with ID: {1}.")] + CannotCreateTopicInfo(u32, u32) = 2002, + #[error("Failed to update topic info for topic with ID: {0} for stream with ID: {1}.")] + CannotUpdateTopicInfo(u32, u32) = 2003, + #[error("Failed to open topic info file for topic with ID: {0} for stream with ID: {1}.")] + CannotOpenTopicInfo(u32, u32) = 2004, + #[error("Failed to read topic info file for topic with ID: {0} for stream with ID: {1}.")] + CannotReadTopicInfo(u32, u32) = 2005, + #[error("Failed to create topic with ID: {0} for stream with ID: {1}.")] + CannotCreateTopic(u32, u32) = 2006, + #[error("Failed to delete topic with ID: {0} for stream with ID: {1}.")] + CannotDeleteTopic(u32, u32) = 2007, + #[error("Failed to delete topic directory with ID: {0} for stream with ID: {1}, Path: {2}")] + CannotDeleteTopicDirectory(u32, u32, String) = 2008, + #[error("Cannot poll topic")] + CannotPollTopic = 2009, + #[error("Topic with ID: {0} for stream with ID: {1} was not found.")] + TopicIdNotFound(u32, u32) = 2010, + #[error("Topic with name: {0} for stream with ID: {1} was not found.")] + TopicNameNotFound(String, u32) = 2011, + #[error("Topic with ID: {0} for stream with ID: {1} already exists.")] + TopicIdAlreadyExists(u32, u32) = 2012, + #[error("Topic with name: {0} for stream with ID: {1} already exists.")] + TopicNameAlreadyExists(String, u32) = 2013, + #[error("Invalid topic name")] + InvalidTopicName = 2014, + #[error("Too many partitions")] + TooManyPartitions = 2015, + #[error("Invalid topic ID")] + InvalidTopicId = 2016, + #[error("Cannot read topics for stream with ID: {0}")] + CannotReadTopics(u32) = 2017, + #[error("Invalid replication factor")] + InvalidReplicationFactor = 2018, + #[error("Cannot create partition with ID: {0} for stream with ID: {1} and topic with ID: {2}")] + CannotCreatePartition(u32, u32, u32) = 3000, + #[error( + "Failed to create directory for partitions for stream with ID: {0} and topic with ID: {1}" + )] + CannotCreatePartitionsDirectory(u32, u32) = 3001, + #[error("Failed to create directory for partition with ID: {0} for stream with ID: {1} and topic with ID: {2}")] + CannotCreatePartitionDirectory(u32, u32, u32) = 3002, + #[error("Cannot open partition log file")] + CannotOpenPartitionLogFile = 3003, + #[error("Cannot read partitions directories. Reason: {0:#}")] + CannotReadPartitions(#[source] anyhow::Error) = 3004, + #[error( + "Failed to delete partition with ID: {0} for stream with ID: {1} and topic with ID: {2}" + )] + CannotDeletePartition(u32, u32, u32) = 3005, + #[error("Failed to delete partition directory with ID: {0} for stream with ID: {1} and topic with ID: {2}")] + CannotDeletePartitionDirectory(u32, u32, u32) = 3006, + #[error( + "Partition with ID: {0} for topic with ID: {1} for stream with ID: {2} was not found." + )] + PartitionNotFound(u32, u32, u32) = 3007, + #[error("Topic with ID: {0} for stream with ID: {1} has no partitions.")] + NoPartitions(u32, u32) = 3008, + #[error("Segment not found")] + SegmentNotFound = 4000, + #[error("Segment with start offset: {0} and partition with ID: {1} is closed")] + SegmentClosed(u64, u32) = 4001, + #[error("Segment size is invalid")] + InvalidSegmentSize(u64) = 4002, + #[error("Failed to create segment log file for Path: {0}.")] + CannotCreateSegmentLogFile(String) = 4003, + #[error("Failed to create segment index file for Path: {0}.")] + CannotCreateSegmentIndexFile(String) = 4004, + #[error("Failed to create segment time index file for Path: {0}.")] + CannotCreateSegmentTimeIndexFile(String) = 4005, + #[error("Cannot save messages to segment. Reason: {0:#}")] + CannotSaveMessagesToSegment(#[source] anyhow::Error) = 4006, + #[error("Cannot save index to segment. Reason: {0:#}")] + CannotSaveIndexToSegment(#[source] anyhow::Error) = 4007, + #[error("Cannot save time index to segment. Reason: {0:#}")] + CannotSaveTimeIndexToSegment(#[source] anyhow::Error) = 4008, + #[error("Invalid messages count")] + InvalidMessagesCount = 4009, + #[error("Cannot append message")] + CannotAppendMessage = 4010, + #[error("Cannot read message")] + CannotReadMessage = 4011, + #[error("Cannot read message ID")] + CannotReadMessageId = 4012, + #[error("Cannot read message state")] + CannotReadMessageState = 4013, + #[error("Cannot read message timestamp")] + CannotReadMessageTimestamp = 4014, + #[error("Cannot read headers length")] + CannotReadHeadersLength = 4015, + #[error("Cannot read headers payload")] + CannotReadHeadersPayload = 4016, + #[error("Too big headers payload")] + TooBigHeadersPayload = 4017, + #[error("Invalid header key")] + InvalidHeaderKey = 4018, + #[error("Invalid header value")] + InvalidHeaderValue = 4019, + #[error("Cannot read message length")] + CannotReadMessageLength = 4020, + #[error("Cannot save messages to segment")] + CannotReadMessagePayload = 4021, + #[error("Too big message payload")] + TooBigMessagePayload = 4022, + #[error("Too many messages")] + TooManyMessages = 4023, + #[error("Empty message payload")] + EmptyMessagePayload = 4024, + #[error("Invalid message payload length")] + InvalidMessagePayloadLength = 4025, + #[error("Cannot read message checksum")] + CannotReadMessageChecksum = 4026, + #[error("Invalid message checksum: {0}, expected: {1}, for offset: {2}")] + InvalidMessageChecksum(u32, u32, u64) = 4027, + #[error("Invalid key value length")] + InvalidKeyValueLength = 4028, + #[error("Invalid offset: {0}")] + InvalidOffset(u64) = 4100, + #[error("Failed to read consumers offsets for partition with ID: {0}")] + CannotReadConsumerOffsets(u32) = 4101, + #[error("Consumer group with ID: {0} for topic with ID: {1} was not found.")] + ConsumerGroupIdNotFound(u32, u32) = 5000, + #[error("Consumer group with ID: {0} for topic with ID: {1} already exists.")] + ConsumerGroupIdAlreadyExists(u32, u32) = 5001, + #[error("Invalid consumer group ID")] + InvalidConsumerGroupId = 5002, + #[error("Consumer group with name: {0} for topic with ID: {1} was not found.")] + ConsumerGroupNameNotFound(String, u32) = 5003, + #[error("Consumer group with name: {0} for topic with ID: {1} already exists.")] + ConsumerGroupNameAlreadyExists(String, u32) = 5004, + #[error("Invalid consumer group name")] + InvalidConsumerGroupName = 5005, + #[error("Consumer group member with ID: {0} for group with ID: {1} for topic with ID: {2} was not found.")] + ConsumerGroupMemberNotFound(u32, u32, u32) = 5006, + #[error("Failed to create consumer group info file for ID: {0} for topic with ID: {1} for stream with ID: {2}.")] + CannotCreateConsumerGroupInfo(u32, u32, u32) = 5007, + #[error("Failed to delete consumer group info file for ID: {0} for topic with ID: {1} for stream with ID: {2}.")] + CannotDeleteConsumerGroupInfo(u32, u32, u32) = 5008, +} + +impl Error { + pub fn as_code(&self) -> u32 { + // SAFETY: SdkError specifies #[repr(u32)] representation. + // https://doc.rust-lang.org/reference/items/enumerations.html#pointer-casting + unsafe { *(self as *const Self as *const u32) } + } + + pub fn as_string(&self) -> &'static str { + self.into() + } + + // pub fn from_code_as_string(code: u32) -> &'static str { + // IggyErrorDiscriminants::from_repr(code) + // .map(|discriminant| discriminant.into()) + // .unwrap_or("unknown error code") + // } +} diff --git a/src/infrastructure/mod.rs b/src/infrastructure/mod.rs new file mode 100644 index 0000000..1cf4e89 --- /dev/null +++ b/src/infrastructure/mod.rs @@ -0,0 +1,11 @@ +pub mod cache; +pub mod clients; +pub mod diagnostics; +pub mod error; +pub mod persistence; +pub mod personal_access_tokens; +pub mod session; +pub mod storage; +pub mod systems; +pub mod users; +pub mod utils; diff --git a/src/infrastructure/persistence/mod.rs b/src/infrastructure/persistence/mod.rs new file mode 100644 index 0000000..cc7df7b --- /dev/null +++ b/src/infrastructure/persistence/mod.rs @@ -0,0 +1 @@ +pub mod persister; diff --git a/src/infrastructure/persistence/persister.rs b/src/infrastructure/persistence/persister.rs new file mode 100644 index 0000000..b320f08 --- /dev/null +++ b/src/infrastructure/persistence/persister.rs @@ -0,0 +1,75 @@ +use crate::infrastructure::error::Error; +use crate::infrastructure::utils::file; +use async_trait::async_trait; +use std::fmt::Debug; +use tokio::fs; +use tokio::io::AsyncWriteExt; + +#[async_trait] +pub trait Persister: Sync + Send { + async fn append(&self, path: &str, bytes: &[u8]) -> Result<(), Error>; + async fn overwrite(&self, path: &str, bytes: &[u8]) -> Result<(), Error>; + async fn delete(&self, path: &str) -> Result<(), Error>; +} + +impl Debug for dyn Persister { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Persister") + .field("type", &"Persister") + .finish() + } +} + +#[derive(Debug)] +pub struct FilePersister; + +#[derive(Debug)] +pub struct FileWithSyncPersister; + +unsafe impl Send for FilePersister {} +unsafe impl Sync for FilePersister {} + +unsafe impl Send for FileWithSyncPersister {} +unsafe impl Sync for FileWithSyncPersister {} + +#[async_trait] +impl Persister for FilePersister { + async fn append(&self, path: &str, bytes: &[u8]) -> Result<(), Error> { + let mut file = file::append(path).await?; + file.write_all(bytes).await?; + Ok(()) + } + + async fn overwrite(&self, path: &str, bytes: &[u8]) -> Result<(), Error> { + let mut file = file::write(path).await?; + file.write_all(bytes).await?; + Ok(()) + } + + async fn delete(&self, path: &str) -> Result<(), Error> { + fs::remove_file(path).await?; + Ok(()) + } +} + +#[async_trait] +impl Persister for FileWithSyncPersister { + async fn append(&self, path: &str, bytes: &[u8]) -> Result<(), Error> { + let mut file = file::append(path).await?; + file.write_all(bytes).await?; + file.sync_all().await?; + Ok(()) + } + + async fn overwrite(&self, path: &str, bytes: &[u8]) -> Result<(), Error> { + let mut file = file::write(path).await?; + file.write_all(bytes).await?; + file.sync_all().await?; + Ok(()) + } + + async fn delete(&self, path: &str) -> Result<(), Error> { + fs::remove_file(path).await?; + Ok(()) + } +} diff --git a/src/infrastructure/personal_access_tokens/mod.rs b/src/infrastructure/personal_access_tokens/mod.rs new file mode 100644 index 0000000..52fe20f --- /dev/null +++ b/src/infrastructure/personal_access_tokens/mod.rs @@ -0,0 +1,2 @@ +pub mod personal_access_token; +pub mod storage; diff --git a/src/infrastructure/personal_access_tokens/personal_access_token.rs b/src/infrastructure/personal_access_tokens/personal_access_token.rs new file mode 100644 index 0000000..709f151 --- /dev/null +++ b/src/infrastructure/personal_access_tokens/personal_access_token.rs @@ -0,0 +1,79 @@ +use crate::infrastructure::utils::hash; +use crate::models::user_info::UserId; +use crate::utils::text::as_base64; +use ring::rand::SecureRandom; +use serde::{Deserialize, Serialize}; + +const SIZE: usize = 50; + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +pub struct PersonalAccessToken { + pub user_id: UserId, + pub name: String, + pub token: String, + pub expiry: Option, +} + +#[allow(dead_code)] +impl PersonalAccessToken { + // Raw token is generated and returned only once + pub fn new(user_id: UserId, name: &str, now: u64, expiry: Option) -> (Self, String) { + let mut buffer: [u8; SIZE] = [0; SIZE]; + let system_random = ring::rand::SystemRandom::new(); + system_random.fill(&mut buffer).unwrap(); + let token = as_base64(&buffer); + let token_hash = Self::hash_token(&token); + let expiry = expiry.map(|e| now + e as u64 * 1_000_000); + ( + Self { + user_id, + name: name.to_string(), + token: token_hash, + expiry, + }, + token, + ) + } + + pub fn is_expired(&self, now: u64) -> bool { + match self.expiry { + Some(expiry) => now > expiry, + None => false, + } + } + + pub fn hash_token(token: &str) -> String { + hash::calculate_256(token.as_bytes()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::utils::timestamp::NigigTimeStamp; + #[test] + fn personal_access_token_should_be_created_with_random_secure_value_and_hashed_successfully() { + let user_id = 1; + let now = NigigTimeStamp::now().to_micros(); + let name = "test_token"; + let (personal_access_token, raw_token) = PersonalAccessToken::new(user_id, name, now, None); + assert_eq!(personal_access_token.name, name); + assert!(!personal_access_token.token.is_empty()); + assert!(!raw_token.is_empty()); + assert_ne!(personal_access_token.token, raw_token); + assert_eq!( + personal_access_token.token, + PersonalAccessToken::hash_token(&raw_token) + ); + } + + #[test] + fn personal_access_token_should_be_expired_given_passed_expiry() { + let user_id = 1; + let now = NigigTimeStamp::now().to_micros(); + let expiry = 1; + let name = "test_token"; + let (personal_access_token, _) = PersonalAccessToken::new(user_id, name, now, Some(expiry)); + assert!(personal_access_token.is_expired(now + expiry as u64 * 1_000_000 + 1)); + } +} diff --git a/src/infrastructure/personal_access_tokens/storage.rs b/src/infrastructure/personal_access_tokens/storage.rs new file mode 100644 index 0000000..0ac43e9 --- /dev/null +++ b/src/infrastructure/personal_access_tokens/storage.rs @@ -0,0 +1,210 @@ +use crate::infrastructure::error::Error; +use crate::infrastructure::personal_access_tokens::personal_access_token::PersonalAccessToken; +use crate::infrastructure::storage::{PersonalAccessTokenStorage, Storage}; +use crate::models::user_info::UserId; +use anyhow::Context; +use async_trait::async_trait; +use sled::Db; +use std::str::from_utf8; +use std::sync::Arc; +use tracing::info; + +const KEY_PREFIX: &str = "personal_access_token"; + +#[derive(Debug)] +pub struct FilePersonalAccessTokenStorage { + db: Arc, +} + +impl FilePersonalAccessTokenStorage { + pub fn new(db: Arc) -> Self { + Self { db } + } +} + +unsafe impl Send for FilePersonalAccessTokenStorage {} +unsafe impl Sync for FilePersonalAccessTokenStorage {} + +#[async_trait] +impl PersonalAccessTokenStorage for FilePersonalAccessTokenStorage { + async fn load_all(&self) -> Result, Error> { + let mut personal_access_tokens = Vec::new(); + for data in self.db.scan_prefix(format!("{}:token:", KEY_PREFIX)) { + let personal_access_token = match data + .with_context(|| format!("Failed to load personal access token, when searching by key: {}", KEY_PREFIX)){ + Ok((_, value)) => match rmp_serde::from_slice::(&value) + .with_context(|| format!("Failed to deserialize personal access token, when searching by key: {}", KEY_PREFIX)){ + Ok(personal_access_token) => personal_access_token, + Err(err) => { + return Err(Error::CannotDeserializeResource(err)); + } + }, + Err(err) => { + return Err(Error::CannotLoadResource(err)); + } + }; + personal_access_tokens.push(personal_access_token); + } + + Ok(personal_access_tokens) + } + + async fn load_for_user(&self, user_id: UserId) -> Result, Error> { + let mut personal_access_tokens = Vec::new(); + let key = format!("{}:user:{}:", KEY_PREFIX, user_id); + for data in self.db.scan_prefix(&key) { + match data.with_context(|| { + format!( + "Failed to load personal access token, for user ID: {}", + user_id + ) + }) { + Ok((_, value)) => { + let token = from_utf8(&value)?; + let personal_access_token = self.load_by_token(token).await?; + personal_access_tokens.push(personal_access_token); + } + Err(err) => { + return Err(Error::CannotLoadResource(err)); + } + }; + } + + Ok(personal_access_tokens) + } + + async fn load_by_token(&self, token: &str) -> Result { + let key = get_key(token); + return match self + .db + .get(&key) + .with_context(|| format!("Failed to load personal access token, token: {}", token)) + { + Ok(personal_access_token) => { + if let Some(personal_access_token) = personal_access_token { + let personal_access_token = + rmp_serde::from_slice::(&personal_access_token) + .with_context(|| "Failed to deserialize personal access token"); + if let Err(err) = personal_access_token { + Err(Error::CannotDeserializeResource(err)) + } else { + Ok(personal_access_token.unwrap()) + } + } else { + Err(Error::ResourceNotFound(key)) + } + } + Err(err) => Err(Error::CannotLoadResource(err)), + }; + } + + async fn load_by_name( + &self, + user_id: UserId, + name: &str, + ) -> Result { + let key = get_name_key(user_id, name); + return match self.db.get(&key).with_context(|| { + format!( + "Failed to load personal access token, token_name: {}, user_id: {}", + name, user_id + ) + }) { + Ok(token) => { + if let Some(token) = token { + let token = from_utf8(&token) + .with_context(|| "Failed to deserialize personal access token"); + if let Err(err) = token { + Err(Error::CannotDeserializeResource(err)) + } else { + Ok(self.load_by_token(token.unwrap()).await?) + } + } else { + Err(Error::ResourceNotFound(key)) + } + } + Err(err) => Err(Error::CannotLoadResource(err)), + }; + } + + async fn delete_for_user(&self, user_id: UserId, name: &str) -> Result<(), Error> { + let personal_access_token = self.load_by_name(user_id, name).await?; + info!("Deleting personal access token with name: {name} for user with ID: {user_id}..."); + let key = get_name_key(user_id, name); + if let Err(err) = self + .db + .remove(key) + .with_context(|| "Failed to delete personal access token") + { + return Err(Error::CannotDeleteResource(err)); + } + let key = get_key(&personal_access_token.token); + if let Err(err) = self + .db + .remove(key) + .with_context(|| "Failed to delete personal access token") + { + return Err(Error::CannotDeleteResource(err)); + } + info!("Deleted personal access token with name: {name} for user with ID: {user_id}."); + Ok(()) + } +} + +#[async_trait] +impl Storage for FilePersonalAccessTokenStorage { + async fn load(&self, personal_access_token: &mut PersonalAccessToken) -> Result<(), Error> { + self.load_by_name(personal_access_token.user_id, &personal_access_token.name) + .await?; + Ok(()) + } + + async fn save(&self, personal_access_token: &PersonalAccessToken) -> Result<(), Error> { + let key = get_key(&personal_access_token.token); + match rmp_serde::to_vec(&personal_access_token) + .with_context(|| "Failed to serialize personal access token") + { + Ok(data) => { + if let Err(err) = self + .db + .insert(key, data) + .with_context(|| "Failed to save personal access token") + { + return Err(Error::CannotSaveResource(err)); + } + if let Err(err) = self + .db + .insert( + get_name_key(personal_access_token.user_id, &personal_access_token.name), + personal_access_token.token.as_bytes(), + ) + .with_context(|| "Failed to save personal access token") + { + return Err(Error::CannotSaveResource(err)); + } + } + Err(err) => { + return Err(Error::CannotSerializeResource(err)); + } + } + + info!( + "Saved personal access token for user with ID: {}.", + personal_access_token.user_id + ); + Ok(()) + } + + async fn delete(&self, personal_access_token: &PersonalAccessToken) -> Result<(), Error> { + self.delete_for_user(personal_access_token.user_id, &personal_access_token.name) + .await + } +} + +fn get_key(token_hash: &str) -> String { + format!("{}:token:{}", KEY_PREFIX, token_hash) +} + +fn get_name_key(user_id: UserId, name: &str) -> String { + format!("{}:user:{}:{}", KEY_PREFIX, user_id, name) +} diff --git a/src/infrastructure/session.rs b/src/infrastructure/session.rs new file mode 100644 index 0000000..abf20b6 --- /dev/null +++ b/src/infrastructure/session.rs @@ -0,0 +1,65 @@ +use crate::models::user_info::{AtomicUserId, UserId}; +use std::fmt::Display; +use std::net::SocketAddr; +use std::sync::atomic::Ordering; + +// This might be extended with more fields in the future e.g. custom name, permissions etc. +#[derive(Debug)] +pub struct Session { + user_id: AtomicUserId, + pub client_id: u32, + pub ip_address: SocketAddr, +} + +impl Session { + pub fn new(client_id: u32, user_id: UserId, ip_address: SocketAddr) -> Self { + Self { + client_id, + user_id: AtomicUserId::new(user_id), + ip_address, + } + } + + pub fn stateless(user_id: UserId, ip_address: SocketAddr) -> Self { + Self::new(0, user_id, ip_address) + } + + pub fn from_client_id(client_id: u32, ip_address: SocketAddr) -> Self { + Self::new(client_id, 0, ip_address) + } + + pub fn get_user_id(&self) -> UserId { + self.user_id.load(Ordering::Acquire) + } + + pub fn set_user_id(&self, user_id: UserId) { + self.user_id.store(user_id, Ordering::Release) + } + + pub fn clear_user_id(&self) { + self.set_user_id(0) + } + + pub fn is_authenticated(&self) -> bool { + self.get_user_id() > 0 + } +} + +impl Display for Session { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let user_id = self.get_user_id(); + if user_id > 0 { + return write!( + f, + "client ID: {}, user ID: {}, IP address: {}", + self.client_id, user_id, self.ip_address + ); + } + + write!( + f, + "client ID: {}, IP address: {}", + self.client_id, self.ip_address + ) + } +} diff --git a/src/infrastructure/storage.rs b/src/infrastructure/storage.rs new file mode 100644 index 0000000..1469985 --- /dev/null +++ b/src/infrastructure/storage.rs @@ -0,0 +1,189 @@ +use crate::infrastructure::error::Error; +use crate::infrastructure::persistence::persister::Persister; +use crate::infrastructure::personal_access_tokens::personal_access_token::PersonalAccessToken; +use crate::infrastructure::personal_access_tokens::storage::FilePersonalAccessTokenStorage; +use crate::infrastructure::systems::info::SystemInfo; +use crate::infrastructure::systems::storage::FileSystemInfoStorage; +use crate::infrastructure::users::storage::FileUserStorage; +use crate::infrastructure::users::user::User; +use crate::models::user_info::UserId; +use async_trait::async_trait; +use sled::Db; +use std::fmt::{Debug, Formatter}; +use std::sync::Arc; + +#[async_trait] +pub trait Storage: Sync + Send { + async fn load(&self, component: &mut T) -> Result<(), Error>; + async fn save(&self, component: &T) -> Result<(), Error>; + async fn delete(&self, component: &T) -> Result<(), Error>; +} + +#[async_trait] +pub trait SystemInfoStorage: Storage {} + +#[async_trait] +pub trait UserStorage: Storage { + async fn load_by_id(&self, id: UserId) -> Result; + async fn load_by_username(&self, username: &str) -> Result; + async fn load_all(&self) -> Result, Error>; +} + +#[async_trait] +pub trait PersonalAccessTokenStorage: Storage { + async fn load_all(&self) -> Result, Error>; + async fn load_for_user(&self, user_id: UserId) -> Result, Error>; + async fn load_by_token(&self, token: &str) -> Result; + async fn load_by_name(&self, user_id: UserId, name: &str) + -> Result; + async fn delete_for_user(&self, user_id: UserId, name: &str) -> Result<(), Error>; +} + +#[derive(Debug)] +pub struct SystemStorage { + pub info: Arc, + pub user: Arc, + pub personal_access_token: Arc, +} + +impl SystemStorage { + pub fn new(db: Arc) -> Self { + Self { + info: Arc::new(FileSystemInfoStorage::new(db.clone())), + user: Arc::new(FileUserStorage::new(db.clone())), + personal_access_token: Arc::new(FilePersonalAccessTokenStorage::new(db.clone())), + } + } +} + +impl Debug for dyn SystemInfoStorage { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "SystemInfoStorage") + } +} + +impl Debug for dyn UserStorage { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "UserStorage") + } +} + +impl Debug for dyn PersonalAccessTokenStorage { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "PersonalAccessTokenStorage") + } +} + +#[cfg(test)] +pub(crate) mod tests { + use crate::infrastructure::{error::Error, systems::info::SystemInfo, users::user::User}; + use async_trait::async_trait; + use std::sync::Arc; + + use super::*; + + struct TestSystemInfoStorage {} + struct TestUserStorage {} + struct TestPersonalAccessTokenStorage {} + + #[async_trait] + impl Storage for TestSystemInfoStorage { + async fn load(&self, _system_info: &mut SystemInfo) -> Result<(), Error> { + Ok(()) + } + + async fn save(&self, _system_info: &SystemInfo) -> Result<(), Error> { + Ok(()) + } + + async fn delete(&self, _system_info: &SystemInfo) -> Result<(), Error> { + Ok(()) + } + } + + #[async_trait] + impl SystemInfoStorage for TestSystemInfoStorage {} + + #[async_trait] + impl Storage for TestUserStorage { + async fn load(&self, _user: &mut User) -> Result<(), Error> { + Ok(()) + } + + async fn save(&self, _user: &User) -> Result<(), Error> { + Ok(()) + } + + async fn delete(&self, _user: &User) -> Result<(), Error> { + Ok(()) + } + } + + #[async_trait] + impl UserStorage for TestUserStorage { + async fn load_by_id(&self, _id: UserId) -> Result { + Ok(User::default()) + } + + async fn load_by_username(&self, _username: &str) -> Result { + Ok(User::default()) + } + + async fn load_all(&self) -> Result, Error> { + Ok(vec![]) + } + } + + #[async_trait] + impl Storage for TestPersonalAccessTokenStorage { + async fn load( + &self, + _personal_access_token: &mut PersonalAccessToken, + ) -> Result<(), Error> { + Ok(()) + } + + async fn save(&self, _personal_access_token: &PersonalAccessToken) -> Result<(), Error> { + Ok(()) + } + + async fn delete(&self, _personal_access_token: &PersonalAccessToken) -> Result<(), Error> { + Ok(()) + } + } + + #[async_trait] + impl PersonalAccessTokenStorage for TestPersonalAccessTokenStorage { + async fn load_all(&self) -> Result, Error> { + Ok(vec![]) + } + + async fn load_for_user(&self, _user_id: UserId) -> Result, Error> { + Ok(vec![]) + } + + async fn load_by_token(&self, _token: &str) -> Result { + Ok(PersonalAccessToken::default()) + } + + async fn load_by_name( + &self, + _user_id: UserId, + _name: &str, + ) -> Result { + Ok(PersonalAccessToken::default()) + } + + async fn delete_for_user(&self, _user_id: UserId, _name: &str) -> Result<(), Error> { + Ok(()) + } + } + + pub fn get_test_system_storage() -> SystemStorage { + SystemStorage { + info: Arc::new(TestSystemInfoStorage {}), + user: Arc::new(TestUserStorage {}), + personal_access_token: Arc::new(TestPersonalAccessTokenStorage {}), + } + } +} diff --git a/src/infrastructure/systems/clients.rs b/src/infrastructure/systems/clients.rs new file mode 100644 index 0000000..a1a0f13 --- /dev/null +++ b/src/infrastructure/systems/clients.rs @@ -0,0 +1,98 @@ +use crate::infrastructure::clients::client_manager::{Client, Transport}; +use crate::infrastructure::error::Error; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::System; +// use crate::models::identifier::Identifier; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::sync::RwLock; +use tracing::info; + +impl System { + pub async fn add_client(&self, address: &SocketAddr, transport: Transport) -> u32 { + let mut client_manager = self.client_manager.write().await; + let client_id = client_manager.add_client(address, transport); + info!("Added {transport} client with ID: {client_id} for IP address: {address}"); + self.metrics.increment_clients(1); + client_id + } + + pub async fn delete_client(&self, address: &SocketAddr) { + // let consumer_groups: Vec<(u32, u32, u32)>; + let client_id; + + { + let client_manager = self.client_manager.read().await; + let client = client_manager.get_client_by_address(address); + if client.is_err() { + return; + } + + let client = client.unwrap(); + let client = client.read().await; + client_id = client.client_id; + + tracing::info!("{}", client_id); + + // consumer_groups = client + // .consumer_groups + // .iter() + // .map(|c| (c.stream_id, c.topic_id, c.consumer_group_id)) + // .collect(); + } + + // for (stream_id, topic_id, consumer_group_id) in consumer_groups.iter() { + // if let Err(error) = self + // .leave_consumer_group_by_client( + // &Identifier::numeric(*stream_id).unwrap(), + // &Identifier::numeric(*topic_id).unwrap(), + // &Identifier::numeric(*consumer_group_id).unwrap(), + // client_id, + // ) + // .await + // { + // error!( + // "Failed to leave consumer group with ID: {} by client with ID: {}. Error: {}", + // consumer_group_id, client_id, error + // ); + // } + // } + + { + let mut client_manager = self.client_manager.write().await; + let client = client_manager.delete_client(address); + if client.is_none() { + return; + } + + self.metrics.decrement_clients(1); + let client = client.unwrap(); + let client = client.read().await; + + info!( + "Deleted {} client with ID: {} for IP address: {}", + client.transport, client.client_id, client.address + ); + } + } + + pub async fn get_client( + &self, + session: &Session, + client_id: u32, + ) -> Result>, Error> { + self.ensure_authenticated(session)?; + self.permissioner.get_clients(session.get_user_id())?; + + let client_manager = self.client_manager.read().await; + client_manager.get_client_by_id(client_id) + } + + pub async fn get_clients(&self, session: &Session) -> Result>>, Error> { + self.ensure_authenticated(session)?; + self.permissioner.get_clients(session.get_user_id())?; + + let client_manager = self.client_manager.read().await; + Ok(client_manager.get_clients()) + } +} diff --git a/src/infrastructure/systems/info.rs b/src/infrastructure/systems/info.rs new file mode 100644 index 0000000..e4da6ba --- /dev/null +++ b/src/infrastructure/systems/info.rs @@ -0,0 +1,179 @@ +use crate::infrastructure::systems::system::System; +use crate::infrastructure::error::Error; +use serde::{Deserialize, Serialize}; +use std::collections::hash_map::DefaultHasher; +use std::fmt::Display; +use std::hash::{Hash, Hasher}; +use std::str::FromStr; +use tracing::info; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct SystemInfo { + pub version: Version, + pub migrations: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Version { + pub version: String, + pub hash: String, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Migration { + pub id: u32, + pub name: String, + pub hash: String, + pub applied_at: u64, +} + +#[derive(Debug)] +pub struct SemanticVersion { + pub major: u32, + pub minor: u32, + pub patch: u32, +} + +impl System { + pub(crate) async fn load_version(&mut self) -> Result<(), Error> { + info!("Loading system info..."); + let mut system_info = SystemInfo::default(); + self.update_system_info(&mut system_info).await?; + // if let Err(err) = self.storage.info.load(&mut system_info).await { + // match err { + // Error::ResourceNotFound(_) => { + // info!("System info not found, creating..."); + // self.update_system_info(&mut system_info).await?; + // } + // _ => return Err(err), + // } + // } + + info!("Loaded {system_info}"); + let current_version = SemanticVersion::from_str(VERSION)?; + let loaded_version = SemanticVersion::from_str(&system_info.version.version)?; + if current_version.is_equal_to(&loaded_version) { + info!("System version {current_version} is up to date."); + } else if current_version.is_greater_than(&loaded_version) { + info!("System version {current_version} is greater than {loaded_version}, checking the available migrations..."); + self.update_system_info(&mut system_info).await?; + } else { + info!("System version {current_version} is lower than {loaded_version}, possible downgrade."); + self.update_system_info(&mut system_info).await?; + } + + Ok(()) + } + + async fn update_system_info(&self, system_info: &mut SystemInfo) -> Result<(), Error> { + system_info.update_version(VERSION); + // self.storage.info.save(system_info).await?; + Ok(()) + } +} + +impl SystemInfo { + pub fn update_version(&mut self, version: &str) { + self.version.version = version.to_string(); + let mut hasher = DefaultHasher::new(); + self.version.hash.hash(&mut hasher); + self.version.hash = hasher.finish().to_string(); + } +} + +impl Hash for SystemInfo { + fn hash(&self, state: &mut H) { + self.version.version.hash(state); + for migration in &self.migrations { + migration.hash(state); + } + } +} + +impl Hash for Migration { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +impl Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "version: {}", self.version) + } +} + +impl Display for SystemInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "system info, {}", self.version) + } +} + +impl FromStr for SemanticVersion { + type Err = Error; + fn from_str(s: &str) -> Result { + let mut version = s.split('.'); + let major = version.next().unwrap().parse::()?; + let minor = version.next().unwrap().parse::()?; + let patch = version.next().unwrap().parse::()?; + Ok(SemanticVersion { + major, + minor, + patch, + }) + } +} + +impl SemanticVersion { + pub fn is_equal_to(&self, other: &SemanticVersion) -> bool { + self.major == other.major && self.minor == other.minor && self.patch == other.patch + } + + pub fn is_greater_than(&self, other: &SemanticVersion) -> bool { + if self.major > other.major { + return true; + } + if self.major < other.major { + return false; + } + + if self.minor > other.minor { + return true; + } + if self.minor < other.minor { + return false; + } + + if self.patch > other.patch { + return true; + } + if self.patch < other.patch { + return false; + } + + false + } +} + +impl Display for SemanticVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{major}.{minor}.{patch}", + major = self.major, + minor = self.minor, + patch = self.patch + ) + } +} + +mod tests { + #[test] + fn should_load_the_expected_version_from_package_definition() { + use super::VERSION; + + const CARGO_TOML_VERSION: &str = env!("CARGO_PKG_VERSION"); + assert_eq!(VERSION, CARGO_TOML_VERSION); + } +} diff --git a/src/infrastructure/systems/mod.rs b/src/infrastructure/systems/mod.rs new file mode 100644 index 0000000..dfd9d0b --- /dev/null +++ b/src/infrastructure/systems/mod.rs @@ -0,0 +1,7 @@ +pub mod clients; +pub mod info; +pub mod personal_access_token; +pub mod stats; +pub mod storage; +pub mod system; +pub mod users; diff --git a/src/infrastructure/systems/personal_access_token.rs b/src/infrastructure/systems/personal_access_token.rs new file mode 100644 index 0000000..5b29477 --- /dev/null +++ b/src/infrastructure/systems/personal_access_token.rs @@ -0,0 +1,127 @@ +use crate::infrastructure::error::Error; +use crate::infrastructure::personal_access_tokens::personal_access_token::PersonalAccessToken; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::System; +use crate::infrastructure::users::user::User; +use crate::utils::text; +use crate::utils::timestamp::NigigTimeStamp; +use tracing::{error, info}; + +impl System { + pub async fn get_personal_access_tokens( + &self, + session: &Session, + ) -> Result, Error> { + self.ensure_authenticated(session)?; + let user_id = session.get_user_id(); + info!("Loading personal access tokens for user with ID: {user_id}...",); + let personal_access_tokens = self + .storage + .personal_access_token + .load_for_user(user_id) + .await?; + info!( + "Loaded {count} personal access tokens for user with ID: {user_id}.", + count = personal_access_tokens.len(), + ); + Ok(personal_access_tokens) + } + + pub async fn create_personal_access_token( + &self, + session: &Session, + name: &str, + expiry: Option, + ) -> Result { + self.ensure_authenticated(session)?; + let user_id = session.get_user_id(); + let max_token_per_user = self.personal_access_token.max_tokens_per_user; + let name = text::to_lowercase_non_whitespace(name); + let personal_access_tokens = self + .storage + .personal_access_token + .load_for_user(user_id) + .await?; + if personal_access_tokens.len() as u32 >= max_token_per_user { + error!( + "User with ID: {} has reached the maximum number of personal access tokens: {}.", + user_id, max_token_per_user, + ); + return Err(Error::PersonalAccessTokensLimitReached( + user_id, + max_token_per_user, + )); + } + + if personal_access_tokens + .iter() + .any(|personal_access_token| personal_access_token.name == name) + { + error!("Personal access token: {name} for user with ID: {user_id} already exists."); + return Err(Error::PersonalAccessTokenAlreadyExists(name, user_id)); + } + + info!("Creating personal access token: {name} for user with ID: {user_id}..."); + let (personal_access_token, token) = PersonalAccessToken::new( + user_id, + &name, + NigigTimeStamp::now().to_micros(), + // Timestamp::now().to_micros(), + expiry, + ); + self.storage + .personal_access_token + .save(&personal_access_token) + .await?; + info!("Created personal access token: {name} for user with ID: {user_id}."); + Ok(token) + } + + pub async fn delete_personal_access_token( + &self, + session: &Session, + name: &str, + ) -> Result<(), Error> { + self.ensure_authenticated(session)?; + let user_id = session.get_user_id(); + let name = text::to_lowercase_non_whitespace(name); + info!("Deleting personal access token: {name} for user with ID: {user_id}..."); + self.storage + .personal_access_token + .delete_for_user(user_id, &name) + .await?; + info!("Deleted personal access token: {name} for user with ID: {user_id}."); + Ok(()) + } + + pub async fn login_with_personal_access_token( + &self, + token: &str, + session: Option<&Session>, + ) -> Result { + let token_hash = PersonalAccessToken::hash_token(token); + let personal_access_token = self + .storage + .personal_access_token + .load_by_token(&token_hash) + .await?; + if personal_access_token.is_expired(NigigTimeStamp::now().to_micros()) { + error!( + "Personal access token: {} for user with ID: {} has expired.", + personal_access_token.name, personal_access_token.user_id + ); + return Err(Error::PersonalAccessTokenExpired( + personal_access_token.name, + personal_access_token.user_id, + )); + } + + let user = self + .storage + .user + .load_by_id(personal_access_token.user_id) + .await?; + self.login_user_with_credentials(&user.username, None, session) + .await + } +} diff --git a/src/infrastructure/systems/stats.rs b/src/infrastructure/systems/stats.rs new file mode 100644 index 0000000..55a72fb --- /dev/null +++ b/src/infrastructure/systems/stats.rs @@ -0,0 +1,70 @@ +use crate::infrastructure::error::Error; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::System; +use crate::models::stats::Stats; +// use sysinfo::{PidExt, ProcessExt, SystemExt}; + +const PROCESS_NAME: &str = "dev"; + +impl System { + pub async fn get_stats(&self, session: &Session) -> Result { + self.ensure_authenticated(session)?; + self.permissioner.get_stats(session.get_user_id())?; + + let mut sys = sysinfo::System::new_all(); + sys.refresh_all(); + + let mut stats = Stats { + process_id: 0, + cpu_usage: 0.0, + memory_usage: 0.into(), + total_memory: 0.into(), + available_memory: 0.into(), + run_time: 0, + start_time: 0, + clients_count: self.client_manager.read().await.get_clients().len() as u32, + + read_bytes: 0.into(), + written_bytes: 0.into(), + hostname: sysinfo::System::host_name().unwrap_or("unknown_hostname".to_string()), + os_name: sysinfo::System::name().unwrap_or("unknown_os_name".to_string()), + os_version: sysinfo::System::long_os_version() + .unwrap_or("unknown_os_version".to_string()), + kernel_version: sysinfo::System::kernel_version() + .unwrap_or("unknown_kernel_version".to_string()), + }; + + for (pid, process) in sys.processes() { + if process.name() != PROCESS_NAME { + continue; + } + + stats.process_id = pid.as_u32(); + stats.cpu_usage = process.cpu_usage(); + stats.memory_usage = process.memory().into(); + stats.total_memory = sys.total_memory().into(); + stats.available_memory = sys.available_memory().into(); + stats.run_time = process.run_time(); + stats.start_time = process.start_time(); + let disk_usage = process.disk_usage(); + stats.read_bytes = disk_usage.total_read_bytes.into(); + stats.written_bytes = disk_usage.total_written_bytes.into(); + break; + } + + // for stream in self.streams.values() { + // for topic in stream.topics.values() { + // for partition in topic.partitions.values() { + // let partition = partition.read().await; + // stats.messages_count += partition.get_messages_count(); + // stats.segments_count += partition.segments.len() as u32; + // for segment in &partition.segments { + // stats.messages_size_bytes += segment.current_size_bytes as u64; + // } + // } + // } + // } + + Ok(stats) + } +} diff --git a/src/infrastructure/systems/storage.rs b/src/infrastructure/systems/storage.rs new file mode 100644 index 0000000..fb11277 --- /dev/null +++ b/src/infrastructure/systems/storage.rs @@ -0,0 +1,91 @@ +use crate::infrastructure::storage::{Storage, SystemInfoStorage}; +use crate::infrastructure::systems::info::SystemInfo; +use anyhow::Context; +use async_trait::async_trait; +use crate::infrastructure::error::Error; +use sled::Db; +use std::sync::Arc; +use tracing::info; + +const KEY: &str = "system"; + +#[derive(Debug)] +pub struct FileSystemInfoStorage { + db: Arc, +} + +impl FileSystemInfoStorage { + pub fn new(db: Arc) -> Self { + Self { db } + } +} + +unsafe impl Send for FileSystemInfoStorage {} +unsafe impl Sync for FileSystemInfoStorage {} + +impl SystemInfoStorage for FileSystemInfoStorage {} + +#[async_trait] +impl Storage for FileSystemInfoStorage { + async fn load(&self, system_info: &mut SystemInfo) -> Result<(), Error> { + let data = match self + .db + .get(KEY) + .with_context(|| "Failed to load system info") + { + Ok(data) => { + if let Some(data) = data { + let data = rmp_serde::from_slice::(&data) + .with_context(|| "Failed to deserialize system info"); + if let Err(err) = data { + return Err(Error::CannotDeserializeResource(err)); + } else { + data.unwrap() + } + } else { + return Err(Error::ResourceNotFound(KEY.to_string())); + } + } + Err(err) => { + return Err(Error::CannotLoadResource(err)); + } + }; + + system_info.version = data.version; + system_info.migrations = data.migrations; + Ok(()) + } + + async fn save(&self, system_info: &SystemInfo) -> Result<(), Error> { + match rmp_serde::to_vec(&system_info).with_context(|| "Failed to serialize system info") { + Ok(data) => { + if let Err(err) = self + .db + .insert(KEY, data) + .with_context(|| "Failed to save system info") + { + return Err(Error::CannotSaveResource(err)); + } + } + Err(err) => { + return Err(Error::CannotSerializeResource(err)); + } + } + + info!("Saved system info, {}", system_info); + Ok(()) + } + + async fn delete(&self, _: &SystemInfo) -> Result<(), Error> { + if let Err(err) = self + .db + .remove(KEY) + .with_context(|| "Failed to delete system info") + { + return Err(Error::CannotDeleteResource(err)); + } + + info!("Deleted system info"); + Ok(()) + } +} diff --git a/src/infrastructure/systems/system.rs b/src/infrastructure/systems/system.rs new file mode 100644 index 0000000..3d359e0 --- /dev/null +++ b/src/infrastructure/systems/system.rs @@ -0,0 +1,198 @@ +use crate::configs::server::PersonalAccessTokenConfig; +use crate::configs::system::SystemConfig; +use crate::infrastructure::cache::memory_tracker::CacheMemoryTracker; +use crate::infrastructure::clients::client_manager::ClientManager; +use crate::infrastructure::diagnostics::metrics::Metrics; +use crate::infrastructure::error::Error; +use crate::infrastructure::persistence::persister::*; +use crate::infrastructure::session::Session; +use crate::infrastructure::storage::SystemStorage; +use crate::infrastructure::users::permissioner::Permissioner; +use crate::utils::crypto::{Aes256GcmEncryptor, Encryptor}; +use sled::Db; +use std::collections::HashMap; +use std::path::Path; +use std::sync::Arc; +use tokio::fs::{create_dir, remove_dir_all}; +use tokio::sync::RwLock; +use tokio::time::Instant; +use tracing::{info, trace}; + +use keepcalm::{SharedMut, SharedReadLock, SharedWriteLock}; + +#[derive(Debug)] +pub struct SharedSystem { + system: SharedMut, +} + +impl SharedSystem { + pub fn new(system: System) -> SharedSystem { + SharedSystem { + system: SharedMut::new(system), + } + } + + pub fn read(&self) -> SharedReadLock { + self.system.read() + } + + pub fn write(&self) -> SharedWriteLock { + self.system.write() + } +} + +impl Clone for SharedSystem { + fn clone(&self) -> Self { + SharedSystem { + system: self.system.clone(), + } + } +} + +#[derive(Debug)] +pub struct System { + pub permissioner: Permissioner, + pub(crate) storage: Arc, + pub(crate) config: Arc, + pub(crate) client_manager: Arc>, + pub(crate) metrics: Metrics, + pub(crate) db: Option>, + pub personal_access_token: PersonalAccessTokenConfig, +} + +/// For each cache eviction, we want to remove more than the size we need. +/// This is done on purpose to avoid evicting messages on every write. +const CACHE_OVER_EVICTION_FACTOR: u64 = 5; + +impl System { + pub fn new( + config: Arc, + db: Option>, + pat_config: PersonalAccessTokenConfig, + ) -> System { + let db = match db { + Some(db) => db, + None => { + let db = sled::open(config.get_database_path()); + if db.is_err() { + panic!("Cannot open database at: {}", config.get_database_path()); + } + Arc::new(db.unwrap()) + } + }; + // let persister: Arc = match config.partition.enforce_fsync { + // true => Arc::new(FileWithSyncPersister {}), + // false => Arc::new(FilePersister {}), + // }; + Self::create(config, SystemStorage::new(db.clone()), Some(db), pat_config) + } + + pub fn create( + config: Arc, + storage: SystemStorage, + db: Option>, + pat_config: PersonalAccessTokenConfig, + ) -> System { + // info!( + // "Server-side encryption is {}.", + // Self::map_toggle_str(config.encryption.enabled) + // ); + System { + // encryptor: match config.encryption.enabled { + // true => Some(Box::new( + // Aes256GcmEncryptor::from_base64_key(&config.encryption.key).unwrap(), + // )), + // false => None, + // }, + config, + // streams: HashMap::new(), + // streams_ids: HashMap::new(), + storage: Arc::new(storage), + client_manager: Arc::new(RwLock::new(ClientManager::default())), + permissioner: Permissioner::default(), + metrics: Metrics::init(), + db, + personal_access_token: pat_config, + } + } + + pub async fn init(&mut self) -> Result<(), Error> { + let system_path = self.config.get_system_path(); + + if !Path::new(&system_path).exists() && create_dir(&system_path).await.is_err() { + return Err(Error::CannotCreateBaseDirectory(system_path)); + } + + // let streams_path = self.config.get_streams_path(); + // if !Path::new(&streams_path).exists() && create_dir(&streams_path).await.is_err() { + // return Err(Error::CannotCreateStreamsDirectory(streams_path)); + // } + + let runtime_path = self.config.get_runtime_path(); + if Path::new(&runtime_path).exists() && remove_dir_all(&runtime_path).await.is_err() { + return Err(Error::CannotRemoveRuntimeDirectory(runtime_path)); + } + + if create_dir(&runtime_path).await.is_err() { + return Err(Error::CannotCreateRuntimeDirectory(runtime_path)); + } + + info!( + "Initializing system, data will be stored at: {}", + self.config.get_system_path() + ); + let now = Instant::now(); + self.load_version().await?; + self.load_users().await?; + // self.load_streams().await?; + info!("Initialized system in {} ms.", now.elapsed().as_millis()); + Ok(()) + } + + // pub async fn shutdown(&mut self, storage: Arc) -> Result<(), Error> { + // self.persist_messages(storage.clone()).await?; + // Ok(()) + // } + + // pub async fn persist_messages(&self, storage: Arc) -> Result<(), Error> { + // trace!("Saving buffered messages on disk..."); + // for stream in self.streams.values() { + // stream.persist_messages(storage.clone()).await?; + // } + + // Ok(()) + // } + + pub fn ensure_authenticated(&self, session: &Session) -> Result<(), Error> { + match session.is_authenticated() { + true => Ok(()), + false => Err(Error::Unauthenticated), + } + } + + fn map_toggle_str<'a>(enabled: bool) -> &'a str { + match enabled { + true => "enabled", + false => "disabled", + } + } + + pub async fn clean_cache(&self, _size_to_clean: u64) { + // for stream in self.streams.values() { + // for topic in stream.get_topics() { + // for partition in topic.get_partitions().into_iter() { + // tokio::task::spawn(async move { + // let memory_tracker = CacheMemoryTracker::get_instance().unwrap(); + // let mut partition_guard = partition.write().await; + // let cache = &mut partition_guard.cache.as_mut().unwrap(); + // let size_to_remove = (cache.current_size() as f64 + // / memory_tracker.usage_bytes() as f64 + // * size_to_clean as f64) + // .ceil() as u64; + // cache.evict_by_size(size_to_remove * CACHE_OVER_EVICTION_FACTOR); + // }); + // } + // } + // } + } +} diff --git a/src/infrastructure/systems/users.rs b/src/infrastructure/systems/users.rs new file mode 100644 index 0000000..19c75d5 --- /dev/null +++ b/src/infrastructure/systems/users.rs @@ -0,0 +1,297 @@ +use crate::infrastructure::error::Error; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::System; +use crate::infrastructure::users::user::User; +use crate::infrastructure::utils::crypto; +use crate::models::identifier::{IdKind, Identifier}; +use crate::models::permissions::Permissions; +use crate::models::user_status::UserStatus; +use crate::utils::text; +use std::sync::atomic::{AtomicU32, Ordering}; +use tracing::log::error; +use tracing::{info, warn}; + +static USER_ID: AtomicU32 = AtomicU32::new(1); + +impl System { + pub(crate) async fn load_users(&mut self) -> Result<(), Error> { + info!("Loading users..."); + let mut users = self.storage.user.load_all().await?; + if users.is_empty() { + info!("No users found, creating the root user..."); + let root = User::root(); + self.storage.user.save(&root).await?; + info!("Created the root user."); + users = self.storage.user.load_all().await?; + } + + let users_count = users.len(); + let current_user_id = users.iter().map(|user| user.id).max().unwrap_or(1); + USER_ID.store(current_user_id + 1, Ordering::SeqCst); + self.permissioner.init(users); + info!("Initialized {} user(s).", users_count); + Ok(()) + } + + pub async fn find_user(&self, session: &Session, user_id: &Identifier) -> Result { + self.ensure_authenticated(session)?; + let user = self.get_user(user_id).await?; + let session_user_id = session.get_user_id(); + if user.id != session_user_id { + self.permissioner.get_user(session_user_id)?; + } + + Ok(user) + } + + pub async fn get_user(&self, user_id: &Identifier) -> Result { + Ok(match user_id.kind { + IdKind::Numeric => { + self.storage + .user + .load_by_id(user_id.get_u32_value()?) + .await? + } + IdKind::String => { + self.storage + .user + .load_by_username(&user_id.get_cow_str_value()?) + .await? + } + }) + } + + pub async fn get_users(&self, session: &Session) -> Result, Error> { + self.ensure_authenticated(session)?; + self.permissioner.get_users(session.get_user_id())?; + self.storage.user.load_all().await + } + + pub async fn create_user( + &mut self, + session: &Session, + username: &str, + password: &str, + status: UserStatus, + permissions: Option, + ) -> Result<(), Error> { + self.ensure_authenticated(session)?; + self.permissioner.create_user(session.get_user_id())?; + let username = text::to_lowercase_non_whitespace(username); + if self.storage.user.load_by_username(&username).await.is_ok() { + error!("User: {username} already exists."); + return Err(Error::UserAlreadyExists); + } + let user_id = USER_ID.fetch_add(1, Ordering::SeqCst); + info!("Creating user: {username} with ID: {user_id}..."); + let user = User::new(user_id, &username, password, status, permissions); + self.storage.user.save(&user).await?; + self.permissioner.init_permissions_for_user(user); + info!("Created user: {username} with ID: {user_id}."); + self.metrics.increment_users(1); + Ok(()) + } + + pub async fn delete_user( + &mut self, + session: &Session, + user_id: &Identifier, + ) -> Result { + self.ensure_authenticated(session)?; + self.permissioner.delete_user(session.get_user_id())?; + let user = self.get_user(user_id).await?; + if user.is_root() { + error!("Cannot delete the root user."); + return Err(Error::CannotDeleteUser(user.id)); + } + + info!("Deleting user: {} with ID: {user_id}...", user.username); + self.storage.user.delete(&user).await?; + self.permissioner.delete_permissions_for_user(user.id); + let mut client_manager = self.client_manager.write().await; + client_manager.delete_clients_for_user(user.id).await?; + info!("Deleted user: {} with ID: {user_id}.", user.username); + self.metrics.decrement_users(1); + Ok(user) + } + + pub async fn update_user( + &self, + session: &Session, + user_id: &Identifier, + username: Option, + status: Option, + ) -> Result { + self.ensure_authenticated(session)?; + self.permissioner.update_user(session.get_user_id())?; + let mut user = self.get_user(user_id).await?; + if let Some(username) = username { + let username = text::to_lowercase_non_whitespace(&username); + let existing_user = self.storage.user.load_by_username(&username).await; + if existing_user.is_ok() && existing_user.unwrap().id != user.id { + error!("User: {username} already exists."); + return Err(Error::UserAlreadyExists); + } + self.storage.user.delete(&user).await?; + user.username = username; + } + + if let Some(status) = status { + user.status = status; + } + + info!("Updating user: {} with ID: {}...", user.username, user.id); + self.storage.user.save(&user).await?; + info!("Updated user: {} with ID: {}.", user.username, user.id); + Ok(user) + } + + pub async fn update_permissions( + &mut self, + session: &Session, + user_id: &Identifier, + permissions: Option, + ) -> Result<(), Error> { + self.ensure_authenticated(session)?; + self.permissioner + .update_permissions(session.get_user_id())?; + let mut user = self.get_user(user_id).await?; + if user.is_root() { + error!("Cannot change the root user permissions."); + return Err(Error::CannotChangePermissions(user.id)); + } + + user.permissions = permissions; + let username = user.username.clone(); + info!( + "Updating permissions for user: {} with ID: {user_id}...", + username + ); + self.storage.user.save(&user).await?; + self.permissioner.update_permissions_for_user(user); + info!( + "Updated permissions for user: {} with ID: {user_id}.", + username + ); + Ok(()) + } + + pub async fn change_password( + &self, + session: &Session, + user_id: &Identifier, + current_password: &str, + new_password: &str, + ) -> Result<(), Error> { + self.ensure_authenticated(session)?; + let mut user = self.get_user(user_id).await?; + let session_user_id = session.get_user_id(); + if user.id != session_user_id { + self.permissioner.change_password(session_user_id)?; + } + + if !crypto::verify_password(current_password, &user.password) { + error!( + "Invalid current password for user: {} with ID: {user_id}.", + user.username + ); + return Err(Error::InvalidCredentials); + } + + info!( + "Changing password for user: {} with ID: {user_id}...", + user.username + ); + user.password = crypto::hash_password(new_password); + self.storage.user.save(&user).await?; + info!( + "Changed password for user: {} with ID: {user_id}.", + user.username + ); + Ok(()) + } + + pub async fn login_user( + &self, + username: &str, + password: &str, + session: Option<&Session>, + ) -> Result { + self.login_user_with_credentials(username, Some(password), session) + .await + } + + pub async fn login_user_with_credentials( + &self, + username: &str, + password: Option<&str>, + session: Option<&Session>, + ) -> Result { + let user = match self.storage.user.load_by_username(username).await { + Ok(user) => user, + Err(_) => { + error!("Cannot login user: {username} (not found)."); + return Err(Error::InvalidCredentials); + } + }; + + info!("Logging in user: {username} with ID: {}...", user.id); + if !user.is_active() { + warn!("User: {username} with ID: {} is inactive.", user.id); + return Err(Error::UserInactive); + } + + if let Some(password) = password { + if !crypto::verify_password(password, &user.password) { + warn!( + "Invalid password for user: {username} with ID: {}.", + user.id + ); + return Err(Error::InvalidCredentials); + } + } + + info!("Logged in user: {username} with ID: {}.", user.id); + if session.is_none() { + return Ok(user); + } + + let session = session.unwrap(); + if session.is_authenticated() { + warn!( + "User: {} with ID: {} was already authenticated, removing the previous session...", + user.username, + session.get_user_id() + ); + self.logout_user(session).await?; + } + + session.set_user_id(user.id); + let mut client_manager = self.client_manager.write().await; + client_manager + .set_user_id(session.client_id, user.id) + .await?; + Ok(user) + } + + pub async fn logout_user(&self, session: &Session) -> Result<(), Error> { + self.ensure_authenticated(session)?; + let user = self + .get_user(&Identifier::numeric(session.get_user_id())?) + .await?; + info!( + "Logging out user: {} with ID: {}...", + user.username, user.id + ); + if session.client_id > 0 { + let mut client_manager = self.client_manager.write().await; + client_manager.clear_user_id(session.client_id).await?; + info!( + "Cleared user ID: {} for client: {}.", + user.id, session.client_id + ); + } + info!("Logged out user: {} with ID: {}.", user.username, user.id); + Ok(()) + } +} diff --git a/src/infrastructure/users/mod.rs b/src/infrastructure/users/mod.rs new file mode 100644 index 0000000..bdf85c0 --- /dev/null +++ b/src/infrastructure/users/mod.rs @@ -0,0 +1,4 @@ +pub mod permissioner; +pub mod permissioner_rules; +pub mod storage; +pub mod user; diff --git a/src/infrastructure/users/permissioner.rs b/src/infrastructure/users/permissioner.rs new file mode 100644 index 0000000..5af38e3 --- /dev/null +++ b/src/infrastructure/users/permissioner.rs @@ -0,0 +1,75 @@ +use crate::infrastructure::users::user::User; +use crate::models::permissions::{GlobalPermissions, StreamPermissions}; +use crate::models::user_info::UserId; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Permissioner { + pub(super) users_permissions: HashMap, + pub(super) users_streams_permissions: HashMap<(UserId, u32), StreamPermissions>, + pub(super) users_that_can_poll_messages_from_all_streams: HashSet, + pub(super) users_that_can_send_messages_to_all_streams: HashSet, + pub(super) users_that_can_poll_messages_from_specific_streams: HashSet<(UserId, u32)>, + pub(super) users_that_can_send_messages_to_specific_streams: HashSet<(UserId, u32)>, +} + +impl Permissioner { + pub fn init(&mut self, users: Vec) { + for user in users { + self.init_permissions_for_user(user); + } + } + + pub fn init_permissions_for_user(&mut self, user: User) { + if user.permissions.is_none() { + return; + } + + // let permissions = user.permissions.unwrap(); + // if permissions.global.poll_messages { + // self.users_that_can_poll_messages_from_all_streams + // .insert(user.id); + // } + // if permissions.global.send_messages { + // self.users_that_can_send_messages_to_all_streams + // .insert(user.id); + // } + // self.users_permissions.insert(user.id, permissions.global); + // if permissions.streams.is_none() { + // return; + // } + // let streams = permissions.streams.unwrap(); + // for (stream_id, stream) in streams { + // if stream.poll_messages { + // self.users_that_can_poll_messages_from_specific_streams + // .insert((user.id, stream_id)); + // } + // if stream.send_messages { + // self.users_that_can_send_messages_to_specific_streams + // .insert((user.id, stream_id)); + // } + // self.users_streams_permissions + // .insert((user.id, stream_id), stream); + // } + } + + pub fn update_permissions_for_user(&mut self, user: User) { + self.delete_permissions_for_user(user.id); + self.init_permissions_for_user(user); + } + + pub fn delete_permissions_for_user(&mut self, user_id: UserId) { + self.users_permissions.remove(&user_id); + // self.users_that_can_poll_messages_from_all_streams + // .remove(&user_id); + // self.users_that_can_send_messages_to_all_streams + // .remove(&user_id); + // self.users_streams_permissions + // .retain(|(id, _), _| *id != user_id); + // self.users_that_can_poll_messages_from_specific_streams + // .retain(|(id, _)| *id != user_id); + // self.users_that_can_send_messages_to_specific_streams + // .retain(|(id, _)| *id != user_id); + } +} diff --git a/src/infrastructure/users/permissioner_rules/mod.rs b/src/infrastructure/users/permissioner_rules/mod.rs new file mode 100644 index 0000000..1005cd6 --- /dev/null +++ b/src/infrastructure/users/permissioner_rules/mod.rs @@ -0,0 +1,2 @@ +mod system; +mod users; diff --git a/src/infrastructure/users/permissioner_rules/system.rs b/src/infrastructure/users/permissioner_rules/system.rs new file mode 100644 index 0000000..569f0ca --- /dev/null +++ b/src/infrastructure/users/permissioner_rules/system.rs @@ -0,0 +1,26 @@ +use crate::infrastructure::users::permissioner::Permissioner; +use crate::infrastructure::error::Error; + +impl Permissioner { + pub fn get_stats(&self, user_id: u32) -> Result<(), Error> { + self.get_server_info(user_id) + } + + pub fn get_clients(&self, user_id: u32) -> Result<(), Error> { + self.get_server_info(user_id) + } + + pub fn get_client(&self, user_id: u32) -> Result<(), Error> { + self.get_server_info(user_id) + } + + fn get_server_info(&self, user_id: u32) -> Result<(), Error> { + if let Some(global_permissions) = self.users_permissions.get(&user_id) { + if global_permissions.manage_servers || global_permissions.read_servers { + return Ok(()); + } + } + + Err(Error::Unauthorized) + } +} diff --git a/src/infrastructure/users/permissioner_rules/users.rs b/src/infrastructure/users/permissioner_rules/users.rs new file mode 100644 index 0000000..a5b4739 --- /dev/null +++ b/src/infrastructure/users/permissioner_rules/users.rs @@ -0,0 +1,52 @@ +use crate::infrastructure::error::Error; +use crate::infrastructure::users::permissioner::Permissioner; + +impl Permissioner { + pub fn get_user(&self, user_id: u32) -> Result<(), Error> { + self.read_users(user_id) + } + + pub fn get_users(&self, user_id: u32) -> Result<(), Error> { + self.read_users(user_id) + } + + pub fn create_user(&self, user_id: u32) -> Result<(), Error> { + self.manager_users(user_id) + } + + pub fn delete_user(&self, user_id: u32) -> Result<(), Error> { + self.manager_users(user_id) + } + + pub fn update_user(&self, user_id: u32) -> Result<(), Error> { + self.manager_users(user_id) + } + + pub fn update_permissions(&self, user_id: u32) -> Result<(), Error> { + self.manager_users(user_id) + } + + pub fn change_password(&self, user_id: u32) -> Result<(), Error> { + self.manager_users(user_id) + } + + fn manager_users(&self, user_id: u32) -> Result<(), Error> { + if let Some(global_permissions) = self.users_permissions.get(&user_id) { + if global_permissions.manage_users { + return Ok(()); + } + } + + Err(Error::Unauthorized) + } + + fn read_users(&self, user_id: u32) -> Result<(), Error> { + if let Some(global_permissions) = self.users_permissions.get(&user_id) { + if global_permissions.manage_users || global_permissions.read_users { + return Ok(()); + } + } + + Err(Error::Unauthorized) + } +} diff --git a/src/infrastructure/users/storage.rs b/src/infrastructure/users/storage.rs new file mode 100644 index 0000000..5845db1 --- /dev/null +++ b/src/infrastructure/users/storage.rs @@ -0,0 +1,193 @@ +use crate::infrastructure::error::Error; +use crate::infrastructure::storage::{Storage, UserStorage}; +use crate::infrastructure::users::user::User; +use crate::models::user_info::UserId; +use anyhow::Context; +use async_trait::async_trait; +use sled::Db; +use std::sync::Arc; +use tracing::info; + +const KEY_PREFIX: &str = "users"; + +#[derive(Debug)] +pub struct FileUserStorage { + db: Arc, +} + +impl FileUserStorage { + pub fn new(db: Arc) -> Self { + Self { db } + } +} + +unsafe impl Send for FileUserStorage {} +unsafe impl Sync for FileUserStorage {} + +#[async_trait] +impl UserStorage for FileUserStorage { + async fn load_by_id(&self, id: UserId) -> Result { + let mut user = User::empty(id); + self.load(&mut user).await?; + Ok(user) + } + + async fn load_by_username(&self, username: &str) -> Result { + let user_id_key = get_id_key(username); + let user_id = self.db.get(&user_id_key).with_context(|| { + format!( + "Failed to load user with key: {}, username: {}", + user_id_key, username + ) + }); + match user_id { + Ok(user_id) => { + if let Some(user_id) = user_id { + let user_id = u32::from_le_bytes(user_id.as_ref().try_into()?); + let mut user = User::empty(user_id); + self.load(&mut user).await?; + Ok(user) + } else { + Err(Error::ResourceNotFound(user_id_key)) + } + } + Err(err) => Err(Error::CannotLoadResource(err)), + } + } + + async fn load_all(&self) -> Result, Error> { + let mut users = Vec::new(); + for data in self.db.scan_prefix(format!("{}:", KEY_PREFIX)) { + let user = match data.with_context(|| { + format!( + "Failed to load user, when searching for key: {}", + KEY_PREFIX + ) + }) { + Ok((_, value)) => match rmp_serde::from_slice::(&value).with_context(|| { + format!( + "Failed to deserialize user, when searching for key: {}", + KEY_PREFIX + ) + }) { + Ok(user) => user, + Err(err) => { + return Err(Error::CannotDeserializeResource(err)); + } + }, + Err(err) => { + return Err(Error::CannotLoadResource(err)); + } + }; + users.push(user); + } + + Ok(users) + } +} + +#[async_trait] +impl Storage for FileUserStorage { + async fn load(&self, user: &mut User) -> Result<(), Error> { + let key = get_key(user.id); + let user_data = match self.db.get(&key).with_context(|| { + format!( + "Failed to load user with key: {}, username: {}", + key, user.username + ) + }) { + Ok(data) => { + if let Some(user_data) = data { + user_data + } else { + return Err(Error::ResourceNotFound(key)); + } + } + Err(err) => { + return Err(Error::CannotLoadResource(err)); + } + }; + + let user_data = rmp_serde::from_slice::(&user_data) + .with_context(|| format!("Failed to deserialize user with key: {}", key)); + match user_data { + Ok(user_data) => { + user.status = user_data.status; + user.username = user_data.username; + user.password = user_data.password; + user.created_at = user_data.created_at; + user.permissions = user_data.permissions; + Ok(()) + } + Err(err) => { + return Err(Error::CannotDeserializeResource(err)); + } + } + } + + async fn save(&self, user: &User) -> Result<(), Error> { + let key = get_key(user.id); + match rmp_serde::to_vec(&user) + .with_context(|| format!("Failed to serialize user with key: {}", key)) + { + Ok(data) => { + if let Err(err) = self + .db + .insert(&key, data) + .with_context(|| format!("Failed to insert user with key: {}", key)) + { + return Err(Error::CannotSaveResource(err)); + } + if let Err(err) = self + .db + .insert(get_id_key(&user.username), &user.id.to_le_bytes()) + .with_context(|| { + format!( + "Failed to insert user with ID: {} key: {}", + &user.id, + get_id_key(&user.username) + ) + }) + { + return Err(Error::CannotSaveResource(err)); + } + } + Err(err) => { + return Err(Error::CannotSerializeResource(err)); + } + } + + info!("Saved user with ID: {}.", user.id); + Ok(()) + } + + async fn delete(&self, user: &User) -> Result<(), Error> { + info!("Deleting user with ID: {}...", user.id); + let key = get_key(user.id); + if let Err(err) = self + .db + .remove(&key) + .with_context(|| format!("Failed to delete user with ID: {}, key: {}", user.id, key)) + { + return Err(Error::CannotDeleteResource(err)); + } else { + let key = get_id_key(&user.username); + if let Err(err) = self.db.remove(&key).with_context(|| { + format!("Failed to delete user with ID: {}, key : {}", user.id, key) + }) { + return Err(Error::CannotDeleteResource(err)); + } else { + info!("Deleted user with ID: {}.", user.id); + Ok(()) + } + } + } +} + +fn get_key(user_id: UserId) -> String { + format!("{}:{}", KEY_PREFIX, user_id) +} + +fn get_id_key(username: &str) -> String { + format!("{}_id:{}", KEY_PREFIX, username) +} diff --git a/src/infrastructure/users/user.rs b/src/infrastructure/users/user.rs new file mode 100644 index 0000000..d3ac68b --- /dev/null +++ b/src/infrastructure/users/user.rs @@ -0,0 +1,99 @@ +use crate::infrastructure::utils::crypto; +use crate::models::user_status::UserStatus; +use crate::models::users::defaults::*; +use crate::models::{permissions::Permissions, user_info::UserId}; +use crate::utils::timestamp::NigigTimeStamp; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct User { + pub id: UserId, + pub status: UserStatus, + pub username: String, + pub password: String, + pub created_at: u64, + pub permissions: Option, +} + +impl Default for User { + fn default() -> Self { + Self { + id: 1, + status: UserStatus::Active, + username: "user".to_string(), + password: "secret".to_string(), + created_at: NigigTimeStamp::now().to_micros(), + permissions: None, + } + } +} + +impl User { + pub fn empty(id: UserId) -> Self { + Self { + id, + ..Default::default() + } + } + + pub fn new( + id: u32, + username: &str, + password: &str, + status: UserStatus, + permissions: Option, + ) -> Self { + Self { + id, + username: username.to_string(), + password: crypto::hash_password(password), + created_at: NigigTimeStamp::now().to_micros(), + status, + permissions, + } + } + + pub fn root() -> Self { + Self::new( + DEFAULT_ROOT_USER_ID, + DEFAULT_ROOT_USERNAME, + DEFAULT_ROOT_PASSWORD, + UserStatus::Active, + Some(Permissions::root()), + ) + } + + pub fn is_root(&self) -> bool { + self.id == DEFAULT_ROOT_USER_ID + } + + pub fn is_active(&self) -> bool { + self.status == UserStatus::Active + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn given_root_user_data_and_credentials_should_be_valid() { + let user = User::root(); + assert_eq!(user.id, DEFAULT_ROOT_USER_ID); + assert_eq!(user.username, DEFAULT_ROOT_USERNAME); + assert_ne!(user.password, DEFAULT_ROOT_PASSWORD); + assert!(crypto::verify_password( + DEFAULT_ROOT_PASSWORD, + &user.password + )); + assert_eq!(user.status, UserStatus::Active); + assert!(user.created_at > 0); + } + + #[test] + fn should_be_created_given_specific_status() { + let status = UserStatus::Inactive; + let user = User::new(1, "test", "test", status, None); + assert_eq!(user.status, status); + } +} diff --git a/src/infrastructure/utils/crypto.rs b/src/infrastructure/utils/crypto.rs new file mode 100644 index 0000000..ddc5c2c --- /dev/null +++ b/src/infrastructure/utils/crypto.rs @@ -0,0 +1,9 @@ +use bcrypt::{hash, verify}; + +pub fn hash_password(password: &str) -> String { + hash(password, 4).unwrap() +} + +pub fn verify_password(password: &str, hash: &str) -> bool { + verify(password, hash).unwrap_or(false) +} diff --git a/src/infrastructure/utils/file.rs b/src/infrastructure/utils/file.rs new file mode 100644 index 0000000..e9b1a18 --- /dev/null +++ b/src/infrastructure/utils/file.rs @@ -0,0 +1,41 @@ +use std::{ + collections::VecDeque, + path::{Path, PathBuf}, +}; +use tokio::fs::{read_dir, File, OpenOptions}; + +pub async fn open(path: &str) -> Result { + OpenOptions::new().read(true).open(path).await +} + +pub async fn append(path: &str) -> Result { + OpenOptions::new().read(true).append(true).open(path).await +} + +pub async fn write(path: &str) -> Result { + OpenOptions::new().create(true).write(true).open(path).await +} + +pub async fn folder_size

(path: P) -> std::io::Result +where + P: Into + AsRef, +{ + let mut total_size = 0; + let mut queue: VecDeque = VecDeque::new(); + queue.push_back(path.into()); + + while let Some(current_path) = queue.pop_front() { + let mut entries = read_dir(¤t_path).await.unwrap(); + + while let Some(entry) = entries.next_entry().await.unwrap() { + let metadata = entry.metadata().await.unwrap(); + + if metadata.is_file() { + total_size += metadata.len(); + } else if metadata.is_dir() { + queue.push_back(entry.path()); + } + } + } + Ok(total_size) +} diff --git a/src/infrastructure/utils/hash.rs b/src/infrastructure/utils/hash.rs new file mode 100644 index 0000000..50640d5 --- /dev/null +++ b/src/infrastructure/utils/hash.rs @@ -0,0 +1,20 @@ +use xxhash_rust::xxh32::xxh32; + +pub fn calculate_32(data: &[u8]) -> u32 { + xxh32(data, 0) +} + +pub fn calculate_256(data: &[u8]) -> String { + blake3::hash(data).to_hex().to_string() +} + +#[cfg(test)] +mod tests { + #[test] + fn given_same_input_calculate_should_produce_same_output() { + let input = "hello world".as_bytes(); + let output1 = super::calculate_32(input); + let output2 = super::calculate_32(input); + assert_eq!(output1, output2); + } +} diff --git a/src/infrastructure/utils/mod.rs b/src/infrastructure/utils/mod.rs new file mode 100644 index 0000000..0aebd88 --- /dev/null +++ b/src/infrastructure/utils/mod.rs @@ -0,0 +1,4 @@ +pub mod crypto; +pub mod file; +pub mod hash; +pub mod random_id; diff --git a/src/infrastructure/utils/random_id.rs b/src/infrastructure/utils/random_id.rs new file mode 100644 index 0000000..8c050ef --- /dev/null +++ b/src/infrastructure/utils/random_id.rs @@ -0,0 +1,10 @@ +use ulid::Ulid; +use uuid::Uuid; + +pub fn get_uuid() -> u128 { + Uuid::new_v4().to_u128_le() +} + +pub fn get_ulid() -> Ulid { + Ulid::new() +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8bef734 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,14 @@ +pub mod args; +pub mod binary; +pub mod channels; +pub mod configs; +pub mod http; +pub mod iggy; +pub mod infrastructure; +pub mod logging; +pub mod models; +pub mod quic; +pub mod server_error; +pub mod tcp; +pub mod utils; +pub mod webtransport; diff --git a/src/logging/mod.rs b/src/logging/mod.rs new file mode 100644 index 0000000..b8b4e90 --- /dev/null +++ b/src/logging/mod.rs @@ -0,0 +1,253 @@ +use crate::configs::system::LoggingConfig; +use crate::server_error::ServerError; +use std::io::{self, Write}; +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; +use tracing::{info, trace}; +use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::{ + filter::LevelFilter, fmt, fmt::MakeWriter, prelude::*, reload, reload::Handle, Layer, Registry, +}; + +const NIGIG_LOG_FILE_PREFIX: &str = "nigig-server.log"; +// const LOG_FILE_PREFIX: &str = "server.log"; + +// Writer that does nothing +struct NullWriter; +impl Write for NullWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +// Wrapper around Arc>> to implement Write +struct VecStringWriter(Arc>>); +impl Write for VecStringWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let mut lock = self.0.lock().unwrap(); + lock.push(String::from_utf8_lossy(buf).into_owned()); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + // Just nop, we don't need to flush anything + Ok(()) + } +} + +// This struct exists solely to implement MakeWriter +struct VecStringMakeWriter(Arc>>); +impl<'a> MakeWriter<'a> for VecStringMakeWriter { + type Writer = VecStringWriter; + + fn make_writer(&'a self) -> Self::Writer { + VecStringWriter(self.0.clone()) + } +} + +pub trait EarlyLogDumper { + fn dump_to_file(&self, writer: &mut W); + fn dump_to_stdout(&self); +} + +impl EarlyLogDumper for Logging { + fn dump_to_file(&self, writer: &mut W) { + let early_logs_buffer = self.early_logs_buffer.lock().unwrap(); + for log in early_logs_buffer.iter() { + let log = strip_ansi_escapes::strip(log); + writer.write_all(&log).unwrap(); + } + } + + fn dump_to_stdout(&self) { + let stdout = io::stdout(); + let mut handle = stdout.lock(); + let early_logs_buffer = self.early_logs_buffer.lock().unwrap(); + for log in early_logs_buffer.iter() { + handle.write_all(log.as_bytes()).unwrap(); + } + } +} + +// Make reload::Layer::new more readable +type ReloadHandle = Handle + Send + Sync>, Registry>; + +pub struct Logging { + stdout_guard: Option, + stdout_reload_handle: Option, + + file_guard: Option, + file_reload_handle: Option, + + filtering_reload_handle: Option, + + early_logs_buffer: Arc>>, +} + +impl Logging { + pub fn new() -> Self { + Self { + stdout_guard: None, + stdout_reload_handle: None, + file_guard: None, + file_reload_handle: None, + filtering_reload_handle: None, + early_logs_buffer: Arc::new(Mutex::new(vec![])), + } + } + + pub fn early_init(&mut self) { + // Initialize layers + // First layer is filtering based on severity + // Second layer will just consume drain log entries and has first layer as a dependency + // Third layer will write to a safe buffer and has first layer as a dependency + // All layers will be replaced during late_init + let mut layers = vec![]; + + let filtering_layer = Self::get_filtering_level(None); + let (_, filtering_layer_reload_handle) = reload::Layer::new(filtering_layer.boxed()); + self.filtering_reload_handle = Some(filtering_layer_reload_handle); + + let stdout_layer = fmt::Layer::default().with_writer(|| NullWriter); + let (stdout_layer, stdout_layer_reload_handle) = reload::Layer::new(stdout_layer.boxed()); + self.stdout_reload_handle = Some(stdout_layer_reload_handle); + layers.push(stdout_layer.and_then(filtering_layer)); + + let file_layer = fmt::Layer::default() + .with_target(true) + .with_writer(VecStringMakeWriter(self.early_logs_buffer.clone())) + .with_ansi(true); + let (file_layer, file_layer_reload_handle) = reload::Layer::new(file_layer.boxed()); + self.file_reload_handle = Some(file_layer_reload_handle); + layers.push(file_layer.and_then(filtering_layer)); + + let subscriber = tracing_subscriber::registry().with(layers); + + tracing::subscriber::set_global_default(subscriber) + .expect("Setting global default subscriber failed"); + + if option_env!("NIGIG_CI_BUILD") == Some("true") { + let version = option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"); + let hash = option_env!("VERGEN_GIT_SHA").unwrap_or("unknown"); + let built_at = option_env!("VERGEN_BUILD_TIMESTAMP").unwrap_or("unknown"); + let rust_version = option_env!("VERGEN_RUSTC_SEMVER").unwrap_or("unknown"); + let target = option_env!("VERGEN_CARGO_TARGET_TRIPLE").unwrap_or("unknown"); + info!( + "Version: {}, hash: {}, built at: {} using rust version: {} for target: {}", + version, hash, built_at, rust_version, target + ); + } else { + info!("It seems that you are a developer. Environment variable NIGIG_CI_BUILD is not set to 'true', skipping build info print.") + } + + // This is moment when we can start logging something and not worry about losing it. + } + + pub fn late_init( + &mut self, + base_directory: String, + config: &LoggingConfig, + ) -> Result<(), ServerError> { + // Write to stdout and file at the same time. + // Use the non_blocking appender to avoid blocking the threads. + // Use the rolling appender to avoid having a huge log file. + // Make sure logs are dumped to the file during graceful shutdown. + + trace!("Logging config: {}", config); + + let filtering_level = Self::get_filtering_level(Some(config)); + let _ = self + .filtering_reload_handle + .as_ref() + .ok_or(ServerError::FilterReloadFailure)? + .modify(|layer| *layer = filtering_level.boxed()); + + // Initialize non-blocking stdout layer + let (_, stdout_guard) = tracing_appender::non_blocking(std::io::stdout()); + let stdout_layer = fmt::Layer::default().with_ansi(true).boxed(); + self.stdout_guard = Some(stdout_guard); + + let _ = self + .stdout_reload_handle + .as_ref() + .ok_or(ServerError::StdoutReloadFailure)? + .modify(|layer| *layer = stdout_layer); + + self.dump_to_stdout(); + + // Initialize directory and file for logs + let base_directory = PathBuf::from(base_directory); + let logs_subdirectory = PathBuf::from(config.path.clone()); + let logs_path = base_directory.join(logs_subdirectory.clone()); + let file_appender = + tracing_appender::rolling::hourly(logs_path.clone(), NIGIG_LOG_FILE_PREFIX); + let (mut non_blocking_file, file_guard) = tracing_appender::non_blocking(file_appender); + + self.dump_to_file(&mut non_blocking_file); + + let file_layer = fmt::layer() + .with_target(true) + .with_writer(non_blocking_file) + .with_ansi(false) + // .with_filter("xitca_http=off") + .boxed(); + + self.file_guard = Some(file_guard); + let _ = self + .file_reload_handle + .as_ref() + .ok_or(ServerError::FileReloadFailure)? + .modify(|layer| *layer = file_layer); + + info!( + "Logging initialized, logs will be stored at: {:?}. Logs will be rotated hourly. Log level is: {}.", + logs_path, filtering_level + ); + + Ok(()) + } + + // RUST_LOG always takes precedence over config + fn get_filtering_level(config: Option<&LoggingConfig>) -> LevelFilter { + if let Ok(rust_log) = std::env::var("RUST_LOG") { + // Parse log level from RUST_LOG env variable + if let Ok(level) = LevelFilter::from_str(&rust_log.to_uppercase()) { + level + } else { + println!("Invalid RUST_LOG value: {}, falling back to info", rust_log); + LevelFilter::INFO + } + } else { + // Parse log level from config + if let Some(config) = config { + if let Ok(level) = LevelFilter::from_str(&config.level.to_uppercase()) { + level + } else { + println!( + "Invalid log level in config: {}, falling back to info", + config.level + ); + LevelFilter::INFO + } + } else { + // config not provided + LevelFilter::INFO + } + } + } + + fn _install_log_rotation_handler(&self) { + todo!("Implement log rotation handler based on size and retention time"); + } +} + +impl Default for Logging { + fn default() -> Self { + Self::new() + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..63bdf2d --- /dev/null +++ b/src/main.rs @@ -0,0 +1,288 @@ +#![allow(warnings)] +#![allow(stable_features)] + +use clap::Parser; +use dev::channels::commands::clean_personal_access_tokens::CleanPersonalAccessTokensExecutor; +use dev::channels::handler::ServerCommandHandler; +use dev::quic::quic_server; +use dev::tcp::tcp_server; +use figlet_rs::FIGfont; +// use std::sync::Arc; +use tokio::time::Instant; +use tracing::info; + +use dev::infrastructure::systems::system::{SharedSystem, System}; +use dev::{ + args::Args, + configs::{config_provider, server::ServerConfig}, + logging::Logging, + server_error::ServerError, + // tcp::tcp_server::start, +}; +// use dev::http::http_server; +use dev::configs::http::HttpConfig; + +#[derive(Debug, Parser)] +struct Cli { + /// The port to run the server on + port: i32, +} +// cfg_if! { +// if #[cfg(feature = "axum")] { +#[tokio::main] +async fn main() -> Result<(), ServerError> { + // async fn main() -> Result<(), Box> { + + let startup_timestamp = Instant::now(); + let standard_font = FIGfont::standard().unwrap(); + let figure = standard_font.convert("App Server"); + println!("{}", figure.unwrap()); + + let mut logging = Logging::new(); + logging.early_init(); + + // env_logger::init(); + // let args = Cli::parse(); + let args = Args::parse(); + + info!("Parsed arguments: {:?}", args); + + let config_provider = config_provider::resolve(&args.config_provider)?; + let config = ServerConfig::load(config_provider.as_ref()).await?; + + logging.late_init(config.system.get_system_path(), &config.system.logging)?; + + // let mut system = System::new(config.system.clone(), None, config.personal_access_token); + let mut system = System::new(config.system.clone(), None, config.personal_access_token); + + system.init().await?; + let system = SharedSystem::new(system); + let _command_handler = ServerCommandHandler::new(system.clone(), &config) + // .install_handler(SaveMessagesExecutor) + // .install_handler(CleanMessagesExecutor) + .install_handler(CleanPersonalAccessTokensExecutor); + + #[cfg(unix)] + let (mut ctrl_c, mut sigterm) = { + use tokio::signal::unix::{signal, SignalKind}; + ( + signal(SignalKind::interrupt())?, + signal(SignalKind::terminate())?, + ) + }; + let mut current_config = config.clone(); + + if config.quic.enabled { + let quic_addr = quic_server::start(config.quic, system.clone()); + current_config.quic.address = quic_addr.to_string(); + } + // if config.tcp.enabled { + // // run(args.port).await; + // // dev::tcp::tcp_server::start(config.tcp); + // // let http_addr = dev::tcp::tcp_server::start(config.http, system.clone()).await; + // let _tcp_addr = dev::tcp::tcp_server::start(config.tcp, system.clone()).await; + // // current_config.tcp.address = tcp_addr.to_string(); + // // tcp_server::start(config.tcp, system.clone()); + // } + if config.tcp.enabled { + let tcp_addr = tcp_server::start(config.tcp, system.clone()).await; + current_config.tcp.address = tcp_addr.to_string(); + } + if config.http.variants.axum_enabled { + let system = system.clone(); + // tokio::spawn(async move { + // dev::http::http_server::start(config.http, system).await; + // dev::http::testserver::start(config.http, system).await; + // + let http_addr = + dev::http::axum_http::http_server::start(&config.http, system.clone()).await; + current_config.http.address[0] = http_addr.to_string(); + // }); + } + if config.http.variants.xitca_enabled { + let system = system.clone(); + let (http_addr, srv) = + dev::http::xitcav_http::http_server::startxitca(config.http, system.clone()).await; + srv.await; + current_config.http.address[1] = http_addr.to_string(); + } + + let runtime_path = current_config.system.get_runtime_path(); + // tracing::warn!("================\n{}", runtime_path); + let current_config_path = format!("{}/current_config.toml", runtime_path); + let current_config_content = + toml::to_string(¤t_config).expect("Cannot serialize current_config"); + + // tracing::warn!("===============\n{}", current_config_content); + // tracing::warn!("===============\n{}", current_config_path); + tokio::fs::write(current_config_path, current_config_content).await?; + + let elapsed_time = startup_timestamp.elapsed(); + info!( + "dev server has started - overall startup took {} ms.", + elapsed_time.as_millis() + ); + #[cfg(unix)] + tokio::select! { + _ = ctrl_c.recv() => { + info!("Received SIGINT. Shutting down dev server..."); + }, + _ = sigterm.recv() => { + info!("Received SIGTERM. Shutting down dev server..."); + } + } + + let shutdown_timestamp = Instant::now(); + // let mut system = system.write().await; + // let persister = Arc::new(FileWithSyncPersister); + // let storage = Arc::new(FileSegmentStorage::new(persister)); + // system.shutdown(storage).await?; + let elapsed_time = shutdown_timestamp.elapsed(); + + info!( + "dev server has shutdown successfully. Shutdown took {} ms.", + elapsed_time.as_millis() + ); + Ok(()) +} + +// } else if #[cfg(feature = "xitca")] { +// use xitca_codegen::State; +// use xitca_http::Request; +// use xitca_unsafe_collection::futures::NowOrPanic; + +// use xitca_web::{ +// App,WebContext, +// body::RequestBody, +// dev::service::Service, +// handler::{ +// extension::ExtensionRef, extension::ExtensionsRef, handler_service, path::PathRef, state::StateRef, +// uri::UriRef, +// }, +// http::{const_header_value::TEXT_UTF8, header::CONTENT_TYPE, Method, Uri}, +// middleware::UncheckedReady, +// route::get, +// }; +// use std::net::SocketAddr; + +// #[derive(State, Clone, Debug, Eq, PartialEq)] +// struct State { +// #[borrow] +// field1: String, +// #[borrow] +// field2: u32, +// } + +// async fn handler( +// StateRef(state): StateRef<'_, String>, +// StateRef(state2): StateRef<'_, u32>, +// StateRef(state3): StateRef<'_, State>, +// ctx: &WebContext<'_, State>, +// ) -> String { +// assert_eq!("state", state); +// assert_eq!(&996, state2); +// assert_eq!(state, ctx.state().field1.as_str()); +// assert_eq!(state3, ctx.state()); +// state.to_string() +// } +// // fn main() -> std::io::Result<()> { +// fn main() -> Result<(), ServerError> { +// tracing_subscriber::fmt() +// .with_env_filter("xitca=info,[xitca-logger]=trace") +// .init(); + +// let startup_timestamp = Instant::now(); +// let standard_font = FIGfont::standard().unwrap(); +// let figure = standard_font.convert("App Server"); +// println!("{}", figure.unwrap()); + +// let mut logging = Logging::new(); +// logging.early_init(); + +// // env_logger::init(); +// // let args = Cli::parse(); +// let args = Args::parse(); + +// info!("Parsed arguments: {:?}", args); + +// // let config_provider = config_provider::resolve(&args.config_provider).map_err(|err| { +// // // Your code when config_provider::resolve fails +// // tracing::error!("Error resolving config provider: {}", err); +// // // Handle the error appropriately, e.g., return an Err or panic +// // });; +// // let config = ServerConfig::load(config_provider.as_ref()); + +// // logging.late_init(config.system.get_system_path(), &config.system.logging).map_err(|err| { +// // // Your code when config_provider::resolve fails +// // tracing::error!("Error resolving config provider: {}", err); +// // // Handle the error appropriately, e.g., return an Err or panic +// // }); + +// // // let mut system = System::new(config.system.clone(), None, config.personal_access_token); +// // let mut system = System::new(config.system.clone(), None, config.personal_access_token); + +// // system.init().await.map_err(|err| { +// // // Your code when config_provider::resolve fails +// // tracing::error!("Error resolving config provider: {}", err); +// // // Handle the error appropriately, e.g., return an Err or panic +// // }); +// // let system = SharedSystem::new(system); + +// // if config.http.enabled { +// // let system = system.clone(); +// // tokio::spawn(async move { +// // startxitca(config.http, system).await; +// // }); +// // } +// Ok(()) + +// // .finish() +// // .call(()) +// // .now_or_panic() +// // .ok() +// // .unwrap() +// // .call(Request::default()) +// // .now_or_panic() +// // .unwrap() +// } +// pub async fn startxitca(config: HttpConfig, system: SharedSystem) -> SocketAddr { +// let api_name = if config.tls.enabled { +// "HTTP API (TLS)" +// } else { +// "HTTP API" +// }; + +// let listener = tokio::net::TcpListener::bind(config.address).await.unwrap(); +// let address = listener +// .local_addr() +// .expect("Failed to get local address for HTTP server"); + +// let state = State { +// field1: String::from("state"), +// field2: 996, +// }; + +// tokio::task::spawn(async move { +// // axum::serve( +// // listener, +// // app.into_make_service_with_connect_info::(), +// // ) +// // .await +// // .expect("Failed to start HTTP server"); +// App::with_state(state) +// .at("/", get(handler_service(handler))) +// .serve() +// .bind("127.0.0.1:8080").expect("REASON") +// .run() +// .wait(); +// }); + +// address +// } + +// } else { +// pub fn main() { + +// } +// } +// } diff --git a/src/mod.rs b/src/mod.rs new file mode 100644 index 0000000..d2706f0 --- /dev/null +++ b/src/mod.rs @@ -0,0 +1,252 @@ +use crate::configs::system::LoggingConfig; +use crate::server_error::ServerError; +use std::io::{self, Write}; +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; +use tracing::{info, trace}; +use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::{ + filter::LevelFilter, fmt, fmt::MakeWriter, prelude::*, reload, reload::Handle, Layer, Registry, +}; + +const NIGIG_LOG_FILE_PREFIX: &str = "nigig-server.log"; +// const LOG_FILE_PREFIX: &str = "server.log"; + +// Writer that does nothing +struct NullWriter; +impl Write for NullWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +// Wrapper around Arc>> to implement Write +struct VecStringWriter(Arc>>); +impl Write for VecStringWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let mut lock = self.0.lock().unwrap(); + lock.push(String::from_utf8_lossy(buf).into_owned()); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + // Just nop, we don't need to flush anything + Ok(()) + } +} + +// This struct exists solely to implement MakeWriter +struct VecStringMakeWriter(Arc>>); +impl<'a> MakeWriter<'a> for VecStringMakeWriter { + type Writer = VecStringWriter; + + fn make_writer(&'a self) -> Self::Writer { + VecStringWriter(self.0.clone()) + } +} + +pub trait EarlyLogDumper { + fn dump_to_file(&self, writer: &mut W); + fn dump_to_stdout(&self); +} + +impl EarlyLogDumper for Logging { + fn dump_to_file(&self, writer: &mut W) { + let early_logs_buffer = self.early_logs_buffer.lock().unwrap(); + for log in early_logs_buffer.iter() { + let log = strip_ansi_escapes::strip(log); + writer.write_all(&log).unwrap(); + } + } + + fn dump_to_stdout(&self) { + let stdout = io::stdout(); + let mut handle = stdout.lock(); + let early_logs_buffer = self.early_logs_buffer.lock().unwrap(); + for log in early_logs_buffer.iter() { + handle.write_all(log.as_bytes()).unwrap(); + } + } +} + +// Make reload::Layer::new more readable +type ReloadHandle = Handle + Send + Sync>, Registry>; + +pub struct Logging { + stdout_guard: Option, + stdout_reload_handle: Option, + + file_guard: Option, + file_reload_handle: Option, + + filtering_reload_handle: Option, + + early_logs_buffer: Arc>>, +} + +impl Logging { + pub fn new() -> Self { + Self { + stdout_guard: None, + stdout_reload_handle: None, + file_guard: None, + file_reload_handle: None, + filtering_reload_handle: None, + early_logs_buffer: Arc::new(Mutex::new(vec![])), + } + } + + pub fn early_init(&mut self) { + // Initialize layers + // First layer is filtering based on severity + // Second layer will just consume drain log entries and has first layer as a dependency + // Third layer will write to a safe buffer and has first layer as a dependency + // All layers will be replaced during late_init + let mut layers = vec![]; + + let filtering_layer = Self::get_filtering_level(None); + let (_, filtering_layer_reload_handle) = reload::Layer::new(filtering_layer.boxed()); + self.filtering_reload_handle = Some(filtering_layer_reload_handle); + + let stdout_layer = fmt::Layer::default().with_writer(|| NullWriter); + let (stdout_layer, stdout_layer_reload_handle) = reload::Layer::new(stdout_layer.boxed()); + self.stdout_reload_handle = Some(stdout_layer_reload_handle); + layers.push(stdout_layer.and_then(filtering_layer)); + + let file_layer = fmt::Layer::default() + .with_target(true) + .with_writer(VecStringMakeWriter(self.early_logs_buffer.clone())) + .with_ansi(true); + let (file_layer, file_layer_reload_handle) = reload::Layer::new(file_layer.boxed()); + self.file_reload_handle = Some(file_layer_reload_handle); + layers.push(file_layer.and_then(filtering_layer)); + + let subscriber = tracing_subscriber::registry().with(layers); + + tracing::subscriber::set_global_default(subscriber) + .expect("Setting global default subscriber failed"); + + if option_env!("NIGIG_CI_BUILD") == Some("true") { + let version = option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"); + let hash = option_env!("VERGEN_GIT_SHA").unwrap_or("unknown"); + let built_at = option_env!("VERGEN_BUILD_TIMESTAMP").unwrap_or("unknown"); + let rust_version = option_env!("VERGEN_RUSTC_SEMVER").unwrap_or("unknown"); + let target = option_env!("VERGEN_CARGO_TARGET_TRIPLE").unwrap_or("unknown"); + info!( + "Version: {}, hash: {}, built at: {} using rust version: {} for target: {}", + version, hash, built_at, rust_version, target + ); + } else { + info!("It seems that you are a developer. Environment variable NIGIG_CI_BUILD is not set to 'true', skipping build info print.") + } + + // This is moment when we can start logging something and not worry about losing it. + } + + pub fn late_init( + &mut self, + base_directory: String, + config: &LoggingConfig, + ) -> Result<(), ServerError> { + // Write to stdout and file at the same time. + // Use the non_blocking appender to avoid blocking the threads. + // Use the rolling appender to avoid having a huge log file. + // Make sure logs are dumped to the file during graceful shutdown. + + trace!("Logging config: {}", config); + + let filtering_level = Self::get_filtering_level(Some(config)); + let _ = self + .filtering_reload_handle + .as_ref() + .ok_or(ServerError::FilterReloadFailure)? + .modify(|layer| *layer = filtering_level.boxed()); + + // Initialize non-blocking stdout layer + let (_, stdout_guard) = tracing_appender::non_blocking(std::io::stdout()); + let stdout_layer = fmt::Layer::default().with_ansi(true).boxed(); + self.stdout_guard = Some(stdout_guard); + + let _ = self + .stdout_reload_handle + .as_ref() + .ok_or(ServerError::StdoutReloadFailure)? + .modify(|layer| *layer = stdout_layer); + + self.dump_to_stdout(); + + // Initialize directory and file for logs + let base_directory = PathBuf::from(base_directory); + let logs_subdirectory = PathBuf::from(config.path.clone()); + let logs_path = base_directory.join(logs_subdirectory.clone()); + let file_appender = + tracing_appender::rolling::hourly(logs_path.clone(), NIGIG_LOG_FILE_PREFIX); + let (mut non_blocking_file, file_guard) = tracing_appender::non_blocking(file_appender); + + self.dump_to_file(&mut non_blocking_file); + + let file_layer = fmt::layer() + .with_target(true) + .with_writer(non_blocking_file) + .with_ansi(false) + .boxed(); + + self.file_guard = Some(file_guard); + let _ = self + .file_reload_handle + .as_ref() + .ok_or(ServerError::FileReloadFailure)? + .modify(|layer| *layer = file_layer); + + info!( + "Logging initialized, logs will be stored at: {:?}. Logs will be rotated hourly. Log level is: {}.", + logs_path, filtering_level + ); + + Ok(()) + } + + // RUST_LOG always takes precedence over config + fn get_filtering_level(config: Option<&LoggingConfig>) -> LevelFilter { + if let Ok(rust_log) = std::env::var("RUST_LOG") { + // Parse log level from RUST_LOG env variable + if let Ok(level) = LevelFilter::from_str(&rust_log.to_uppercase()) { + level + } else { + println!("Invalid RUST_LOG value: {}, falling back to info", rust_log); + LevelFilter::INFO + } + } else { + // Parse log level from config + if let Some(config) = config { + if let Ok(level) = LevelFilter::from_str(&config.level.to_uppercase()) { + level + } else { + println!( + "Invalid log level in config: {}, falling back to info", + config.level + ); + LevelFilter::INFO + } + } else { + // config not provided + LevelFilter::INFO + } + } + } + + fn _install_log_rotation_handler(&self) { + todo!("Implement log rotation handler based on size and retention time"); + } +} + +impl Default for Logging { + fn default() -> Self { + Self::new() + } +} diff --git a/src/models/binary/binary_client.rs b/src/models/binary/binary_client.rs new file mode 100644 index 0000000..735ee56 --- /dev/null +++ b/src/models/binary/binary_client.rs @@ -0,0 +1,27 @@ +use crate::infrastructure::error::Error; +use crate::models::client::Client; +use async_trait::async_trait; + +use bytes::Bytes; + +/// The state of the client. +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum ClientState { + /// The client is disconnected. + Disconnected, + /// The client is connected. + Connected, + /// The client is connected and authenticated. + Authenticated, +} + +/// A client that can send and receive binary messages. +#[async_trait] +pub trait BinaryClient: Client { + /// Gets the state of the client. + async fn get_state(&self) -> ClientState; + /// Sets the state of the client. + async fn set_state(&self, state: ClientState); + /// Sends a command and returns the response. + async fn send_with_response(&self, command: u32, payload: Bytes) -> Result; +} diff --git a/src/models/binary/mapper.rs b/src/models/binary/mapper.rs new file mode 100644 index 0000000..7c2a41e --- /dev/null +++ b/src/models/binary/mapper.rs @@ -0,0 +1,287 @@ +use crate::infrastructure::error::Error; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::client_info::{ClientInfo, ClientInfoDetails}; +// use crate::models::consumer_group::{ConsumerGroup, ConsumerGroupDetails, ConsumerGroupMember}; +// use crate::models::consumer_offset_info::ConsumerOffsetInfo; +use crate::models::identity_info::IdentityInfo; +// use crate::models::messages::{Message, MessageState, PolledMessages}; +// use crate::models::partition::Partition; +use crate::models::permissions::Permissions; +use crate::models::personal_access_token::{PersonalAccessTokenInfo, RawPersonalAccessToken}; +use crate::models::stats::Stats; +// use crate::models::stream::{Stream, StreamDetails}; +// use crate::models::topic::{Topic, TopicDetails}; +use crate::models::user_info::{UserInfo, UserInfoDetails}; +use crate::models::user_status::UserStatus; +// use bytes::Bytes; +// use std::collections::HashMap; +use bytes::Bytes; +use std::str::from_utf8; + +// const EMPTY_MESSAGES: Vec = vec![]; +// const EMPTY_TOPICS: Vec = vec![]; +// const EMPTY_STREAMS: Vec = vec![]; +const EMPTY_CLIENTS: Vec = vec![]; +const EMPTY_USERS: Vec = vec![]; +const EMPTY_PERSONAL_ACCESS_TOKENS: Vec = vec![]; +// const EMPTY_CONSUMER_GROUPS: Vec = vec![]; + +pub fn map_stats(payload: Bytes) -> Result { + let process_id = u32::from_le_bytes(payload[..4].try_into()?); + let cpu_usage = f32::from_le_bytes(payload[4..8].try_into()?); + let memory_usage = u64::from_le_bytes(payload[8..16].try_into()?).into(); + let total_memory = u64::from_le_bytes(payload[16..24].try_into()?).into(); + let available_memory = u64::from_le_bytes(payload[24..32].try_into()?).into(); + let run_time = u64::from_le_bytes(payload[32..40].try_into()?); + let start_time = u64::from_le_bytes(payload[40..48].try_into()?); + let read_bytes = u64::from_le_bytes(payload[48..56].try_into()?).into(); + let written_bytes = u64::from_le_bytes(payload[56..64].try_into()?).into(); + // let total_size_bytes = u64::from_le_bytes(payload[64..72].try_into()?).into(); + // let streams_count = u32::from_le_bytes(payload[72..76].try_into()?); + // let topics_count = u32::from_le_bytes(payload[76..80].try_into()?); + // let partitions_count = u32::from_le_bytes(payload[80..84].try_into()?); + // let segments_count = u32::from_le_bytes(payload[84..88].try_into()?); + // let messages_count = u64::from_le_bytes(payload[88..96].try_into()?); + let clients_count = u32::from_le_bytes(payload[96..100].try_into()?); + // let consumer_groups_count = u32::from_le_bytes(payload[100..104].try_into()?); + let mut current_position = 104; + let hostname_length = + u32::from_le_bytes(payload[current_position..current_position + 4].try_into()?) as usize; + let hostname = + from_utf8(&payload[current_position + 4..current_position + 4 + hostname_length])? + .to_string(); + current_position += 4 + hostname_length; + let os_name_length = + u32::from_le_bytes(payload[current_position..current_position + 4].try_into()?) as usize; + let os_name = from_utf8(&payload[current_position + 4..current_position + 4 + os_name_length])? + .to_string(); + current_position += 4 + os_name_length; + let os_version_length = + u32::from_le_bytes(payload[current_position..current_position + 4].try_into()?) as usize; + let os_version = + from_utf8(&payload[current_position + 4..current_position + 4 + os_version_length])? + .to_string(); + current_position += 4 + os_version_length; + let kernel_version_length = + u32::from_le_bytes(payload[current_position..current_position + 4].try_into()?) as usize; + let kernel_version = + from_utf8(&payload[current_position + 4..current_position + 4 + kernel_version_length])? + .to_string(); + + Ok(Stats { + process_id, + cpu_usage, + memory_usage, + total_memory, + available_memory, + run_time, + start_time, + read_bytes, + written_bytes, + // messages_size_bytes: total_size_bytes, + // streams_count, + // topics_count, + // partitions_count, + // segments_count, + // messages_count, + clients_count, + // consumer_groups_count, + hostname, + os_name, + os_version, + kernel_version, + }) +} + +pub fn map_user(payload: Bytes) -> Result { + let (user, position) = map_to_user_info(payload.clone(), 0)?; + let has_permissions = payload[position]; + let permissions = if has_permissions == 1 { + let permissions_length = + u32::from_le_bytes(payload[position + 1..position + 5].try_into()?) as usize; + let permissions = payload.slice(position + 5..position + 5 + permissions_length); + Some(Permissions::from_bytes(permissions)?) + } else { + None + }; + + let user = UserInfoDetails { + id: user.id, + created_at: user.created_at, + status: user.status, + username: user.username, + permissions, + }; + Ok(user) +} + +pub fn map_users(payload: Bytes) -> Result, Error> { + if payload.is_empty() { + return Ok(EMPTY_USERS); + } + + let mut users = Vec::new(); + let length = payload.len(); + let mut position = 0; + while position < length { + let (user, read_bytes) = map_to_user_info(payload.clone(), position)?; + users.push(user); + position += read_bytes; + } + users.sort_by(|x, y| x.id.cmp(&y.id)); + Ok(users) +} + +pub fn map_personal_access_tokens(payload: Bytes) -> Result, Error> { + if payload.is_empty() { + return Ok(EMPTY_PERSONAL_ACCESS_TOKENS); + } + + let mut personal_access_tokens = Vec::new(); + let length = payload.len(); + let mut position = 0; + while position < length { + let (personal_access_token, read_bytes) = map_to_pat_info(payload.clone(), position)?; + personal_access_tokens.push(personal_access_token); + position += read_bytes; + } + personal_access_tokens.sort_by(|x, y| x.name.cmp(&y.name)); + Ok(personal_access_tokens) +} + +pub fn map_identity_info(payload: Bytes) -> Result { + let user_id = u32::from_le_bytes(payload[..4].try_into()?); + Ok(IdentityInfo { + user_id, + tokens: None, + }) +} + +pub fn map_raw_pat(payload: Bytes) -> Result { + let token_length = payload[0]; + let token = from_utf8(&payload[1..1 + token_length as usize])?.to_string(); + Ok(RawPersonalAccessToken { token }) +} + +pub fn map_client(payload: Bytes) -> Result { + let (client, mut position) = map_to_client_info(payload.clone(), 0)?; + // let mut consumer_groups = Vec::new(); + let length = payload.len(); + // while position < length { + // for _ in 0..client.consumer_groups_count { + // let stream_id = u32::from_le_bytes(payload[position..position + 4].try_into()?); + // let topic_id = u32::from_le_bytes(payload[position + 4..position + 8].try_into()?); + // let consumer_group_id = + // u32::from_le_bytes(payload[position + 8..position + 12].try_into()?); + // let consumer_group = ConsumerGroupInfo { + // stream_id, + // topic_id, + // consumer_group_id, + // }; + // consumer_groups.push(consumer_group); + // position += 12; + // } + // } + + // consumer_groups.sort_by(|x, y| x.consumer_group_id.cmp(&y.consumer_group_id)); + let client = ClientInfoDetails { + client_id: client.client_id, + user_id: client.user_id, + address: client.address, + transport: client.transport, + // consumer_groups_count: client.consumer_groups_count, + // consumer_groups, + }; + Ok(client) +} + +pub fn map_clients(payload: Bytes) -> Result, Error> { + if payload.is_empty() { + return Ok(EMPTY_CLIENTS); + } + + let mut clients = Vec::new(); + let length = payload.len(); + let mut position = 0; + while position < length { + let (client, read_bytes) = map_to_client_info(payload.clone(), position)?; + clients.push(client); + position += read_bytes; + } + clients.sort_by(|x, y| x.client_id.cmp(&y.client_id)); + Ok(clients) +} + +fn map_to_client_info(payload: Bytes, mut position: usize) -> Result<(ClientInfo, usize), Error> { + let mut read_bytes; + let client_id = u32::from_le_bytes(payload[position..position + 4].try_into()?); + let user_id = u32::from_le_bytes(payload[position + 4..position + 8].try_into()?); + let user_id = match user_id { + 0 => None, + _ => Some(user_id), + }; + + let transport = payload[position + 8]; + let transport = match transport { + 1 => "TCP", + 2 => "QUIC", + _ => "Unknown", + } + .to_string(); + + let address_length = + u32::from_le_bytes(payload[position + 9..position + 13].try_into()?) as usize; + let address = from_utf8(&payload[position + 13..position + 13 + address_length])?.to_string(); + read_bytes = 4 + 4 + 1 + 4 + address_length; + position += read_bytes; + let consumer_groups_count = u32::from_le_bytes(payload[position..position + 4].try_into()?); + read_bytes += 4; + Ok(( + ClientInfo { + client_id, + user_id, + address, + transport, + consumer_groups_count, + }, + read_bytes, + )) +} + +fn map_to_user_info(payload: Bytes, position: usize) -> Result<(UserInfo, usize), Error> { + let id = u32::from_le_bytes(payload[position..position + 4].try_into()?); + let created_at = u64::from_le_bytes(payload[position + 4..position + 12].try_into()?); + let status = payload[position + 12]; + let status = UserStatus::from_code(status)?; + let username_length = payload[position + 13]; + let username = + from_utf8(&payload[position + 14..position + 14 + username_length as usize])?.to_string(); + let read_bytes = 4 + 8 + 1 + 1 + username_length as usize; + + Ok(( + UserInfo { + id, + created_at, + status, + username, + }, + read_bytes, + )) +} + +fn map_to_pat_info( + payload: Bytes, + position: usize, +) -> Result<(PersonalAccessTokenInfo, usize), Error> { + let name_length = payload[position]; + let name = from_utf8(&payload[position + 1..position + 1 + name_length as usize])?.to_string(); + let position = position + 1 + name_length as usize; + let expiry = u64::from_le_bytes(payload[position..position + 8].try_into()?); + let expiry = match expiry { + 0 => None, + _ => Some(expiry), + }; + let read_bytes = 1 + name_length as usize + 8; + + Ok((PersonalAccessTokenInfo { name, expiry }, read_bytes)) +} diff --git a/src/models/binary/mod.rs b/src/models/binary/mod.rs new file mode 100644 index 0000000..aa6af3f --- /dev/null +++ b/src/models/binary/mod.rs @@ -0,0 +1,21 @@ +use crate::infrastructure::error::Error; +use crate::models::binary::binary_client::{BinaryClient, ClientState}; + +pub mod binary_client; +// pub mod consumer_groups; +// pub mod consumer_offsets; +mod mapper; +// pub mod messages; +// pub mod partitions; +pub mod personal_access_tokens; +// pub mod streams; +pub mod system; +// pub mod topics; +pub mod users; + +async fn fail_if_not_authenticated(client: &dyn BinaryClient) -> Result<(), Error> { + if client.get_state().await != ClientState::Authenticated { + return Err(Error::Unauthenticated); + } + Ok(()) +} diff --git a/src/models/binary/personal_access_tokens.rs b/src/models/binary/personal_access_tokens.rs new file mode 100644 index 0000000..69bd801 --- /dev/null +++ b/src/models/binary/personal_access_tokens.rs @@ -0,0 +1,58 @@ +use crate::infrastructure::error::Error; +use crate::models::binary::binary_client::{BinaryClient, ClientState}; +use crate::models::binary::{fail_if_not_authenticated, mapper}; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::client::PersonalAccessTokenClient; +use crate::models::command::*; +use crate::models::identity_info::IdentityInfo; +use crate::models::personal_access_token::{PersonalAccessTokenInfo, RawPersonalAccessToken}; +use crate::models::personal_access_tokens::create_personal_access_token::CreatePersonalAccessToken; +use crate::models::personal_access_tokens::delete_personal_access_token::DeletePersonalAccessToken; +use crate::models::personal_access_tokens::get_personal_access_tokens::GetPersonalAccessTokens; +use crate::models::personal_access_tokens::login_with_personal_access_token::LoginWithPersonalAccessToken; + +#[async_trait::async_trait] +impl PersonalAccessTokenClient for B { + async fn get_personal_access_tokens( + &self, + command: &GetPersonalAccessTokens, + ) -> Result, Error> { + fail_if_not_authenticated(self).await?; + let response = self + .send_with_response(GET_PERSONAL_ACCESS_TOKENS_CODE, command.as_bytes()) + .await?; + mapper::map_personal_access_tokens(response) + } + + async fn create_personal_access_token( + &self, + command: &CreatePersonalAccessToken, + ) -> Result { + fail_if_not_authenticated(self).await?; + let response = self + .send_with_response(CREATE_PERSONAL_ACCESS_TOKEN_CODE, command.as_bytes()) + .await?; + mapper::map_raw_pat(response) + } + + async fn delete_personal_access_token( + &self, + command: &DeletePersonalAccessToken, + ) -> Result<(), Error> { + fail_if_not_authenticated(self).await?; + self.send_with_response(DELETE_PERSONAL_ACCESS_TOKEN_CODE, command.as_bytes()) + .await?; + Ok(()) + } + + async fn login_with_personal_access_token( + &self, + command: &LoginWithPersonalAccessToken, + ) -> Result { + let response = self + .send_with_response(LOGIN_WITH_PERSONAL_ACCESS_TOKEN_CODE, command.as_bytes()) + .await?; + self.set_state(ClientState::Authenticated).await; + mapper::map_identity_info(response) + } +} diff --git a/src/models/binary/system.rs b/src/models/binary/system.rs new file mode 100644 index 0000000..916bed7 --- /dev/null +++ b/src/models/binary/system.rs @@ -0,0 +1,56 @@ +use crate::infrastructure::error::Error; +use crate::models::binary::binary_client::BinaryClient; +use crate::models::binary::{fail_if_not_authenticated, mapper}; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::client::SystemClient; +use crate::models::client_info::{ClientInfo, ClientInfoDetails}; +use crate::models::command::{ + GET_CLIENTS_CODE, GET_CLIENT_CODE, GET_ME_CODE, GET_STATS_CODE, PING_CODE, +}; +use crate::models::stats::Stats; +use crate::models::system::get_client::GetClient; +use crate::models::system::get_clients::GetClients; +use crate::models::system::get_me::GetMe; +use crate::models::system::get_stats::GetStats; +use crate::models::system::ping::Ping; + +#[async_trait::async_trait] +impl SystemClient for B { + async fn get_stats(&self, command: &GetStats) -> Result { + fail_if_not_authenticated(self).await?; + let response = self + .send_with_response(GET_STATS_CODE, command.as_bytes()) + .await?; + mapper::map_stats(response) + } + + async fn get_me(&self, command: &GetMe) -> Result { + fail_if_not_authenticated(self).await?; + let response = self + .send_with_response(GET_ME_CODE, command.as_bytes()) + .await?; + mapper::map_client(response) + } + + async fn get_client(&self, command: &GetClient) -> Result { + fail_if_not_authenticated(self).await?; + let response = self + .send_with_response(GET_CLIENT_CODE, command.as_bytes()) + .await?; + mapper::map_client(response) + } + + async fn get_clients(&self, command: &GetClients) -> Result, Error> { + fail_if_not_authenticated(self).await?; + let response = self + .send_with_response(GET_CLIENTS_CODE, command.as_bytes()) + .await?; + mapper::map_clients(response) + } + + async fn ping(&self, command: &Ping) -> Result<(), Error> { + self.send_with_response(PING_CODE, command.as_bytes()) + .await?; + Ok(()) + } +} diff --git a/src/models/binary/users.rs b/src/models/binary/users.rs new file mode 100644 index 0000000..0999868 --- /dev/null +++ b/src/models/binary/users.rs @@ -0,0 +1,87 @@ +use crate::infrastructure::error::Error; +use crate::models::binary::binary_client::{BinaryClient, ClientState}; +use crate::models::binary::{fail_if_not_authenticated, mapper}; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::client::UserClient; +use crate::models::command::*; +use crate::models::identity_info::IdentityInfo; +use crate::models::user_info::{UserInfo, UserInfoDetails}; +use crate::models::users::change_password::ChangePassword; +use crate::models::users::create_user::CreateUser; +use crate::models::users::delete_user::DeleteUser; +use crate::models::users::get_user::GetUser; +use crate::models::users::get_users::GetUsers; +use crate::models::users::login_user::LoginUser; +use crate::models::users::logout_user::LogoutUser; +use crate::models::users::update_permissions::UpdatePermissions; +use crate::models::users::update_user::UpdateUser; + +#[async_trait::async_trait] +impl UserClient for B { + async fn get_user(&self, command: &GetUser) -> Result { + fail_if_not_authenticated(self).await?; + let response = self + .send_with_response(GET_USER_CODE, command.as_bytes()) + .await?; + mapper::map_user(response) + } + + async fn get_users(&self, command: &GetUsers) -> Result, Error> { + fail_if_not_authenticated(self).await?; + let response = self + .send_with_response(GET_USERS_CODE, command.as_bytes()) + .await?; + mapper::map_users(response) + } + + async fn create_user(&self, command: &CreateUser) -> Result<(), Error> { + fail_if_not_authenticated(self).await?; + self.send_with_response(CREATE_USER_CODE, command.as_bytes()) + .await?; + Ok(()) + } + + async fn delete_user(&self, command: &DeleteUser) -> Result<(), Error> { + fail_if_not_authenticated(self).await?; + self.send_with_response(DELETE_USER_CODE, command.as_bytes()) + .await?; + Ok(()) + } + + async fn update_user(&self, command: &UpdateUser) -> Result<(), Error> { + fail_if_not_authenticated(self).await?; + self.send_with_response(UPDATE_USER_CODE, command.as_bytes()) + .await?; + Ok(()) + } + + async fn update_permissions(&self, command: &UpdatePermissions) -> Result<(), Error> { + fail_if_not_authenticated(self).await?; + self.send_with_response(UPDATE_PERMISSIONS_CODE, command.as_bytes()) + .await?; + Ok(()) + } + + async fn change_password(&self, command: &ChangePassword) -> Result<(), Error> { + fail_if_not_authenticated(self).await?; + self.send_with_response(CHANGE_PASSWORD_CODE, command.as_bytes()) + .await?; + Ok(()) + } + + async fn login_user(&self, command: &LoginUser) -> Result { + let response = self + .send_with_response(LOGIN_USER_CODE, command.as_bytes()) + .await?; + self.set_state(ClientState::Authenticated).await; + mapper::map_identity_info(response) + } + + async fn logout_user(&self, command: &LogoutUser) -> Result<(), Error> { + fail_if_not_authenticated(self).await?; + self.send_with_response(LOGOUT_USER_CODE, command.as_bytes()) + .await?; + self.set_state(ClientState::Connected).await; + Ok(()) + } +} diff --git a/src/models/bytes_serializable.rs b/src/models/bytes_serializable.rs new file mode 100644 index 0000000..60660f2 --- /dev/null +++ b/src/models/bytes_serializable.rs @@ -0,0 +1,13 @@ +use crate::infrastructure::error::Error; +use bytes::Bytes; + +/// The trait represents the logic responsible for serializing and deserializing the struct to and from bytes. +pub trait BytesSerializable { + /// Serializes the struct to bytes. + fn as_bytes(&self) -> Bytes; + + /// Deserializes the struct from bytes. + fn from_bytes(bytes: Bytes) -> Result + where + Self: Sized; +} diff --git a/src/models/client.rs b/src/models/client.rs new file mode 100644 index 0000000..db5ea3c --- /dev/null +++ b/src/models/client.rs @@ -0,0 +1,126 @@ +use crate::infrastructure::error::Error; +use crate::models::client_info::{ClientInfo, ClientInfoDetails}; +use crate::models::identity_info::IdentityInfo; +use crate::models::personal_access_token::{PersonalAccessTokenInfo, RawPersonalAccessToken}; +use crate::models::personal_access_tokens::create_personal_access_token::CreatePersonalAccessToken; +use crate::models::personal_access_tokens::delete_personal_access_token::DeletePersonalAccessToken; +use crate::models::personal_access_tokens::get_personal_access_tokens::GetPersonalAccessTokens; +use crate::models::personal_access_tokens::login_with_personal_access_token::LoginWithPersonalAccessToken; +use crate::models::stats::Stats; +use crate::models::system::get_client::GetClient; +use crate::models::system::get_clients::GetClients; +use crate::models::system::get_me::GetMe; +use crate::models::system::get_stats::GetStats; +use crate::models::system::ping::Ping; +use crate::models::user_info::{UserInfo, UserInfoDetails}; +use crate::models::users::change_password::ChangePassword; +use crate::models::users::create_user::CreateUser; +use crate::models::users::delete_user::DeleteUser; +use crate::models::users::get_user::GetUser; +use crate::models::users::get_users::GetUsers; +use crate::models::users::login_user::LoginUser; +use crate::models::users::logout_user::LogoutUser; +use crate::models::users::update_permissions::UpdatePermissions; +use crate::models::users::update_user::UpdateUser; +use async_trait::async_trait; +use std::fmt::Debug; + +/// The client is the main interface to the Iggy server. +/// It consists of multiple modules, each of which is responsible for a specific set of commands. +/// Except the ping, login and get me commands, all the other commands require authentication. +#[async_trait] +pub trait Client: + SystemClient + UserClient + PersonalAccessTokenClient + Sync + Send + Debug +{ + /// Connect to the server. Depending on the selected transport and provided configuration it might also perform authentication, retry logic etc. + /// If the client is already connected, it will do nothing. + async fn connect(&self) -> Result<(), Error>; + + /// Disconnect from the server. If the client is not connected, it will do nothing. + async fn disconnect(&self) -> Result<(), Error>; +} + +/// This trait defines the methods to interact with the system module. +#[async_trait] +pub trait SystemClient { + /// Get the stats of the system such as PID, memory usage, streams count etc. + /// + /// Authentication is required, and the permission to read the server info. + async fn get_stats(&self, command: &GetStats) -> Result; + /// Get the info about the currently connected client (not to be confused with the user). + /// + /// Authentication is required. + async fn get_me(&self, command: &GetMe) -> Result; + /// Get the info about a specific client by unique ID (not to be confused with the user). + /// + /// Authentication is required, and the permission to read the server info. + async fn get_client(&self, command: &GetClient) -> Result; + /// Get the info about all the currently connected clients (not to be confused with the users). + /// + /// Authentication is required, and the permission to read the server info. + async fn get_clients(&self, command: &GetClients) -> Result, Error>; + /// Ping the server to check if it's alive. + async fn ping(&self, command: &Ping) -> Result<(), Error>; +} + +/// This trait defines the methods to interact with the user module. +#[async_trait] +pub trait UserClient { + /// Get the info about a specific user by unique ID or username. + /// + /// Authentication is required, and the permission to read the users, unless the provided user ID is the same as the authenticated user. + async fn get_user(&self, command: &GetUser) -> Result; + /// Get the info about all the users. + /// + /// Authentication is required, and the permission to read the users. + async fn get_users(&self, command: &GetUsers) -> Result, Error>; + /// Create a new user. + /// + /// Authentication is required, and the permission to manage the users. + async fn create_user(&self, command: &CreateUser) -> Result<(), Error>; + /// Delete a user by unique ID or username. + /// + /// Authentication is required, and the permission to manage the users. + async fn delete_user(&self, command: &DeleteUser) -> Result<(), Error>; + /// Update a user by unique ID or username. + /// + /// Authentication is required, and the permission to manage the users. + async fn update_user(&self, command: &UpdateUser) -> Result<(), Error>; + /// Update the permissions of a user by unique ID or username. + /// + /// Authentication is required, and the permission to manage the users. + async fn update_permissions(&self, command: &UpdatePermissions) -> Result<(), Error>; + /// Change the password of a user by unique ID or username. + /// + /// Authentication is required, and the permission to manage the users, unless the provided user ID is the same as the authenticated user. + async fn change_password(&self, command: &ChangePassword) -> Result<(), Error>; + /// Login a user by username and password. + async fn login_user(&self, command: &LoginUser) -> Result; + /// Logout the currently authenticated user. + async fn logout_user(&self, command: &LogoutUser) -> Result<(), Error>; +} + +/// This trait defines the methods to interact with the personal access token module. +#[async_trait] +pub trait PersonalAccessTokenClient { + /// Get the info about all the personal access tokens of the currently authenticated user. + async fn get_personal_access_tokens( + &self, + command: &GetPersonalAccessTokens, + ) -> Result, Error>; + /// Create a new personal access token for the currently authenticated user. + async fn create_personal_access_token( + &self, + command: &CreatePersonalAccessToken, + ) -> Result; + /// Delete a personal access token of the currently authenticated user by unique token name. + async fn delete_personal_access_token( + &self, + command: &DeletePersonalAccessToken, + ) -> Result<(), Error>; + /// Login the user with the provided personal access token. + async fn login_with_personal_access_token( + &self, + command: &LoginWithPersonalAccessToken, + ) -> Result; +} diff --git a/src/models/client_info.rs b/src/models/client_info.rs new file mode 100644 index 0000000..86fa812 --- /dev/null +++ b/src/models/client_info.rs @@ -0,0 +1,61 @@ +use serde::{Deserialize, Serialize}; + +/// `ClientInfo` represents the information about a client. +/// It consists of the following fields: +/// - `client_id`: the unique identifier of the client. +/// - `user_id`: the unique identifier of the user. This field is optional, as the client might be connected but not authenticated yet. +/// - `address`: the remote address of the client. +/// - `transport`: the transport protocol used by the client. +/// - `consumer_groups_count`: the number of consumer groups the client is part of. +#[derive(Debug, Serialize, Deserialize)] +pub struct ClientInfo { + /// The unique identifier of the client. + pub client_id: u32, + /// The unique identifier of the user. This field is optional, as the client might be connected but not authenticated yet. + pub user_id: Option, + /// The remote address of the client. + pub address: String, + /// The transport protocol used by the client. + pub transport: String, + /// The number of consumer groups the client is part of. + pub consumer_groups_count: u32, +} + +/// `ClientInfoDetails` represents the detailed information about a client. +/// It consists of the following fields: +/// - `client_id`: the unique identifier of the client. +/// - `user_id`: the unique identifier of the user. This field is optional, as the client might be connected but not authenticated yet. +/// - `address`: the remote address of the client. +/// - `transport`: the transport protocol used by the client. +/// - `consumer_groups_count`: the number of consumer groups the client is part of. +/// - `consumer_groups`: the collection of consumer groups the client is part of. +#[derive(Debug, Serialize, Deserialize)] +pub struct ClientInfoDetails { + /// The unique identifier of the client. + pub client_id: u32, + /// The unique identifier of the user. This field is optional, as the client might be connected but not authenticated yet. + pub user_id: Option, + // The remote address of the client. + pub address: String, + /// The transport protocol used by the client. + pub transport: String, + // /// The number of consumer groups the client is part of. + // pub consumer_groups_count: u32, + // /// The collection of consumer groups the client is part of. + // pub consumer_groups: Vec, +} + +/// `ConsumerGroupInfo` represents the information about a consumer group. +/// It consists of the following fields: +/// - `stream_id`: the unique identifier (numeric) of the stream. +/// - `topic_id`: the unique identifier (numeric) of the topic. +/// - `consumer_group_id`: the unique identifier (numeric) of the consumer group. +#[derive(Debug, Serialize, Deserialize)] +pub struct ConsumerGroupInfo { + /// The unique identifier (numeric) of the stream. + pub stream_id: u32, + /// The unique identifier (numeric) of the topic. + pub topic_id: u32, + /// The unique identifier (numeric) of the consumer group. + pub consumer_group_id: u32, +} diff --git a/src/models/command.rs b/src/models/command.rs new file mode 100644 index 0000000..f5b19d2 --- /dev/null +++ b/src/models/command.rs @@ -0,0 +1,708 @@ +use crate::infrastructure::error::Error; +// use crate::models::bytes_serializable::BytesSerializable; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::personal_access_tokens::create_personal_access_token::CreatePersonalAccessToken; +use crate::models::personal_access_tokens::delete_personal_access_token::DeletePersonalAccessToken; +use crate::models::personal_access_tokens::get_personal_access_tokens::GetPersonalAccessTokens; +use crate::models::personal_access_tokens::login_with_personal_access_token::LoginWithPersonalAccessToken; +use crate::models::system::get_client::GetClient; +use crate::models::system::get_clients::GetClients; +use crate::models::system::get_me::GetMe; +use crate::models::system::get_stats::GetStats; +use crate::models::system::ping::Ping; +use crate::models::users::change_password::ChangePassword; +use crate::models::users::create_user::CreateUser; +use crate::models::users::delete_user::DeleteUser; +use crate::models::users::get_user::GetUser; +use crate::models::users::get_users::GetUsers; +use crate::models::users::login_user::LoginUser; +use crate::models::users::logout_user::LogoutUser; +use crate::models::users::update_permissions::UpdatePermissions; +use crate::models::users::update_user::UpdateUser; +use bytes::{BufMut, Bytes, BytesMut}; +use std::fmt::{Display, Formatter}; +// use std::str::FromStr; + +pub const PING: &str = "ping"; +pub const PING_CODE: u32 = 1; +pub const GET_STATS: &str = "stats"; +pub const GET_STATS_CODE: u32 = 10; +pub const GET_ME: &str = "me"; +pub const GET_ME_CODE: u32 = 20; +pub const GET_CLIENT: &str = "client.get"; +pub const GET_CLIENT_CODE: u32 = 21; +pub const GET_CLIENTS: &str = "client.list"; +pub const GET_CLIENTS_CODE: u32 = 22; +pub const GET_USER: &str = "user.get"; +pub const GET_USER_CODE: u32 = 31; +pub const GET_USERS: &str = "user.list"; +pub const GET_USERS_CODE: u32 = 32; +pub const CREATE_USER: &str = "user.create"; +pub const CREATE_USER_CODE: u32 = 33; +pub const DELETE_USER: &str = "user.delete"; +pub const DELETE_USER_CODE: u32 = 34; +pub const UPDATE_USER: &str = "user.update"; +pub const UPDATE_USER_CODE: u32 = 35; +pub const UPDATE_PERMISSIONS: &str = "user.permissions"; +pub const UPDATE_PERMISSIONS_CODE: u32 = 36; +pub const CHANGE_PASSWORD: &str = "user.password"; +pub const CHANGE_PASSWORD_CODE: u32 = 37; +pub const LOGIN_USER: &str = "user.login"; +pub const LOGIN_USER_CODE: u32 = 38; +pub const LOGOUT_USER: &str = "user.logout"; +pub const LOGOUT_USER_CODE: u32 = 39; +pub const GET_PERSONAL_ACCESS_TOKENS: &str = "personal_access_token.list"; +pub const GET_PERSONAL_ACCESS_TOKENS_CODE: u32 = 41; +pub const CREATE_PERSONAL_ACCESS_TOKEN: &str = "personal_access_token.create"; +pub const CREATE_PERSONAL_ACCESS_TOKEN_CODE: u32 = 42; +pub const DELETE_PERSONAL_ACCESS_TOKEN: &str = "personal_access_token.delete"; +pub const DELETE_PERSONAL_ACCESS_TOKEN_CODE: u32 = 43; +pub const LOGIN_WITH_PERSONAL_ACCESS_TOKEN: &str = "personal_access_token.login"; +pub const LOGIN_WITH_PERSONAL_ACCESS_TOKEN_CODE: u32 = 44; + +#[derive(Debug, PartialEq)] +pub enum Command { + Ping(Ping), + GetStats(GetStats), + GetMe(GetMe), + GetClient(GetClient), + GetClients(GetClients), + GetUser(GetUser), + GetUsers(GetUsers), + CreateUser(CreateUser), + DeleteUser(DeleteUser), + UpdateUser(UpdateUser), + UpdatePermissions(UpdatePermissions), + ChangePassword(ChangePassword), + LoginUser(LoginUser), + LogoutUser(LogoutUser), + GetPersonalAccessTokens(GetPersonalAccessTokens), + CreatePersonalAccessToken(CreatePersonalAccessToken), + DeletePersonalAccessToken(DeletePersonalAccessToken), + LoginWithPersonalAccessToken(LoginWithPersonalAccessToken), +} + +/// A trait for all command payloads. +pub trait CommandPayload: BytesSerializable + Display {} + +impl BytesSerializable for Command { + fn as_bytes(&self) -> Bytes { + match self { + Command::Ping(payload) => as_bytes(PING_CODE, payload.as_bytes()), + Command::GetStats(payload) => as_bytes(GET_STATS_CODE, payload.as_bytes()), + Command::GetMe(payload) => as_bytes(GET_ME_CODE, payload.as_bytes()), + Command::GetClient(payload) => as_bytes(GET_CLIENT_CODE, payload.as_bytes()), + Command::GetClients(payload) => as_bytes(GET_CLIENTS_CODE, payload.as_bytes()), + Command::GetUser(payload) => as_bytes(GET_USER_CODE, payload.as_bytes()), + Command::GetUsers(payload) => as_bytes(GET_USERS_CODE, payload.as_bytes()), + Command::CreateUser(payload) => as_bytes(CREATE_USER_CODE, payload.as_bytes()), + Command::DeleteUser(payload) => as_bytes(DELETE_USER_CODE, payload.as_bytes()), + Command::UpdateUser(payload) => as_bytes(UPDATE_USER_CODE, payload.as_bytes()), + Command::UpdatePermissions(payload) => { + as_bytes(UPDATE_PERMISSIONS_CODE, payload.as_bytes()) + } + Command::ChangePassword(payload) => as_bytes(CHANGE_PASSWORD_CODE, payload.as_bytes()), + Command::LoginUser(payload) => as_bytes(LOGIN_USER_CODE, payload.as_bytes()), + Command::LogoutUser(payload) => as_bytes(LOGOUT_USER_CODE, payload.as_bytes()), + Command::GetPersonalAccessTokens(payload) => { + as_bytes(GET_PERSONAL_ACCESS_TOKENS_CODE, payload.as_bytes()) + } + Command::CreatePersonalAccessToken(payload) => { + as_bytes(CREATE_PERSONAL_ACCESS_TOKEN_CODE, payload.as_bytes()) + } + Command::DeletePersonalAccessToken(payload) => { + as_bytes(DELETE_PERSONAL_ACCESS_TOKEN_CODE, payload.as_bytes()) + } + Command::LoginWithPersonalAccessToken(payload) => { + as_bytes(LOGIN_WITH_PERSONAL_ACCESS_TOKEN_CODE, payload.as_bytes()) + } + } + } + + fn from_bytes(bytes: Bytes) -> Result { + tracing::warn!("Here7a"); + + let bytes_array = bytes.as_ref(); + if bytes_array.len() < 4 { + return Err(Error::InvalidCommand); + } + + let command = u32::from_le_bytes(bytes[..4].try_into().map_err(|_| Error::InvalidCommand)?); + tracing::warn!("Here7b"); + let payload = bytes.slice(4..); + + tracing::warn!("Here7c"); + match command { + PING_CODE => { + tracing::warn!("Here7c1"); + Ok(Command::Ping(Ping::from_bytes(payload)?)) + } + GET_STATS_CODE => Ok(Command::GetStats(GetStats::from_bytes(payload)?)), + GET_ME_CODE => Ok(Command::GetMe(GetMe::from_bytes(payload)?)), + GET_CLIENT_CODE => Ok(Command::GetClient(GetClient::from_bytes(payload)?)), + GET_CLIENTS_CODE => Ok(Command::GetClients(GetClients::from_bytes(payload)?)), + GET_USER_CODE => Ok(Command::GetUser(GetUser::from_bytes(payload)?)), + GET_USERS_CODE => Ok(Command::GetUsers(GetUsers::from_bytes(payload)?)), + CREATE_USER_CODE => Ok(Command::CreateUser(CreateUser::from_bytes(payload)?)), + DELETE_USER_CODE => Ok(Command::DeleteUser(DeleteUser::from_bytes(payload)?)), + UPDATE_USER_CODE => Ok(Command::UpdateUser(UpdateUser::from_bytes(payload)?)), + UPDATE_PERMISSIONS_CODE => Ok(Command::UpdatePermissions( + UpdatePermissions::from_bytes(payload)?, + )), + CHANGE_PASSWORD_CODE => Ok(Command::ChangePassword(ChangePassword::from_bytes( + payload, + )?)), + LOGIN_USER_CODE => Ok(Command::LoginUser(LoginUser::from_bytes(payload)?)), + LOGOUT_USER_CODE => Ok(Command::LogoutUser(LogoutUser::from_bytes(payload)?)), + GET_PERSONAL_ACCESS_TOKENS_CODE => Ok(Command::GetPersonalAccessTokens( + GetPersonalAccessTokens::from_bytes(payload)?, + )), + CREATE_PERSONAL_ACCESS_TOKEN_CODE => Ok(Command::CreatePersonalAccessToken( + CreatePersonalAccessToken::from_bytes(payload)?, + )), + DELETE_PERSONAL_ACCESS_TOKEN_CODE => Ok(Command::DeletePersonalAccessToken( + DeletePersonalAccessToken::from_bytes(payload)?, + )), + LOGIN_WITH_PERSONAL_ACCESS_TOKEN_CODE => Ok(Command::LoginWithPersonalAccessToken( + LoginWithPersonalAccessToken::from_bytes(payload)?, + )), + _ => Err(Error::InvalidCommand), + } + } +} + +fn as_bytes(command: u32, payload: Bytes) -> Bytes { + let mut bytes = BytesMut::with_capacity(4 + payload.len()); + bytes.put_u32_le(command); + bytes.put_slice(&payload); + bytes.freeze() +} + +// impl FromStr for Command { +// type Err = Error; +// fn from_str(input: &str) -> Result { +// let (command, payload) = input.split_once('|').unwrap_or((input, "")); +// match command { +// PING => Ok(Command::Ping(Ping::from_str(payload)?)), +// GET_STATS => Ok(Command::GetStats(GetStats::from_str(payload)?)), +// GET_ME => Ok(Command::GetMe(GetMe::from_str(payload)?)), +// GET_CLIENT => Ok(Command::GetClient(GetClient::from_str(payload)?)), +// GET_CLIENTS => Ok(Command::GetClients(GetClients::from_str(payload)?)), +// GET_USER => Ok(Command::GetUser(GetUser::from_str(payload)?)), +// GET_USERS => Ok(Command::GetUsers(GetUsers::from_str(payload)?)), +// CREATE_USER => Ok(Command::CreateUser(CreateUser::from_str(payload)?)), +// DELETE_USER => Ok(Command::DeleteUser(DeleteUser::from_str(payload)?)), +// UPDATE_USER => Ok(Command::UpdateUser(UpdateUser::from_str(payload)?)), +// UPDATE_PERMISSIONS => Ok(Command::UpdatePermissions(UpdatePermissions::from_str( +// payload, +// )?)), +// CHANGE_PASSWORD => Ok(Command::ChangePassword(ChangePassword::from_str(payload)?)), +// LOGIN_USER => Ok(Command::LoginUser(LoginUser::from_str(payload)?)), +// LOGOUT_USER => Ok(Command::LogoutUser(LogoutUser::from_str(payload)?)), +// GET_PERSONAL_ACCESS_TOKENS => Ok(Command::GetPersonalAccessTokens( +// GetPersonalAccessTokens::from_str(payload)?, +// )), +// CREATE_PERSONAL_ACCESS_TOKEN => Ok(Command::CreatePersonalAccessToken( +// CreatePersonalAccessToken::from_str(payload)?, +// )), +// DELETE_PERSONAL_ACCESS_TOKEN => Ok(Command::DeletePersonalAccessToken( +// DeletePersonalAccessToken::from_str(payload)?, +// )), +// LOGIN_WITH_PERSONAL_ACCESS_TOKEN => Ok(Command::LoginWithPersonalAccessToken( +// LoginWithPersonalAccessToken::from_str(payload)?, +// )), + +// _ => Err(Error::InvalidCommand), +// } +// } +// } + +impl Display for Command { + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Command::Ping(_) => write!(formatter, "{PING}"), + Command::GetStats(_) => write!(formatter, "{GET_STATS}"), + Command::GetMe(_) => write!(formatter, "{GET_ME}"), + Command::GetClient(payload) => write!(formatter, "{GET_CLIENT}|{payload}"), + Command::GetClients(_) => write!(formatter, "{GET_CLIENTS}"), + Command::GetUser(payload) => write!(formatter, "{GET_USER}|{payload}"), + Command::GetUsers(_) => write!(formatter, "{GET_USERS}"), + Command::CreateUser(payload) => write!(formatter, "{CREATE_USER}|{payload}"), + Command::DeleteUser(payload) => write!(formatter, "{DELETE_USER}|{payload}"), + Command::UpdateUser(payload) => write!(formatter, "{UPDATE_USER}|{payload}"), + Command::UpdatePermissions(payload) => { + write!(formatter, "{UPDATE_PERMISSIONS}|{payload}") + } + Command::ChangePassword(payload) => { + write!(formatter, "{CHANGE_PASSWORD}|{payload}") + } + Command::LoginUser(payload) => write!(formatter, "{LOGIN_USER}|{payload}"), + Command::LogoutUser(_) => write!(formatter, "{LOGOUT_USER}"), + Command::GetPersonalAccessTokens(_) => { + write!(formatter, "{GET_PERSONAL_ACCESS_TOKENS}") + } + Command::CreatePersonalAccessToken(payload) => { + write!(formatter, "{CREATE_PERSONAL_ACCESS_TOKEN}|{payload}") + } + Command::DeletePersonalAccessToken(payload) => { + write!(formatter, "{DELETE_PERSONAL_ACCESS_TOKEN}|{payload}") + } + Command::LoginWithPersonalAccessToken(payload) => { + write!(formatter, "{LOGIN_WITH_PERSONAL_ACCESS_TOKEN}|{payload}") + } + } + } +} + +// #[cfg(test)] +// mod tests { +// use super::*; + +// #[test] +// fn should_be_serialized_as_bytes_and_deserialized_from_bytes() { +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::Ping(Ping::default()), +// PING_CODE, +// &Ping::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::GetStats(GetStats::default()), +// GET_STATS_CODE, +// &GetStats::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::GetMe(GetMe::default()), +// GET_ME_CODE, +// &GetMe::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::GetClient(GetClient::default()), +// GET_CLIENT_CODE, +// &GetClient::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::GetClients(GetClients::default()), +// GET_CLIENTS_CODE, +// &GetClients::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::GetUser(GetUser::default()), +// GET_USER_CODE, +// &GetUser::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::GetUsers(GetUsers::default()), +// GET_USERS_CODE, +// &GetUsers::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::CreateUser(CreateUser::default()), +// CREATE_USER_CODE, +// &CreateUser::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::DeleteUser(DeleteUser::default()), +// DELETE_USER_CODE, +// &DeleteUser::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::UpdateUser(UpdateUser::default()), +// UPDATE_USER_CODE, +// &UpdateUser::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::UpdatePermissions(UpdatePermissions::default()), +// UPDATE_PERMISSIONS_CODE, +// &UpdatePermissions::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::ChangePassword(ChangePassword::default()), +// CHANGE_PASSWORD_CODE, +// &ChangePassword::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::LoginUser(LoginUser::default()), +// LOGIN_USER_CODE, +// &LoginUser::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::LogoutUser(LogoutUser::default()), +// LOGOUT_USER_CODE, +// &LogoutUser::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::GetPersonalAccessTokens(GetPersonalAccessTokens::default()), +// GET_PERSONAL_ACCESS_TOKENS_CODE, +// &GetPersonalAccessTokens::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::CreatePersonalAccessToken(CreatePersonalAccessToken::default()), +// CREATE_PERSONAL_ACCESS_TOKEN_CODE, +// &CreatePersonalAccessToken::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::DeletePersonalAccessToken(DeletePersonalAccessToken::default()), +// DELETE_PERSONAL_ACCESS_TOKEN_CODE, +// &DeletePersonalAccessToken::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::LoginWithPersonalAccessToken(LoginWithPersonalAccessToken::default()), +// LOGIN_WITH_PERSONAL_ACCESS_TOKEN_CODE, +// &LoginWithPersonalAccessToken::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::SendMessages(SendMessages::default()), +// SEND_MESSAGES_CODE, +// &SendMessages::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::PollMessages(PollMessages::default()), +// POLL_MESSAGES_CODE, +// &PollMessages::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::StoreConsumerOffset(StoreConsumerOffset::default()), +// STORE_CONSUMER_OFFSET_CODE, +// &StoreConsumerOffset::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::GetConsumerOffset(GetConsumerOffset::default()), +// GET_CONSUMER_OFFSET_CODE, +// &GetConsumerOffset::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::GetStream(GetStream::default()), +// GET_STREAM_CODE, +// &GetStream::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::GetStreams(GetStreams::default()), +// GET_STREAMS_CODE, +// &GetStreams::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::CreateStream(CreateStream::default()), +// CREATE_STREAM_CODE, +// &CreateStream::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::DeleteStream(DeleteStream::default()), +// DELETE_STREAM_CODE, +// &DeleteStream::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::UpdateStream(UpdateStream::default()), +// UPDATE_STREAM_CODE, +// &UpdateStream::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::GetTopic(GetTopic::default()), +// GET_TOPIC_CODE, +// &GetTopic::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::GetTopics(GetTopics::default()), +// GET_TOPICS_CODE, +// &GetTopics::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::CreateTopic(CreateTopic::default()), +// CREATE_TOPIC_CODE, +// &CreateTopic::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::DeleteTopic(DeleteTopic::default()), +// DELETE_TOPIC_CODE, +// &DeleteTopic::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::UpdateTopic(UpdateTopic::default()), +// UPDATE_TOPIC_CODE, +// &UpdateTopic::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::CreatePartitions(CreatePartitions::default()), +// CREATE_PARTITIONS_CODE, +// &CreatePartitions::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::DeletePartitions(DeletePartitions::default()), +// DELETE_PARTITIONS_CODE, +// &DeletePartitions::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::GetConsumerGroup(GetConsumerGroup::default()), +// GET_CONSUMER_GROUP_CODE, +// &GetConsumerGroup::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::GetConsumerGroups(GetConsumerGroups::default()), +// GET_CONSUMER_GROUPS_CODE, +// &GetConsumerGroups::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::CreateConsumerGroup(CreateConsumerGroup::default()), +// CREATE_CONSUMER_GROUP_CODE, +// &CreateConsumerGroup::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::DeleteConsumerGroup(DeleteConsumerGroup::default()), +// DELETE_CONSUMER_GROUP_CODE, +// &DeleteConsumerGroup::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::JoinConsumerGroup(JoinConsumerGroup::default()), +// JOIN_CONSUMER_GROUP_CODE, +// &JoinConsumerGroup::default(), +// ); +// assert_serialized_as_bytes_and_deserialized_from_bytes( +// &Command::LeaveConsumerGroup(LeaveConsumerGroup::default()), +// LEAVE_CONSUMER_GROUP_CODE, +// &LeaveConsumerGroup::default(), +// ); +// } + +// #[test] +// fn should_be_read_from_string() { +// assert_read_from_string(&Command::Ping(Ping::default()), PING, &Ping::default()); +// assert_read_from_string( +// &Command::GetStats(GetStats::default()), +// GET_STATS, +// &GetStats::default(), +// ); +// assert_read_from_string(&Command::GetMe(GetMe::default()), GET_ME, &GetMe::default()); +// assert_read_from_string( +// &Command::GetClient(GetClient::default()), +// GET_CLIENT, +// &GetClient::default(), +// ); +// assert_read_from_string( +// &Command::GetClients(GetClients::default()), +// GET_CLIENTS, +// &GetClients::default(), +// ); +// assert_read_from_string( +// &Command::GetUser(GetUser::default()), +// GET_USER, +// &GetUser::default(), +// ); +// assert_read_from_string( +// &Command::GetUsers(GetUsers::default()), +// GET_USERS, +// &GetUsers::default(), +// ); +// assert_read_from_string( +// &Command::CreateUser(CreateUser::default()), +// CREATE_USER, +// &CreateUser::default(), +// ); +// assert_read_from_string( +// &Command::DeleteUser(DeleteUser::default()), +// DELETE_USER, +// &DeleteUser::default(), +// ); +// assert_read_from_string( +// &Command::UpdateUser(UpdateUser::default()), +// UPDATE_USER, +// &UpdateUser::default(), +// ); +// assert_read_from_string( +// &Command::UpdatePermissions(UpdatePermissions::default()), +// UPDATE_PERMISSIONS, +// &UpdatePermissions::default(), +// ); +// assert_read_from_string( +// &Command::ChangePassword(ChangePassword::default()), +// CHANGE_PASSWORD, +// &ChangePassword::default(), +// ); +// assert_read_from_string( +// &Command::LoginUser(LoginUser::default()), +// LOGIN_USER, +// &LoginUser::default(), +// ); +// assert_read_from_string( +// &Command::LogoutUser(LogoutUser::default()), +// LOGOUT_USER, +// &LogoutUser::default(), +// ); +// assert_read_from_string( +// &Command::GetPersonalAccessTokens(GetPersonalAccessTokens::default()), +// GET_PERSONAL_ACCESS_TOKENS, +// &GetPersonalAccessTokens::default(), +// ); +// assert_read_from_string( +// &Command::CreatePersonalAccessToken(CreatePersonalAccessToken::default()), +// CREATE_PERSONAL_ACCESS_TOKEN, +// &CreatePersonalAccessToken::default(), +// ); +// assert_read_from_string( +// &Command::DeletePersonalAccessToken(DeletePersonalAccessToken::default()), +// DELETE_PERSONAL_ACCESS_TOKEN, +// &DeletePersonalAccessToken::default(), +// ); +// assert_read_from_string( +// &Command::LoginWithPersonalAccessToken(LoginWithPersonalAccessToken::default()), +// LOGIN_WITH_PERSONAL_ACCESS_TOKEN, +// &LoginWithPersonalAccessToken::default(), +// ); +// assert_read_from_string( +// &Command::SendMessages(SendMessages::default()), +// SEND_MESSAGES, +// &SendMessages::default(), +// ); +// assert_read_from_string( +// &Command::PollMessages(PollMessages::default()), +// POLL_MESSAGES, +// &PollMessages::default(), +// ); +// assert_read_from_string( +// &Command::StoreConsumerOffset(StoreConsumerOffset::default()), +// STORE_CONSUMER_OFFSET, +// &StoreConsumerOffset::default(), +// ); +// assert_read_from_string( +// &Command::GetConsumerOffset(GetConsumerOffset::default()), +// GET_CONSUMER_OFFSET, +// &GetConsumerOffset::default(), +// ); +// assert_read_from_string( +// &Command::GetStream(GetStream::default()), +// GET_STREAM, +// &GetStream::default(), +// ); +// assert_read_from_string( +// &Command::GetStreams(GetStreams::default()), +// GET_STREAMS, +// &GetStreams::default(), +// ); +// assert_read_from_string( +// &Command::CreateStream(CreateStream::default()), +// CREATE_STREAM, +// &CreateStream::default(), +// ); +// assert_read_from_string( +// &Command::DeleteStream(DeleteStream::default()), +// DELETE_STREAM, +// &DeleteStream::default(), +// ); +// assert_read_from_string( +// &Command::UpdateStream(UpdateStream::default()), +// UPDATE_STREAM, +// &UpdateStream::default(), +// ); +// assert_read_from_string( +// &Command::GetTopic(GetTopic::default()), +// GET_TOPIC, +// &GetTopic::default(), +// ); +// assert_read_from_string( +// &Command::GetTopics(GetTopics::default()), +// GET_TOPICS, +// &GetTopics::default(), +// ); +// assert_read_from_string( +// &Command::CreateTopic(CreateTopic::default()), +// CREATE_TOPIC, +// &CreateTopic::default(), +// ); +// assert_read_from_string( +// &Command::DeleteTopic(DeleteTopic::default()), +// DELETE_TOPIC, +// &DeleteTopic::default(), +// ); +// assert_read_from_string( +// &Command::UpdateTopic(UpdateTopic::default()), +// UPDATE_TOPIC, +// &UpdateTopic::default(), +// ); +// assert_read_from_string( +// &Command::CreatePartitions(CreatePartitions::default()), +// CREATE_PARTITIONS, +// &CreatePartitions::default(), +// ); +// assert_read_from_string( +// &Command::DeletePartitions(DeletePartitions::default()), +// DELETE_PARTITIONS, +// &DeletePartitions::default(), +// ); +// assert_read_from_string( +// &Command::GetConsumerGroup(GetConsumerGroup::default()), +// GET_CONSUMER_GROUP, +// &GetConsumerGroup::default(), +// ); +// assert_read_from_string( +// &Command::GetConsumerGroups(GetConsumerGroups::default()), +// GET_CONSUMER_GROUPS, +// &GetConsumerGroups::default(), +// ); +// assert_read_from_string( +// &Command::CreateConsumerGroup(CreateConsumerGroup::default()), +// CREATE_CONSUMER_GROUP, +// &CreateConsumerGroup::default(), +// ); +// assert_read_from_string( +// &Command::DeleteConsumerGroup(DeleteConsumerGroup::default()), +// DELETE_CONSUMER_GROUP, +// &DeleteConsumerGroup::default(), +// ); +// assert_read_from_string( +// &Command::JoinConsumerGroup(JoinConsumerGroup::default()), +// JOIN_CONSUMER_GROUP, +// &JoinConsumerGroup::default(), +// ); +// assert_read_from_string( +// &Command::LeaveConsumerGroup(LeaveConsumerGroup::default()), +// LEAVE_CONSUMER_GROUP, +// &LeaveConsumerGroup::default(), +// ); +// } + +// fn assert_serialized_as_bytes_and_deserialized_from_bytes( +// command: &Command, +// command_id: u32, +// payload: &dyn CommandPayload, +// ) { +// assert_serialized_as_bytes(command, command_id, payload); +// assert_deserialized_from_bytes(command, command_id, payload); +// } + +// fn assert_serialized_as_bytes( +// command: &Command, +// command_id: u32, +// payload: &dyn CommandPayload, +// ) { +// let payload = payload.as_bytes(); +// let mut bytes = Vec::with_capacity(4 + payload.len()); +// bytes.put_u32_le(command_id); +// bytes.extend(payload); +// assert_eq!(command.as_bytes(), bytes); +// } + +// fn assert_deserialized_from_bytes( +// command: &Command, +// command_id: u32, +// payload: &dyn CommandPayload, +// ) { +// let payload = payload.as_bytes(); +// let mut bytes = Vec::with_capacity(4 + payload.len()); +// bytes.put_u32_le(command_id); +// bytes.extend(payload); +// assert_eq!(&Command::from_bytes(&bytes).unwrap(), command); +// } + +// fn assert_read_from_string( +// command: &Command, +// command_name: &str, +// payload: &dyn CommandPayload, +// ) { +// let payload = payload.to_string(); +// let mut string = String::with_capacity(command_name.len() + payload.len()); +// string.push_str(command_name); +// if !payload.is_empty() { +// string.push('|'); +// string.push_str(&payload); +// } +// assert_eq!(&Command::from_str(&string).unwrap(), command); +// } +// } diff --git a/src/models/header.rs b/src/models/header.rs new file mode 100644 index 0000000..ce83c9f --- /dev/null +++ b/src/models/header.rs @@ -0,0 +1,896 @@ +use crate::infrastructure::error::Error; +use crate::models::bytes_serializable::BytesSerializable; +use bytes::{BufMut, Bytes, BytesMut}; +use serde::{Deserialize, Serialize}; +use serde_with::base64::Base64; +use serde_with::serde_as; +use std::collections::HashMap; +use std::fmt::{Display, Formatter}; +use std::hash::{Hash, Hasher}; +use std::str::FromStr; + +/// Represents a header key with a unique name. The name is case-insensitive and wraps a string. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct HeaderKey(String); + +impl HeaderKey { + pub fn new(key: &str) -> Result { + if key.is_empty() || key.len() > 255 { + return Err(Error::InvalidHeaderKey); + } + + Ok(Self(key.to_lowercase().to_string())) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl Hash for HeaderKey { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + +impl FromStr for HeaderKey { + type Err = Error; + fn from_str(s: &str) -> Result { + Self::new(s) + } +} + +impl TryFrom<&str> for HeaderKey { + type Error = Error; + fn try_from(value: &str) -> Result { + Self::new(value) + } +} + +/// Represents a header value of a specific kind. +/// It consists of the following fields: +/// - `kind`: the kind of the header value. +/// - `value`: the value of the header. +#[serde_as] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct HeaderValue { + /// The kind of the header value. + pub kind: HeaderKind, + /// The binary value of the header payload. + #[serde_as(as = "Base64")] + pub value: Vec, +} + +/// Represents the kind of a header value. +#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum HeaderKind { + Raw, + String, + Bool, + Int8, + Int16, + Int32, + Int64, + Int128, + Uint8, + Uint16, + Uint32, + Uint64, + Uint128, + Float32, + Float64, +} + +impl HeaderKind { + /// Returns the code of the header kind. + pub fn as_code(&self) -> u8 { + match self { + HeaderKind::Raw => 1, + HeaderKind::String => 2, + HeaderKind::Bool => 3, + HeaderKind::Int8 => 4, + HeaderKind::Int16 => 5, + HeaderKind::Int32 => 6, + HeaderKind::Int64 => 7, + HeaderKind::Int128 => 8, + HeaderKind::Uint8 => 9, + HeaderKind::Uint16 => 10, + HeaderKind::Uint32 => 11, + HeaderKind::Uint64 => 12, + HeaderKind::Uint128 => 13, + HeaderKind::Float32 => 14, + HeaderKind::Float64 => 15, + } + } + + /// Returns the header kind from the code. + pub fn from_code(code: u8) -> Result { + match code { + 1 => Ok(HeaderKind::Raw), + 2 => Ok(HeaderKind::String), + 3 => Ok(HeaderKind::Bool), + 4 => Ok(HeaderKind::Int8), + 5 => Ok(HeaderKind::Int16), + 6 => Ok(HeaderKind::Int32), + 7 => Ok(HeaderKind::Int64), + 8 => Ok(HeaderKind::Int128), + 9 => Ok(HeaderKind::Uint8), + 10 => Ok(HeaderKind::Uint16), + 11 => Ok(HeaderKind::Uint32), + 12 => Ok(HeaderKind::Uint64), + 13 => Ok(HeaderKind::Uint128), + 14 => Ok(HeaderKind::Float32), + 15 => Ok(HeaderKind::Float64), + _ => Err(Error::InvalidCommand), + } + } +} + +impl FromStr for HeaderKind { + type Err = Error; + fn from_str(s: &str) -> Result { + match s { + "raw" => Ok(HeaderKind::Raw), + "string" => Ok(HeaderKind::String), + "bool" => Ok(HeaderKind::Bool), + "int8" => Ok(HeaderKind::Int8), + "int16" => Ok(HeaderKind::Int16), + "int32" => Ok(HeaderKind::Int32), + "int64" => Ok(HeaderKind::Int64), + "int128" => Ok(HeaderKind::Int128), + "uint8" => Ok(HeaderKind::Uint8), + "uint16" => Ok(HeaderKind::Uint16), + "uint32" => Ok(HeaderKind::Uint32), + "uint64" => Ok(HeaderKind::Uint64), + "uint128" => Ok(HeaderKind::Uint128), + "float32" => Ok(HeaderKind::Float32), + "float64" => Ok(HeaderKind::Float64), + _ => Err(Error::InvalidCommand), + } + } +} + +impl Display for HeaderValue { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: ", self.kind)?; + match self.kind { + HeaderKind::Raw => write!(f, "{:?}", self.value), + HeaderKind::String => write!(f, "{}", String::from_utf8_lossy(&self.value)), + HeaderKind::Bool => write!(f, "{}", self.value[0] != 0), + HeaderKind::Int8 => write!( + f, + "{}", + i8::from_le_bytes(self.value.clone().try_into().unwrap()) + ), + HeaderKind::Int16 => write!( + f, + "{}", + i16::from_le_bytes(self.value.clone().try_into().unwrap()) + ), + HeaderKind::Int32 => write!( + f, + "{}", + i32::from_le_bytes(self.value.clone().try_into().unwrap()) + ), + HeaderKind::Int64 => write!( + f, + "{}", + i64::from_le_bytes(self.value.clone().try_into().unwrap()) + ), + HeaderKind::Int128 => write!( + f, + "{}", + i128::from_le_bytes(self.value.clone().try_into().unwrap()) + ), + HeaderKind::Uint8 => write!( + f, + "{}", + u8::from_le_bytes(self.value.clone().try_into().unwrap()) + ), + HeaderKind::Uint16 => write!( + f, + "{}", + u16::from_le_bytes(self.value.clone().try_into().unwrap()) + ), + HeaderKind::Uint32 => write!( + f, + "{}", + u32::from_le_bytes(self.value.clone().try_into().unwrap()) + ), + HeaderKind::Uint64 => write!( + f, + "{}", + u64::from_le_bytes(self.value.clone().try_into().unwrap()) + ), + HeaderKind::Uint128 => write!( + f, + "{}", + u128::from_le_bytes(self.value.clone().try_into().unwrap()) + ), + HeaderKind::Float32 => write!( + f, + "{}", + f32::from_le_bytes(self.value.clone().try_into().unwrap()) + ), + HeaderKind::Float64 => write!( + f, + "{}", + f64::from_le_bytes(self.value.clone().try_into().unwrap()) + ), + } + } +} + +impl Display for HeaderKind { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + match *self { + HeaderKind::Raw => write!(f, "raw"), + HeaderKind::String => write!(f, "string"), + HeaderKind::Bool => write!(f, "bool"), + HeaderKind::Int8 => write!(f, "int8"), + HeaderKind::Int16 => write!(f, "int16"), + HeaderKind::Int32 => write!(f, "int32"), + HeaderKind::Int64 => write!(f, "int64"), + HeaderKind::Int128 => write!(f, "int128"), + HeaderKind::Uint8 => write!(f, "uint8"), + HeaderKind::Uint16 => write!(f, "uint16"), + HeaderKind::Uint32 => write!(f, "uint32"), + HeaderKind::Uint64 => write!(f, "uint64"), + HeaderKind::Uint128 => write!(f, "uint128"), + HeaderKind::Float32 => write!(f, "float32"), + HeaderKind::Float64 => write!(f, "float64"), + } + } +} + +impl FromStr for HeaderValue { + type Err = Error; + fn from_str(s: &str) -> Result { + Self::from(HeaderKind::String, s.as_bytes()) + } +} + +impl HeaderValue { + /// Creates a new header value from the specified raw bytes. + pub fn from_raw(value: &[u8]) -> Result { + Self::from(HeaderKind::Raw, value) + } + + /// Returns the raw bytes of the header value. + pub fn as_raw(&self) -> Result<&[u8], Error> { + if self.kind != HeaderKind::Raw { + return Err(Error::InvalidHeaderValue); + } + + Ok(&self.value) + } + + /// Returns the string representation of the header value. + pub fn as_str(&self) -> Result<&str, Error> { + if self.kind != HeaderKind::String { + return Err(Error::InvalidHeaderValue); + } + + Ok(std::str::from_utf8(&self.value)?) + } + + /// Creates a new header value from the specified string. + pub fn from_bool(value: bool) -> Result { + Self::from(HeaderKind::Bool, if value { &[1] } else { &[0] }) + } + + /// Returns the boolean representation of the header value. + pub fn as_bool(&self) -> Result { + if self.kind != HeaderKind::Bool { + return Err(Error::InvalidHeaderValue); + } + + match self.value[0] { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(Error::InvalidHeaderValue), + } + } + + /// Creates a new header value from the specified boolean. + pub fn from_int8(value: i8) -> Result { + Self::from(HeaderKind::Int8, &value.to_le_bytes()) + } + + /// Returns the i8 representation of the header value. + pub fn as_int8(&self) -> Result { + if self.kind != HeaderKind::Int8 { + return Err(Error::InvalidHeaderValue); + } + + let value = self.value.clone().try_into(); + if value.is_err() { + return Err(Error::InvalidHeaderValue); + } + + Ok(i8::from_le_bytes(value.unwrap())) + } + + /// Creates a new header value from the specified i8. + pub fn from_int16(value: i16) -> Result { + Self::from(HeaderKind::Int16, &value.to_le_bytes()) + } + + /// Returns the i16 representation of the header value. + pub fn as_int16(&self) -> Result { + if self.kind != HeaderKind::Int16 { + return Err(Error::InvalidHeaderValue); + } + + let value = self.value.clone().try_into(); + if value.is_err() { + return Err(Error::InvalidHeaderValue); + } + + Ok(i16::from_le_bytes(value.unwrap())) + } + + /// Creates a new header value from the specified i16. + pub fn from_int32(value: i32) -> Result { + Self::from(HeaderKind::Int32, &value.to_le_bytes()) + } + + /// Returns the i32 representation of the header value. + pub fn as_int32(&self) -> Result { + if self.kind != HeaderKind::Int32 { + return Err(Error::InvalidHeaderValue); + } + + let value = self.value.clone().try_into(); + if value.is_err() { + return Err(Error::InvalidHeaderValue); + } + + Ok(i32::from_le_bytes(value.unwrap())) + } + + /// Creates a new header value from the specified i32. + pub fn from_int64(value: i64) -> Result { + Self::from(HeaderKind::Int64, &value.to_le_bytes()) + } + + /// Returns the i64 representation of the header value. + pub fn as_int64(&self) -> Result { + if self.kind != HeaderKind::Int64 { + return Err(Error::InvalidHeaderValue); + } + + let value = self.value.clone().try_into(); + if value.is_err() { + return Err(Error::InvalidHeaderValue); + } + + Ok(i64::from_le_bytes(value.unwrap())) + } + + /// Creates a new header value from the specified i128. + pub fn from_int128(value: i128) -> Result { + Self::from(HeaderKind::Int128, &value.to_le_bytes()) + } + + /// Returns the i128 representation of the header value. + pub fn as_int128(&self) -> Result { + if self.kind != HeaderKind::Int128 { + return Err(Error::InvalidHeaderValue); + } + + let value = self.value.clone().try_into(); + if value.is_err() { + return Err(Error::InvalidHeaderValue); + } + + Ok(i128::from_le_bytes(value.unwrap())) + } + + /// Creates a new header value from the specified u8. + pub fn from_uint8(value: u8) -> Result { + Self::from(HeaderKind::Uint8, &value.to_le_bytes()) + } + + /// Returns the u8 representation of the header value. + pub fn as_uint8(&self) -> Result { + if self.kind != HeaderKind::Uint8 { + return Err(Error::InvalidHeaderValue); + } + + Ok(self.value[0]) + } + + /// Creates a new header value from the specified u16. + pub fn from_uint16(value: u16) -> Result { + Self::from(HeaderKind::Uint16, &value.to_le_bytes()) + } + + /// Returns the u16 representation of the header value. + pub fn as_uint16(&self) -> Result { + if self.kind != HeaderKind::Uint16 { + return Err(Error::InvalidHeaderValue); + } + + let value = self.value.clone().try_into(); + if value.is_err() { + return Err(Error::InvalidHeaderValue); + } + + Ok(u16::from_le_bytes(value.unwrap())) + } + + /// Creates a new header value from the specified u32. + pub fn from_uint32(value: u32) -> Result { + Self::from(HeaderKind::Uint32, &value.to_le_bytes()) + } + + /// Returns the u32 representation of the header value. + pub fn as_uint32(&self) -> Result { + if self.kind != HeaderKind::Uint32 { + return Err(Error::InvalidHeaderValue); + } + + let value = self.value.clone().try_into(); + if value.is_err() { + return Err(Error::InvalidHeaderValue); + } + + Ok(u32::from_le_bytes(value.unwrap())) + } + + /// Creates a new header value from the specified u64. + pub fn from_uint64(value: u64) -> Result { + Self::from(HeaderKind::Uint64, &value.to_le_bytes()) + } + + /// Returns the u64 representation of the header value. + pub fn as_uint64(&self) -> Result { + if self.kind != HeaderKind::Uint64 { + return Err(Error::InvalidHeaderValue); + } + + let value = self.value.clone().try_into(); + if value.is_err() { + return Err(Error::InvalidHeaderValue); + } + + Ok(u64::from_le_bytes(value.unwrap())) + } + + /// Creates a new header value from the specified u128. + pub fn from_uint128(value: u128) -> Result { + Self::from(HeaderKind::Uint128, &value.to_le_bytes()) + } + + /// Returns the u128 representation of the header value. + pub fn as_uint128(&self) -> Result { + if self.kind != HeaderKind::Uint128 { + return Err(Error::InvalidHeaderValue); + } + + let value = self.value.clone().try_into(); + if value.is_err() { + return Err(Error::InvalidHeaderValue); + } + + Ok(u128::from_le_bytes(value.unwrap())) + } + + /// Creates a new header value from the specified f32. + pub fn from_float32(value: f32) -> Result { + Self::from(HeaderKind::Float32, &value.to_le_bytes()) + } + + /// Returns the f32 representation of the header value. + pub fn as_float32(&self) -> Result { + if self.kind != HeaderKind::Float32 { + return Err(Error::InvalidHeaderValue); + } + + let value = self.value.clone().try_into(); + if value.is_err() { + return Err(Error::InvalidHeaderValue); + } + + Ok(f32::from_le_bytes(value.unwrap())) + } + + /// Creates a new header value from the specified f64. + pub fn from_float64(value: f64) -> Result { + Self::from(HeaderKind::Float64, &value.to_le_bytes()) + } + + /// Returns the f64 representation of the header value. + pub fn as_float64(&self) -> Result { + if self.kind != HeaderKind::Float64 { + return Err(Error::InvalidHeaderValue); + } + + let value = self.value.clone().try_into(); + if value.is_err() { + return Err(Error::InvalidHeaderValue); + } + + Ok(f64::from_le_bytes(value.unwrap())) + } + + /// Creates a new header value from the specified kind and value. + fn from(kind: HeaderKind, value: &[u8]) -> Result { + if value.is_empty() || value.len() > 255 { + return Err(Error::InvalidHeaderValue); + } + + Ok(Self { + kind, + value: value.to_vec(), + }) + } +} + +impl BytesSerializable for HashMap { + fn as_bytes(&self) -> Bytes { + if self.is_empty() { + return Bytes::new(); + } + + let mut bytes = BytesMut::new(); + for (key, value) in self { + #[allow(clippy::cast_possible_truncation)] + bytes.put_u32_le(key.0.len() as u32); + bytes.put_slice(key.0.as_bytes()); + bytes.put_u8(value.kind.as_code()); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u32_le(value.value.len() as u32); + bytes.put_slice(&value.value); + } + + bytes.freeze() + } + + fn from_bytes(bytes: Bytes) -> Result + where + Self: Sized, + { + if bytes.is_empty() { + return Ok(Self::new()); + } + + let mut headers = Self::new(); + let mut position = 0; + while position < bytes.len() { + let key_length = u32::from_le_bytes(bytes[position..position + 4].try_into()?) as usize; + if key_length == 0 || key_length > 255 { + return Err(Error::InvalidHeaderKey); + } + position += 4; + let key = String::from_utf8(bytes[position..position + key_length].to_vec()); + if key.is_err() { + return Err(Error::InvalidHeaderKey); + } + let key = key.unwrap(); + position += key_length; + let kind = HeaderKind::from_code(bytes[position])?; + position += 1; + let value_length = + u32::from_le_bytes(bytes[position..position + 4].try_into()?) as usize; + if value_length == 0 || value_length > 255 { + return Err(Error::InvalidHeaderValue); + } + position += 4; + let value = bytes[position..position + value_length].to_vec(); + position += value_length; + headers.insert(HeaderKey(key), HeaderValue { kind, value }); + } + + Ok(headers) + } +} + +/// Returns the size in bytes of the specified headers. +pub fn get_headers_size_bytes(headers: &Option>) -> u32 { + // Headers length field + let mut size = 4; + if let Some(headers) = headers { + for (key, value) in headers { + // Key length + Key + Kind + Value length + Value + size += 4 + key.as_str().len() as u32 + 1 + 4 + value.value.len() as u32; + } + } + size +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn header_key_should_be_created_for_valid_value() { + let value = "key-1"; + let header_key = HeaderKey::new(value); + assert!(header_key.is_ok()); + assert_eq!(header_key.unwrap().0, value); + } + + #[test] + fn header_key_should_not_be_created_for_empty_value() { + let value = ""; + let header_key = HeaderKey::new(value); + assert!(header_key.is_err()); + let error = header_key.unwrap_err(); + assert_eq!(error.as_code(), Error::InvalidHeaderKey.as_code()); + } + + #[test] + fn header_key_should_not_be_created_for_too_long_value() { + let value = "a".repeat(256); + let header_key = HeaderKey::new(&value); + assert!(header_key.is_err()); + let error = header_key.unwrap_err(); + assert_eq!(error.as_code(), Error::InvalidHeaderKey.as_code()); + } + + #[test] + fn header_value_should_not_be_created_for_empty_value() { + let header_value = HeaderValue::from(HeaderKind::Raw, &[]); + assert!(header_value.is_err()); + let error = header_value.unwrap_err(); + assert_eq!(error.as_code(), Error::InvalidHeaderValue.as_code()); + } + + #[test] + fn header_value_should_not_be_created_for_too_long_value() { + let value = b"a".repeat(256); + let header_value = HeaderValue::from(HeaderKind::Raw, &value); + assert!(header_value.is_err()); + let error = header_value.unwrap_err(); + assert_eq!(error.as_code(), Error::InvalidHeaderValue.as_code()); + } + + #[test] + fn header_value_should_be_created_from_raw_bytes() { + let value = b"Value 1"; + let header_value = HeaderValue::from_raw(value); + assert!(header_value.is_ok()); + assert_eq!(header_value.unwrap().value, value); + } + + #[test] + fn header_value_should_be_created_from_str() { + let value = "Value 1"; + let header_value = HeaderValue::from_str(value); + assert!(header_value.is_ok()); + let header_value = header_value.unwrap(); + assert_eq!(header_value.kind, HeaderKind::String); + assert_eq!(header_value.value, value.as_bytes()); + assert_eq!(header_value.as_str().unwrap(), value); + } + + #[test] + fn header_value_should_be_created_from_bool() { + let value = true; + let header_value = HeaderValue::from_bool(value); + assert!(header_value.is_ok()); + let header_value = header_value.unwrap(); + assert_eq!(header_value.kind, HeaderKind::Bool); + assert_eq!(header_value.value, if value { [1] } else { [0] }); + assert_eq!(header_value.as_bool().unwrap(), value); + } + + #[test] + fn header_value_should_be_created_from_int8() { + let value = 123; + let header_value = HeaderValue::from_int8(value); + assert!(header_value.is_ok()); + let header_value = header_value.unwrap(); + assert_eq!(header_value.kind, HeaderKind::Int8); + assert_eq!(header_value.value, value.to_le_bytes()); + assert_eq!(header_value.as_int8().unwrap(), value); + } + + #[test] + fn header_value_should_be_created_from_int16() { + let value = 12345; + let header_value = HeaderValue::from_int16(value); + assert!(header_value.is_ok()); + let header_value = header_value.unwrap(); + assert_eq!(header_value.kind, HeaderKind::Int16); + assert_eq!(header_value.value, value.to_le_bytes()); + assert_eq!(header_value.as_int16().unwrap(), value); + } + + #[test] + fn header_value_should_be_created_from_int32() { + let value = 123_456; + let header_value = HeaderValue::from_int32(value); + assert!(header_value.is_ok()); + let header_value = header_value.unwrap(); + assert_eq!(header_value.kind, HeaderKind::Int32); + assert_eq!(header_value.value, value.to_le_bytes()); + assert_eq!(header_value.as_int32().unwrap(), value); + } + + #[test] + fn header_value_should_be_created_from_int64() { + let value = 123_4567; + let header_value = HeaderValue::from_int64(value); + assert!(header_value.is_ok()); + let header_value = header_value.unwrap(); + assert_eq!(header_value.kind, HeaderKind::Int64); + assert_eq!(header_value.value, value.to_le_bytes()); + assert_eq!(header_value.as_int64().unwrap(), value); + } + + #[test] + fn header_value_should_be_created_from_int128() { + let value = 1234_5678; + let header_value = HeaderValue::from_int128(value); + assert!(header_value.is_ok()); + let header_value = header_value.unwrap(); + assert_eq!(header_value.kind, HeaderKind::Int128); + assert_eq!(header_value.value, value.to_le_bytes()); + assert_eq!(header_value.as_int128().unwrap(), value); + } + + #[test] + fn header_value_should_be_created_from_uint8() { + let value = 123; + let header_value = HeaderValue::from_uint8(value); + assert!(header_value.is_ok()); + let header_value = header_value.unwrap(); + assert_eq!(header_value.kind, HeaderKind::Uint8); + assert_eq!(header_value.value, value.to_le_bytes()); + assert_eq!(header_value.as_uint8().unwrap(), value); + } + + #[test] + fn header_value_should_be_created_from_uint16() { + let value = 12345; + let header_value = HeaderValue::from_uint16(value); + assert!(header_value.is_ok()); + let header_value = header_value.unwrap(); + assert_eq!(header_value.kind, HeaderKind::Uint16); + assert_eq!(header_value.value, value.to_le_bytes()); + assert_eq!(header_value.as_uint16().unwrap(), value); + } + + #[test] + fn header_value_should_be_created_from_uint32() { + let value = 123_456; + let header_value = HeaderValue::from_uint32(value); + assert!(header_value.is_ok()); + let header_value = header_value.unwrap(); + assert_eq!(header_value.kind, HeaderKind::Uint32); + assert_eq!(header_value.value, value.to_le_bytes()); + assert_eq!(header_value.as_uint32().unwrap(), value); + } + + #[test] + fn header_value_should_be_created_from_uint64() { + let value = 123_4567; + let header_value = HeaderValue::from_uint64(value); + assert!(header_value.is_ok()); + let header_value = header_value.unwrap(); + assert_eq!(header_value.kind, HeaderKind::Uint64); + assert_eq!(header_value.value, value.to_le_bytes()); + assert_eq!(header_value.as_uint64().unwrap(), value); + } + + #[test] + fn header_value_should_be_created_from_uint128() { + let value = 1234_5678; + let header_value = HeaderValue::from_uint128(value); + assert!(header_value.is_ok()); + let header_value = header_value.unwrap(); + assert_eq!(header_value.kind, HeaderKind::Uint128); + assert_eq!(header_value.value, value.to_le_bytes()); + assert_eq!(header_value.as_uint128().unwrap(), value); + } + + #[test] + fn header_value_should_be_created_from_float32() { + let value = 123.01; + let header_value = HeaderValue::from_float32(value); + assert!(header_value.is_ok()); + let header_value = header_value.unwrap(); + assert_eq!(header_value.kind, HeaderKind::Float32); + assert_eq!(header_value.value, value.to_le_bytes()); + assert_eq!(header_value.as_float32().unwrap(), value); + } + + #[test] + fn header_value_should_be_created_from_float64() { + let value = 1234.01234; + let header_value = HeaderValue::from_float64(value); + assert!(header_value.is_ok()); + let header_value = header_value.unwrap(); + assert_eq!(header_value.kind, HeaderKind::Float64); + assert_eq!(header_value.value, value.to_le_bytes()); + assert_eq!(header_value.as_float64().unwrap(), value); + } + + #[test] + fn should_be_serialized_as_bytes() { + let mut headers = HashMap::new(); + headers.insert( + HeaderKey::new("key-1").unwrap(), + HeaderValue::from_str("Value 1").unwrap(), + ); + headers.insert( + HeaderKey::new("key 1").unwrap(), + HeaderValue::from_uint64(12345).unwrap(), + ); + headers.insert( + HeaderKey::new("key_3").unwrap(), + HeaderValue::from_bool(true).unwrap(), + ); + + let bytes = headers.as_bytes(); + + let mut position = 0; + let mut headers_count = 0; + while position < bytes.len() { + let key_length = + u32::from_le_bytes(bytes[position..position + 4].try_into().unwrap()) as usize; + position += 4; + let key = String::from_utf8(bytes[position..position + key_length].to_vec()).unwrap(); + position += key_length; + let kind = HeaderKind::from_code(bytes[position]).unwrap(); + position += 1; + let value_length = + u32::from_le_bytes(bytes[position..position + 4].try_into().unwrap()) as usize; + position += 4; + let value = bytes[position..position + value_length].to_vec(); + position += value_length; + let header = headers.get(&HeaderKey::new(&key).unwrap()); + assert!(header.is_some()); + let header = header.unwrap(); + assert_eq!(header.kind, kind); + assert_eq!(header.value, value); + headers_count += 1; + } + + assert_eq!(headers_count, headers.len()); + } + + #[test] + fn should_be_deserialized_from_bytes() { + let mut headers = HashMap::new(); + headers.insert( + HeaderKey::new("key-1").unwrap(), + HeaderValue::from_str("Value 1").unwrap(), + ); + headers.insert( + HeaderKey::new("key 2").unwrap(), + HeaderValue::from_uint64(12345).unwrap(), + ); + headers.insert( + HeaderKey::new("key_3").unwrap(), + HeaderValue::from_bool(true).unwrap(), + ); + + let mut bytes = BytesMut::new(); + for (key, value) in &headers { + bytes.put_u32_le(key.0.len() as u32); + bytes.put_slice(key.0.as_bytes()); + bytes.put_u8(value.kind.as_code()); + bytes.put_u32_le(value.value.len() as u32); + bytes.put_slice(&value.value); + } + + let deserialized_headers = HashMap::::from_bytes(bytes.freeze()); + + assert!(deserialized_headers.is_ok()); + let deserialized_headers = deserialized_headers.unwrap(); + assert_eq!(deserialized_headers.len(), headers.len()); + + for (key, value) in &headers { + let deserialized_value = deserialized_headers.get(key); + assert!(deserialized_value.is_some()); + let deserialized_value = deserialized_value.unwrap(); + assert_eq!(deserialized_value.kind, value.kind); + assert_eq!(deserialized_value.value, value.value); + } + } +} diff --git a/src/models/http/client.rs b/src/models/http/client.rs new file mode 100644 index 0000000..16bc134 --- /dev/null +++ b/src/models/http/client.rs @@ -0,0 +1,249 @@ +use crate::infrastructure::error::Error; +use crate::models::client::Client; +use crate::models::http::config::HttpClientConfig; +use crate::models::identity_info::IdentityInfo; +use async_trait::async_trait; +use reqwest::{Response, Url}; +use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; +use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware}; +use serde::Serialize; +use std::sync::Arc; +use tokio::sync::RwLock; + +const UNAUTHORIZED_PATHS: &[&str] = &[ + "/", + "/metrics", + "/ping", + "/users/login", + "/users/refresh-token", + "/personal-access-tokens/login", +]; + +/// HTTP client for interacting with the Iggy API. +/// It requires a valid API URL. +#[derive(Debug)] +pub struct HttpClient { + /// The URL of the Iggy API. + pub api_url: Url, + client: ClientWithMiddleware, + access_token: RwLock, + refresh_token: RwLock, +} + +#[async_trait] +impl Client for HttpClient { + async fn connect(&self) -> Result<(), Error> { + Ok(()) + } + async fn disconnect(&self) -> Result<(), Error> { + Ok(()) + } +} + +unsafe impl Send for HttpClient {} +unsafe impl Sync for HttpClient {} + +impl Default for HttpClient { + fn default() -> Self { + HttpClient::create(Arc::new(HttpClientConfig::default())).unwrap() + } +} + +impl HttpClient { + /// Create a new HTTP client for interacting with the Iggy API using the provided API URL. + pub fn new(api_url: &str) -> Result { + Self::create(Arc::new(HttpClientConfig { + api_url: api_url.to_string(), + ..Default::default() + })) + } + + /// Create a new HTTP client for interacting with the Iggy API using the provided configuration. + pub fn create(config: Arc) -> Result { + let api_url = Url::parse(&config.api_url); + if api_url.is_err() { + return Err(Error::CannotParseUrl); + } + let api_url = api_url.unwrap(); + let retry_policy = ExponentialBackoff::builder().build_with_max_retries(config.retries); + let client = ClientBuilder::new(reqwest::Client::new()) + .with(RetryTransientMiddleware::new_with_policy(retry_policy)) + .build(); + + Ok(Self { + api_url, + client, + access_token: RwLock::new("".to_string()), + refresh_token: RwLock::new("".to_string()), + }) + } + + /// Invoke HTTP GET request to the Iggy API. + pub async fn get(&self, path: &str) -> Result { + let url = self.get_url(path)?; + self.fail_if_not_authenticated(path).await?; + let token = self.access_token.read().await; + let response = self.client.get(url).bearer_auth(token).send().await?; + Self::handle_response(response).await + } + + /// Invoke HTTP GET request to the Iggy API with query parameters. + pub async fn get_with_query( + &self, + path: &str, + query: &T, + ) -> Result { + let url = self.get_url(path)?; + self.fail_if_not_authenticated(path).await?; + let token = self.access_token.read().await; + let response = self + .client + .get(url) + .bearer_auth(token) + .query(query) + .send() + .await?; + Self::handle_response(response).await + } + + /// Invoke HTTP POST request to the Iggy API. + pub async fn post( + &self, + path: &str, + payload: &T, + ) -> Result { + let url = self.get_url(path)?; + self.fail_if_not_authenticated(path).await?; + let token = self.access_token.read().await; + let response = self + .client + .post(url) + .bearer_auth(token) + .json(payload) + .send() + .await?; + Self::handle_response(response).await + } + + /// Invoke HTTP PUT request to the Iggy API. + pub async fn put( + &self, + path: &str, + payload: &T, + ) -> Result { + let url = self.get_url(path)?; + self.fail_if_not_authenticated(path).await?; + let token = self.access_token.read().await; + let response = self + .client + .put(url) + .bearer_auth(token) + .json(payload) + .send() + .await?; + Self::handle_response(response).await + } + + /// Invoke HTTP DELETE request to the Iggy API. + pub async fn delete(&self, path: &str) -> Result { + let url = self.get_url(path)?; + self.fail_if_not_authenticated(path).await?; + let token = self.access_token.read().await; + let response = self.client.delete(url).bearer_auth(token).send().await?; + Self::handle_response(response).await + } + + /// Invoke HTTP DELETE request to the Iggy API with query parameters. + pub async fn delete_with_query( + &self, + path: &str, + query: &T, + ) -> Result { + let url = self.get_url(path)?; + self.fail_if_not_authenticated(path).await?; + let token = self.access_token.read().await; + let response = self + .client + .delete(url) + .bearer_auth(token) + .query(query) + .send() + .await?; + Self::handle_response(response).await + } + + /// Get full URL for the provided path. + pub fn get_url(&self, path: &str) -> Result { + self.api_url.join(path).map_err(|_| Error::CannotParseUrl) + } + + /// Returns true if the client is authenticated. + pub async fn is_authenticated(&self) -> bool { + let token = self.access_token.read().await; + !token.is_empty() + } + + /// Set the refresh token. + pub async fn set_refresh_token(&self, token: Option) { + let mut current_token = self.refresh_token.write().await; + if let Some(token) = token { + *current_token = token; + } else { + *current_token = "".to_string(); + } + } + + /// Set the access token. + pub async fn set_access_token(&self, token: Option) { + let mut current_token = self.access_token.write().await; + if let Some(token) = token { + *current_token = token; + } else { + *current_token = "".to_string(); + } + } + + /// Set the access token and refresh token from the provided identity. + pub async fn set_tokens_from_identity(&self, identity: &IdentityInfo) -> Result<(), Error> { + if identity.tokens.is_none() { + return Err(Error::JwtMissing); + } + + let tokens = identity.tokens.as_ref().unwrap(); + if tokens.access_token.token.is_empty() { + return Err(Error::JwtMissing); + } + + self.set_access_token(Some(tokens.access_token.token.clone())) + .await; + self.set_refresh_token(Some(tokens.refresh_token.token.clone())) + .await; + Ok(()) + } + + /// Refresh the access token using the provided refresh token. + pub async fn refresh_access_token_using_current_refresh_token(&self) -> Result<(), Error> { + let refresh_token = self.refresh_token.read().await; + self.refresh_access_token(&refresh_token).await + } + + async fn handle_response(response: Response) -> Result { + match response.status().is_success() { + true => Ok(response), + false => Err(Error::HttpResponseError( + response.status().as_u16(), + response.text().await.unwrap_or("error".to_string()), + )), + } + } + + async fn fail_if_not_authenticated(&self, path: &str) -> Result<(), Error> { + if UNAUTHORIZED_PATHS.contains(&path) { + return Ok(()); + } + if !self.is_authenticated().await { + return Err(Error::Unauthenticated); + } + Ok(()) + } +} diff --git a/src/models/http/config.rs b/src/models/http/config.rs new file mode 100644 index 0000000..418ad35 --- /dev/null +++ b/src/models/http/config.rs @@ -0,0 +1,17 @@ +/// Configuration for the HTTP client. +#[derive(Debug, Clone)] +pub struct HttpClientConfig { + /// The URL of the Iggy API. + pub api_url: String, + /// The number of retries to perform on transient errors. + pub retries: u32, +} + +impl Default for HttpClientConfig { + fn default() -> HttpClientConfig { + HttpClientConfig { + api_url: "http://127.0.0.1:3000".to_string(), + retries: 3, + } + } +} diff --git a/src/models/http/mod.rs b/src/models/http/mod.rs new file mode 100644 index 0000000..bed2dc4 --- /dev/null +++ b/src/models/http/mod.rs @@ -0,0 +1,11 @@ +pub mod client; +pub mod config; +// pub mod consumer_groups; +// pub mod consumer_offsets; +// pub mod messages; +// pub mod partitions; +pub mod personal_access_tokens; +// pub mod streams; +pub mod system; +// pub mod topics; +pub mod users; diff --git a/src/models/http/personal_access_tokens.rs b/src/models/http/personal_access_tokens.rs new file mode 100644 index 0000000..1c35f04 --- /dev/null +++ b/src/models/http/personal_access_tokens.rs @@ -0,0 +1,51 @@ +use crate::infrastructure::error::Error; +use crate::models::client::PersonalAccessTokenClient; +use crate::models::http::client::HttpClient; +use crate::models::identity_info::IdentityInfo; +use crate::models::personal_access_token::{PersonalAccessTokenInfo, RawPersonalAccessToken}; +use crate::models::personal_access_tokens::create_personal_access_token::CreatePersonalAccessToken; +use crate::models::personal_access_tokens::delete_personal_access_token::DeletePersonalAccessToken; +use crate::models::personal_access_tokens::get_personal_access_tokens::GetPersonalAccessTokens; +use crate::models::personal_access_tokens::login_with_personal_access_token::LoginWithPersonalAccessToken; +use async_trait::async_trait; + +const PATH: &str = "/personal-access-tokens"; + +#[async_trait] +impl PersonalAccessTokenClient for HttpClient { + async fn get_personal_access_tokens( + &self, + _command: &GetPersonalAccessTokens, + ) -> Result, Error> { + let response = self.get(PATH).await?; + let personal_access_tokens = response.json().await?; + Ok(personal_access_tokens) + } + + async fn create_personal_access_token( + &self, + command: &CreatePersonalAccessToken, + ) -> Result { + let response = self.post(PATH, &command).await?; + let personal_access_token: RawPersonalAccessToken = response.json().await?; + Ok(personal_access_token) + } + + async fn delete_personal_access_token( + &self, + command: &DeletePersonalAccessToken, + ) -> Result<(), Error> { + self.delete(&format!("{PATH}/{}", command.name)).await?; + Ok(()) + } + + async fn login_with_personal_access_token( + &self, + command: &LoginWithPersonalAccessToken, + ) -> Result { + let response = self.post(&format!("{PATH}/login"), &command).await?; + let identity_info: IdentityInfo = response.json().await?; + self.set_tokens_from_identity(&identity_info).await?; + Ok(identity_info) + } +} diff --git a/src/models/http/system.rs b/src/models/http/system.rs new file mode 100644 index 0000000..56bf54e --- /dev/null +++ b/src/models/http/system.rs @@ -0,0 +1,46 @@ +use crate::infrastructure::error::Error; +use crate::models::client::SystemClient; +use crate::models::client_info::{ClientInfo, ClientInfoDetails}; +use crate::models::http::client::HttpClient; +use crate::models::stats::Stats; +use crate::models::system::get_client::GetClient; +use crate::models::system::get_clients::GetClients; +use crate::models::system::get_me::GetMe; +use crate::models::system::get_stats::GetStats; +use crate::models::system::ping::Ping; +use async_trait::async_trait; + +const PING: &str = "/ping"; +const CLIENTS: &str = "/clients"; +const STATS: &str = "/stats"; + +#[async_trait] +impl SystemClient for HttpClient { + async fn get_stats(&self, _command: &GetStats) -> Result { + let response = self.get(STATS).await?; + let stats = response.json().await?; + Ok(stats) + } + + async fn get_me(&self, _command: &GetMe) -> Result { + Err(Error::FeatureUnavailable) + } + + async fn get_client(&self, command: &GetClient) -> Result { + let path = format!("{}/{}", CLIENTS, command.client_id); + let response = self.get(&path).await?; + let client = response.json().await?; + Ok(client) + } + + async fn get_clients(&self, _command: &GetClients) -> Result, Error> { + let response = self.get(CLIENTS).await?; + let clients = response.json().await?; + Ok(clients) + } + + async fn ping(&self, _command: &Ping) -> Result<(), Error> { + self.get(PING).await?; + Ok(()) + } +} diff --git a/src/models/http/users.rs b/src/models/http/users.rs new file mode 100644 index 0000000..e242a54 --- /dev/null +++ b/src/models/http/users.rs @@ -0,0 +1,102 @@ +use crate::models::http::client::HttpClient; +use crate::infrastructure::error::Error; +use crate::models::client::UserClient; +use crate::models::identity_info::IdentityInfo; +use crate::models::user_info::{UserInfo, UserInfoDetails}; +use crate::models::users::change_password::ChangePassword; +use crate::models::users::create_user::CreateUser; +use crate::models::users::delete_user::DeleteUser; +use crate::models::users::get_user::GetUser; +use crate::models::users::get_users::GetUsers; +use crate::models::users::login_user::LoginUser; +use crate::models::users::logout_user::LogoutUser; +use crate::models::users::update_permissions::UpdatePermissions; +use crate::models::users::update_user::UpdateUser; +use async_trait::async_trait; +use serde::Serialize; + +const PATH: &str = "/users"; + +#[async_trait] +impl UserClient for HttpClient { + async fn get_user(&self, command: &GetUser) -> Result { + let response = self.get(&format!("{PATH}/{}", command.user_id)).await?; + let user = response.json().await?; + Ok(user) + } + + async fn get_users(&self, _command: &GetUsers) -> Result, Error> { + let response = self.get(PATH).await?; + let users = response.json().await?; + Ok(users) + } + + async fn create_user(&self, command: &CreateUser) -> Result<(), Error> { + self.post(PATH, &command).await?; + Ok(()) + } + + async fn delete_user(&self, command: &DeleteUser) -> Result<(), Error> { + self.delete(&format!("{PATH}/{}", command.user_id)).await?; + Ok(()) + } + + async fn update_user(&self, command: &UpdateUser) -> Result<(), Error> { + self.put(&format!("{PATH}/{}", command.user_id), &command) + .await?; + Ok(()) + } + + async fn update_permissions(&self, command: &UpdatePermissions) -> Result<(), Error> { + self.put(&format!("{PATH}/{}/permissions", command.user_id), &command) + .await?; + Ok(()) + } + + async fn change_password(&self, command: &ChangePassword) -> Result<(), Error> { + self.put(&format!("{PATH}/{}/password", command.user_id), &command) + .await?; + Ok(()) + } + + async fn login_user(&self, command: &LoginUser) -> Result { + let response = self.post(&format!("{PATH}/login"), &command).await?; + let identity_info: IdentityInfo = response.json().await?; + self.set_tokens_from_identity(&identity_info).await?; + Ok(identity_info) + } + + async fn logout_user(&self, command: &LogoutUser) -> Result<(), Error> { + self.post(&format!("{PATH}/logout"), &command).await?; + self.set_access_token(None).await; + self.set_refresh_token(None).await; + Ok(()) + } +} + +impl HttpClient { + pub async fn refresh_access_token(&self, refresh_token: &str) -> Result<(), Error> { + if refresh_token.is_empty() { + return Err(Error::RefreshTokenMissing); + } + + let command = RefreshToken { + refresh_token: refresh_token.to_string(), + }; + let response = self + .post(&format!("{PATH}/refresh-token"), &command) + .await?; + let identity_info: IdentityInfo = response.json().await?; + if identity_info.tokens.is_none() { + return Err(Error::JwtMissing); + } + + self.set_tokens_from_identity(&identity_info).await?; + Ok(()) + } +} + +#[derive(Debug, Serialize)] +struct RefreshToken { + refresh_token: String, +} diff --git a/src/models/identifier.rs b/src/models/identifier.rs new file mode 100644 index 0000000..07d049e --- /dev/null +++ b/src/models/identifier.rs @@ -0,0 +1,268 @@ +use crate::infrastructure::error::Error; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::validatable::Validatable; +use bytes::{BufMut, Bytes, BytesMut}; +use serde::{Deserialize, Serialize}; +use serde_with::base64::Base64; +use serde_with::serde_as; +use std::borrow::Cow; +use std::fmt::Display; +use std::str::FromStr; + +/// `Identifier` represents the unique identifier of the resources such as stream, topic, partition, user etc. +/// It consists of the following fields: +/// - `kind`: the kind of the identifier. +/// - `length`: the length of the identifier payload. +/// - `value`: the binary value of the identifier payload. +#[serde_as] +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct Identifier { + /// The kind of the identifier. + pub kind: IdKind, + /// The length of the identifier payload. + #[serde(skip)] + pub length: u8, + /// The binary value of the identifier payload, max length is 255 bytes. + #[serde_as(as = "Base64")] + pub value: Vec, +} + +/// `IdKind` represents the kind of the identifier. +#[derive(Debug, Serialize, Deserialize, PartialEq, Default, Copy, Clone)] +#[serde(rename_all = "snake_case")] +pub enum IdKind { + /// The identifier is numeric. + #[default] + Numeric, + /// The identifier is string. + String, +} + +impl Default for Identifier { + fn default() -> Self { + Self { + kind: IdKind::default(), + length: 4, + value: 1u32.to_le_bytes().to_vec(), + } + } +} + +impl Validatable for Identifier { + fn validate(&self) -> Result<(), Error> { + if self.length == 0 { + return Err(Error::InvalidCommand); + } + + if self.value.is_empty() { + return Err(Error::InvalidCommand); + } + + #[allow(clippy::cast_possible_truncation)] + if self.length != self.value.len() as u8 { + return Err(Error::InvalidCommand); + } + + if self.kind == IdKind::Numeric && self.length != 4 { + return Err(Error::InvalidCommand); + } + + Ok(()) + } +} + +impl Identifier { + /// Returns the numeric value of the identifier. + pub fn get_u32_value(&self) -> Result { + if self.kind != IdKind::Numeric { + return Err(Error::InvalidCommand); + } + + if self.length != 4 { + return Err(Error::InvalidCommand); + } + + Ok(u32::from_le_bytes(self.value.clone().try_into().unwrap())) + } + + /// Returns the string value of the identifier. + pub fn get_string_value(&self) -> Result { + self.get_cow_str_value().map(|cow| cow.to_string()) + } + + /// Returns the Cow value of the identifier. + pub fn get_cow_str_value(&self) -> Result, Error> { + if self.kind != IdKind::String { + return Err(Error::InvalidCommand); + } + + Ok(String::from_utf8_lossy(&self.value)) + } + + /// Returns the string representation of the identifier. + pub fn as_string(&self) -> String { + self.as_cow_str().to_string() + } + + // Returns the Cow representation of the identifier. + pub fn as_cow_str(&self) -> Cow { + match self.kind { + IdKind::Numeric => Cow::Owned(self.get_u32_value().unwrap().to_string()), + IdKind::String => self.get_cow_str_value().unwrap(), + } + } + + /// Returns the size of the identifier in bytes. + pub fn get_size_bytes(&self) -> u32 { + 2 + u32::from(self.length) + } + + /// Creates a new identifier from the given identifier. + pub fn from_identifier(identifier: &Identifier) -> Self { + Self { + kind: identifier.kind, + length: identifier.length, + value: identifier.value.clone(), + } + } + + /// Creates a new identifier from the given string value, either numeric or string. + pub fn from_str_value(value: &str) -> Result { + let length = value.len(); + if length == 0 || length > 255 { + return Err(Error::InvalidCommand); + } + + match value.parse::() { + Ok(id) => Identifier::numeric(id), + Err(_) => Identifier::named(value), + } + } + + /// Creates a new identifier from the given numeric value. + pub fn numeric(value: u32) -> Result { + if value == 0 { + return Err(Error::InvalidCommand); + } + + Ok(Self { + kind: IdKind::Numeric, + length: 4, + value: value.to_le_bytes().to_vec(), + }) + } + + /// Creates a new identifier from the given string value. The name will be always converted to lowercase and all whitespaces will be replaced with dots. + pub fn named(value: &str) -> Result { + let length = value.len(); + if length == 0 || length > 255 { + return Err(Error::InvalidCommand); + } + + Ok(Self { + kind: IdKind::String, + #[allow(clippy::cast_possible_truncation)] + length: length as u8, + value: value.as_bytes().to_vec(), + }) + } +} + +impl BytesSerializable for Identifier { + fn as_bytes(&self) -> Bytes { + let mut bytes = BytesMut::with_capacity(2 + self.length as usize); + bytes.put_u8(self.kind.as_code()); + bytes.put_u8(self.length); + bytes.put_slice(&self.value); + bytes.freeze() + } + + fn from_bytes(bytes: Bytes) -> Result + where + Self: Sized, + { + if bytes.len() < 3 { + return Err(Error::InvalidCommand); + } + + let kind = IdKind::from_code(bytes[0])?; + let length = bytes[1]; + let value = bytes[2..2 + length as usize].to_vec(); + if value.len() != length as usize { + return Err(Error::InvalidCommand); + } + + let identifier = Identifier { + kind, + length, + value, + }; + identifier.validate()?; + Ok(identifier) + } +} + +impl IdKind { + /// Returns the code of the identifier kind. + pub fn as_code(&self) -> u8 { + match self { + IdKind::Numeric => 1, + IdKind::String => 2, + } + } + + /// Returns the identifier kind from the code. + pub fn from_code(code: u8) -> Result { + match code { + 1 => Ok(IdKind::Numeric), + 2 => Ok(IdKind::String), + _ => Err(Error::InvalidCommand), + } + } +} + +impl FromStr for IdKind { + type Err = Error; + fn from_str(input: &str) -> Result { + match input { + "n" | "numeric" => Ok(IdKind::Numeric), + "s" | "string" => Ok(IdKind::String), + _ => Err(Error::InvalidCommand), + } + } +} + +impl FromStr for Identifier { + type Err = Error; + fn from_str(input: &str) -> Result { + if let Ok(value) = input.parse::() { + return Identifier::numeric(value); + } + + let identifier = Identifier::named(input)?; + identifier.validate()?; + Ok(identifier) + } +} + +impl Display for Identifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.kind { + IdKind::Numeric => write!( + f, + "{}", + u32::from_le_bytes(self.value.as_slice().try_into().unwrap()) + ), + IdKind::String => write!(f, "{}", String::from_utf8_lossy(&self.value)), + } + } +} + +impl Display for IdKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + IdKind::Numeric => write!(f, "numeric"), + IdKind::String => write!(f, "string"), + } + } +} diff --git a/src/models/identity_info.rs b/src/models/identity_info.rs new file mode 100644 index 0000000..7d0d469 --- /dev/null +++ b/src/models/identity_info.rs @@ -0,0 +1,38 @@ +use super::user_info::UserId; +use serde::{Deserialize, Serialize}; + +/// `IdentityInfo` represents the information about an identity. +/// It consists of the following fields: +/// - `user_id`: the unique identifier (numeric) of the user. +/// - `tokens`: the optional tokens, used only by HTTP transport. +#[derive(Debug, Serialize, Deserialize)] +pub struct IdentityInfo { + /// The unique identifier (numeric) of the user. + pub user_id: UserId, + /// The optional tokens, used only by HTTP transport. + pub tokens: Option, +} + +/// `IdentityTokens` represents the information about the tokens, currently used only by HTTP transport. +/// It consists of the following fields: +/// - `access_token`: the access token used for the authentication. +/// - `refresh_token`: the refresh token used to refresh the access token. +#[derive(Debug, Serialize, Deserialize)] +pub struct IdentityTokens { + /// The access token used for the authentication. + pub access_token: TokenInfo, + /// The refresh token used to refresh the access token. + pub refresh_token: TokenInfo, +} + +/// `TokenInfo` represents the details of the particular token. +/// It consists of the following fields: +/// - `token`: the value of token. +/// - `expiry`: the expiry of token. +#[derive(Debug, Serialize, Deserialize)] +pub struct TokenInfo { + /// The value of token. + pub token: String, + /// The expiry of token. + pub expiry: u64, +} diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..8bd0628 --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,20 @@ +pub mod binary; +pub mod bytes_serializable; +pub mod client; +pub mod client_info; +pub mod command; +pub mod header; +pub mod http; +pub mod identifier; +pub mod identity_info; +pub mod permissions; +pub mod personal_access_token; +pub mod personal_access_tokens; +pub mod sizeable; +pub mod stats; +pub mod system; +pub mod tcp; +pub mod user_info; +pub mod user_status; +pub mod users; +pub mod validatable; diff --git a/src/models/permissions.rs b/src/models/permissions.rs new file mode 100644 index 0000000..d4c9ce2 --- /dev/null +++ b/src/models/permissions.rs @@ -0,0 +1,411 @@ +use crate::infrastructure::error::Error; +use crate::models::bytes_serializable::BytesSerializable; +use bytes::{Buf, BufMut, Bytes, BytesMut}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fmt::Display; + +/// `Permissions` is used to define the permissions of a user. +/// It consists of global permissions and stream permissions. +/// Global permissions are applied to all streams. +/// Stream permissions are applied to a specific stream. +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)] +pub struct Permissions { + /// Global permissions are applied to all streams. + pub global: GlobalPermissions, + + /// Stream permissions are applied to a specific stream. + pub streams: Option>, +} + +/// `GlobalPermissions` are applied to all streams without a need to specify them one by one in the `streams` field. +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)] +pub struct GlobalPermissions { + /// `manage_servers` permission allows to manage the servers and includes all the permissions of `read_servers`. + pub manage_servers: bool, + + /// `read_servers` permission allows to invoke the following methods: + /// - get_stats + /// - get_clients + /// - get_client + pub read_servers: bool, + + /// `manage_users` permission allows to manage the users and includes all the permissions of `read_users`. + /// Additionally, the following methods can be invoked: + /// - create_user + /// - update_user + /// - delete_user + /// - update_permissions + /// - change_password + pub manage_users: bool, + + /// `read_users` permission allows to invoke the following methods: + /// - get_user + /// - get_users + pub read_users: bool, + + /// `manage_streams` permission allows to manage the streams and includes all the permissions of `read_streams`. + /// Also, it allows to manage all the topics of a stream, thus it has all the permissions of `manage_topics`. + /// Additionally, the following methods can be invoked: + /// - create_stream + /// - update_stream + /// - delete_stream + pub manage_streams: bool, + + /// `read_streams` permission allows to read the streams and includes all the permissions of `read_topics`. + /// Additionally, the following methods can be invoked: + /// - get_stream + /// - get_streams + pub read_streams: bool, + + /// `manage_topics` permission allows to manage the topics and includes all the permissions of `read_topics`. + /// Also, it allows to manage all the partitions of a topic, thus it has all the permissions of `manage_topic`. + /// Additionally, the following methods can be invoked: + /// - create_topic + /// - update_topic + /// - delete_topic + pub manage_topics: bool, + + /// `read_topics` permission allows to read the topics and includes all the permissions of `read_messages`. + /// Additionally, the following methods can be invoked: + /// - get_topic + /// - get_topics + pub read_topics: bool, + + /// `poll_messages` permission allows to poll messages from all the streams and theirs topics. + pub poll_messages: bool, + + /// `send_messages` permission allows to send messages to all the streams and theirs topics. + pub send_messages: bool, +} + +/// `StreamPermissions` are applied to a specific stream and its all topics. If you want to define granular permissions for each topic, use the `topics` field. +/// These permissions do not override the global permissions, but extend them, and allow more granular control over the streams and the users that can access them. +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)] +pub struct StreamPermissions { + /// `manage_stream` permission allows to manage the stream and includes all the permissions of `read_stream`. + /// Also, it allows to manage all the topics of a stream, thus it has all the permissions of `manage_topics`. + /// Additionally, the following methods can be invoked: + /// - create_stream + /// - update_stream + /// - delete_stream + pub manage_stream: bool, + + /// `read_stream` permission allows to read the stream and includes all the permissions of `read_topics`. + /// Also, it allows to read all the messages of a topic, thus it has all the permissions of `read_messages`. + /// Additionally, the following methods can be invoked: + /// - get_stream + /// - get_streams + pub read_stream: bool, + + /// `manage_topics` permission allows to manage the topics and includes all the permissions of `read_topics`. + /// Also, it allows to manage all the partitions of a topic, thus it has all the permissions of `manage_topic`. + /// Additionally, the following methods can be invoked: + /// - create_topic + /// - update_topic + /// - delete_topic + pub manage_topics: bool, + + /// `read_topics` permission allows to read the topics and includes all the permissions of `read_messages`. + pub read_topics: bool, + + /// `poll_messages` permission allows to poll messages from the stream and its topics. + pub poll_messages: bool, + + /// `send_messages` permission allows to send messages to the stream and its topics. + pub send_messages: bool, + + /// The `topics` field allows to define the granular permissions for each topic of a stream. + pub topics: Option>, +} + +/// `TopicPermissions` are applied to a specific topic of a stream. This is the lowest level of permissions. +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)] +pub struct TopicPermissions { + /// `manage_topic` permission allows to manage the topic and includes all the permissions of `read_topic`. + pub manage_topic: bool, + + /// `read_topic` permission allows to read the topic and includes all the permissions of `read_messages`. + pub read_topic: bool, + + /// `poll_messages` permission allows to poll messages from the topic. + pub poll_messages: bool, + + /// `send_messages` permission allows to send messages to the topic. + pub send_messages: bool, +} + +impl Permissions { + pub fn root() -> Self { + Self { + global: GlobalPermissions { + manage_servers: true, + read_servers: true, + manage_users: true, + read_users: true, + manage_streams: true, + read_streams: true, + manage_topics: true, + read_topics: true, + poll_messages: true, + send_messages: true, + }, + streams: None, + } + } +} + +impl Display for Permissions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut result = String::new(); + result.push_str(&format!("manage_servers: {}\n", self.global.manage_servers)); + result.push_str(&format!("read_servers: {}\n", self.global.read_servers)); + result.push_str(&format!("manage_users: {}\n", self.global.manage_users)); + result.push_str(&format!("read_users: {}\n", self.global.read_users)); + result.push_str(&format!("manage_streams: {}\n", self.global.manage_streams)); + result.push_str(&format!("read_streams: {}\n", self.global.read_streams)); + result.push_str(&format!("manage_topics: {}\n", self.global.manage_topics)); + result.push_str(&format!("read_topics: {}\n", self.global.read_topics)); + result.push_str(&format!("poll_messages: {}\n", self.global.poll_messages)); + result.push_str(&format!("send_messages: {}\n", self.global.send_messages)); + if let Some(streams) = &self.streams { + for (stream_id, stream) in streams { + result.push_str(&format!("stream_id: {}\n", stream_id)); + result.push_str(&format!("manage_stream: {}\n", stream.manage_stream)); + result.push_str(&format!("read_stream: {}\n", stream.read_stream)); + result.push_str(&format!("manage_topics: {}\n", stream.manage_topics)); + result.push_str(&format!("read_topics: {}\n", stream.read_topics)); + result.push_str(&format!("poll_messages: {}\n", stream.poll_messages)); + result.push_str(&format!("send_messages: {}\n", stream.send_messages)); + if let Some(topics) = &stream.topics { + for (topic_id, topic) in topics { + result.push_str(&format!("topic_id: {}\n", topic_id)); + result.push_str(&format!("manage_topic: {}\n", topic.manage_topic)); + result.push_str(&format!("read_topic: {}\n", topic.read_topic)); + result.push_str(&format!("poll_messages: {}\n", topic.poll_messages)); + result.push_str(&format!("send_messages: {}\n", topic.send_messages)); + } + } + } + } + + write!(f, "{}", result) + } +} + +impl BytesSerializable for Permissions { + fn as_bytes(&self) -> Bytes { + let mut bytes = BytesMut::new(); + bytes.put_u8(if self.global.manage_servers { 1 } else { 0 }); + bytes.put_u8(if self.global.read_servers { 1 } else { 0 }); + bytes.put_u8(if self.global.manage_users { 1 } else { 0 }); + bytes.put_u8(if self.global.read_users { 1 } else { 0 }); + bytes.put_u8(if self.global.manage_streams { 1 } else { 0 }); + bytes.put_u8(if self.global.read_streams { 1 } else { 0 }); + bytes.put_u8(if self.global.manage_topics { 1 } else { 0 }); + bytes.put_u8(if self.global.read_topics { 1 } else { 0 }); + bytes.put_u8(if self.global.poll_messages { 1 } else { 0 }); + bytes.put_u8(if self.global.send_messages { 1 } else { 0 }); + if let Some(streams) = &self.streams { + bytes.put_u8(1); + let streams_count = streams.len(); + let mut current_stream = 1; + for (stream_id, stream) in streams { + bytes.put_u32_le(*stream_id); + bytes.put_u8(if stream.manage_stream { 1 } else { 0 }); + bytes.put_u8(if stream.read_stream { 1 } else { 0 }); + bytes.put_u8(if stream.manage_topics { 1 } else { 0 }); + bytes.put_u8(if stream.read_topics { 1 } else { 0 }); + bytes.put_u8(if stream.poll_messages { 1 } else { 0 }); + bytes.put_u8(if stream.send_messages { 1 } else { 0 }); + if let Some(topics) = &stream.topics { + bytes.put_u8(1); + let topics_count = topics.len(); + let mut current_topic = 1; + for (topic_id, topic) in topics { + bytes.put_u32_le(*topic_id); + bytes.put_u8(if topic.manage_topic { 1 } else { 0 }); + bytes.put_u8(if topic.read_topic { 1 } else { 0 }); + bytes.put_u8(if topic.poll_messages { 1 } else { 0 }); + bytes.put_u8(if topic.send_messages { 1 } else { 0 }); + if current_topic < topics_count { + current_topic += 1; + bytes.put_u8(1); + } else { + bytes.put_u8(0); + } + } + } else { + bytes.put_u8(0); + } + if current_stream < streams_count { + current_stream += 1; + bytes.put_u8(1); + } else { + bytes.put_u8(0); + } + } + } else { + bytes.put_u8(0); + } + bytes.freeze() + } + + fn from_bytes(bytes: Bytes) -> Result + where + Self: Sized, + { + let mut bytes = bytes; + let manage_servers = bytes.get_u8() == 1; + let read_servers = bytes.get_u8() == 1; + let manage_users = bytes.get_u8() == 1; + let read_users = bytes.get_u8() == 1; + let manage_streams = bytes.get_u8() == 1; + let read_streams = bytes.get_u8() == 1; + let manage_topics = bytes.get_u8() == 1; + let read_topics = bytes.get_u8() == 1; + let poll_messages = bytes.get_u8() == 1; + let send_messages = bytes.get_u8() == 1; + let mut streams = None; + if bytes.get_u8() == 1 { + let mut streams_map = HashMap::new(); + loop { + let stream_id = bytes.get_u32_le(); + let manage_stream = bytes.get_u8() == 1; + let read_stream = bytes.get_u8() == 1; + let manage_topics = bytes.get_u8() == 1; + let read_topics = bytes.get_u8() == 1; + let poll_messages = bytes.get_u8() == 1; + let send_messages = bytes.get_u8() == 1; + let mut topics = None; + if bytes.get_u8() == 1 { + let mut topics_map = HashMap::new(); + loop { + let topic_id = bytes.get_u32_le(); + let manage_topic = bytes.get_u8() == 1; + let read_topic = bytes.get_u8() == 1; + let poll_messages = bytes.get_u8() == 1; + let send_messages = bytes.get_u8() == 1; + topics_map.insert( + topic_id, + TopicPermissions { + manage_topic, + read_topic, + poll_messages, + send_messages, + }, + ); + if bytes.get_u8() == 0 { + break; + } + } + topics = Some(topics_map); + } + streams_map.insert( + stream_id, + StreamPermissions { + manage_stream, + read_stream, + manage_topics, + read_topics, + poll_messages, + send_messages, + topics, + }, + ); + if bytes.get_u8() == 0 { + break; + } + } + streams = Some(streams_map); + } + Ok(Self { + global: GlobalPermissions { + manage_servers, + read_servers, + manage_users, + read_users, + manage_streams, + read_streams, + manage_topics, + read_topics, + poll_messages, + send_messages, + }, + streams, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_be_serialized_and_deserialized_from_bytes() { + let permissions = Permissions { + global: GlobalPermissions { + manage_servers: true, + read_servers: true, + manage_users: true, + read_users: true, + manage_streams: false, + read_streams: true, + manage_topics: false, + read_topics: true, + poll_messages: true, + send_messages: true, + }, + streams: Some(HashMap::from([ + ( + 1, + StreamPermissions { + manage_stream: true, + read_stream: true, + manage_topics: true, + read_topics: true, + poll_messages: true, + send_messages: true, + topics: Some(HashMap::from([ + ( + 1, + TopicPermissions { + manage_topic: true, + read_topic: true, + poll_messages: true, + send_messages: true, + }, + ), + ( + 2, + TopicPermissions { + manage_topic: true, + read_topic: false, + poll_messages: true, + send_messages: false, + }, + ), + ])), + }, + ), + ( + 2, + StreamPermissions { + manage_stream: false, + read_stream: true, + manage_topics: false, + read_topics: true, + poll_messages: true, + send_messages: true, + topics: None, + }, + ), + ])), + }; + + let bytes = permissions.as_bytes(); + let deserialized_permissions = Permissions::from_bytes(bytes).unwrap(); + + assert_eq!(permissions, deserialized_permissions); + } +} diff --git a/src/models/personal_access_token.rs b/src/models/personal_access_token.rs new file mode 100644 index 0000000..2ef1cc2 --- /dev/null +++ b/src/models/personal_access_token.rs @@ -0,0 +1,22 @@ +use serde::{Deserialize, Serialize}; + +/// `RawPersonalAccessToken` represents the raw personal access token - the secured token which is returned only once during the creation. +/// It consists of the following fields: +/// - `token`: the unique token that should be securely stored by the user and can be used for authentication. +#[derive(Debug, Serialize, Deserialize)] +pub struct RawPersonalAccessToken { + /// The unique token that should be securely stored by the user and can be used for authentication. + pub token: String, +} + +/// `PersonalAccessToken` represents the personal access token. It does not contain the token itself, but the information about the token. +/// It consists of the following fields: +/// - `name`: the unique name of the token. +/// - `expiry`: the optional expiry of the token. +#[derive(Debug, Serialize, Deserialize)] +pub struct PersonalAccessTokenInfo { + /// The unique name of the token. + pub name: String, + /// The optional expiry of the token. + pub expiry: Option, +} diff --git a/src/models/personal_access_tokens/create_personal_access_token.rs b/src/models/personal_access_tokens/create_personal_access_token.rs new file mode 100644 index 0000000..d8b465d --- /dev/null +++ b/src/models/personal_access_tokens/create_personal_access_token.rs @@ -0,0 +1,138 @@ +use crate::infrastructure::error::Error; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::command::CommandPayload; +use crate::models::users::defaults::*; +use crate::models::validatable::Validatable; +use crate::utils::text; +use bytes::{BufMut, Bytes, BytesMut}; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use std::str::from_utf8; + +/// `CreatePersonalAccessToken` command is used to create a new personal access token for the authenticated user. +/// It has additional payload: +/// - `name` - unique name of the token, must be between 3 and 30 characters long. The name will be always converted to lowercase and all whitespaces will be replaced with dots. +/// - `expiry` - expiry in seconds (optional), if provided, must be between 1 and 4294967295. Otherwise, the token will never expire. +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct CreatePersonalAccessToken { + /// Unique name of the token, must be between 3 and 30 characters long. + pub name: String, + /// Expiry in seconds (optional), if provided, must be between 1 and 4294967295. Otherwise, the token will never expire. + pub expiry: Option, +} + +impl CommandPayload for CreatePersonalAccessToken {} + +impl Default for CreatePersonalAccessToken { + fn default() -> Self { + CreatePersonalAccessToken { + name: "token".to_string(), + expiry: None, + } + } +} + +impl Validatable for CreatePersonalAccessToken { + fn validate(&self) -> Result<(), Error> { + if self.name.is_empty() + || self.name.len() > MAX_PERSONAL_ACCESS_TOKEN_NAME_LENGTH + || self.name.len() < MIN_PERSONAL_ACCESS_TOKEN_NAME_LENGTH + { + return Err(Error::InvalidPersonalAccessTokenName); + } + + if !text::is_resource_name_valid(&self.name) { + return Err(Error::InvalidPersonalAccessTokenName); + } + + Ok(()) + } +} + +impl BytesSerializable for CreatePersonalAccessToken { + fn as_bytes(&self) -> Bytes { + let mut bytes = BytesMut::with_capacity(5 + self.name.len()); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u8(self.name.len() as u8); + bytes.put_slice(self.name.as_bytes()); + bytes.put_u32_le(self.expiry.unwrap_or(0)); + bytes.freeze() + } + + fn from_bytes(bytes: Bytes) -> Result { + if bytes.len() < 8 { + return Err(Error::InvalidCommand); + } + + let name_length = bytes[0]; + let name = from_utf8(&bytes.slice(1..1 + name_length as usize))?.to_string(); + if name.len() != name_length as usize { + return Err(Error::InvalidCommand); + } + + let position = 1 + name_length as usize; + let expiry = u32::from_le_bytes(bytes[position..position + 4].try_into()?); + let expiry = match expiry { + 0 => None, + _ => Some(expiry), + }; + + let command = CreatePersonalAccessToken { name, expiry }; + command.validate()?; + Ok(command) + } +} + +impl Display for CreatePersonalAccessToken { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}|{}", self.name, self.expiry.unwrap_or(0)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_be_serialized_as_bytes() { + let command = CreatePersonalAccessToken { + name: "test".to_string(), + expiry: Some(100), + }; + + let bytes = command.as_bytes(); + let name_length = bytes[0]; + let name = from_utf8(&bytes[1..1 + name_length as usize]).unwrap(); + let expiry = u32::from_le_bytes( + bytes[1 + name_length as usize..5 + name_length as usize] + .try_into() + .unwrap(), + ); + let expiry = match expiry { + 0 => None, + _ => Some(expiry), + }; + + assert!(!bytes.is_empty()); + assert_eq!(name, command.name); + assert_eq!(expiry, command.expiry); + } + + #[test] + fn should_be_deserialized_from_bytes() { + let name = "test"; + let expiry = 100; + let mut bytes = BytesMut::new(); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u8(name.len() as u8); + bytes.put_slice(name.as_bytes()); + bytes.put_u32_le(expiry); + + let command = CreatePersonalAccessToken::from_bytes(bytes.freeze()); + assert!(command.is_ok()); + + let command = command.unwrap(); + assert_eq!(command.name, name); + assert_eq!(command.expiry, Some(expiry)); + } +} diff --git a/src/models/personal_access_tokens/delete_personal_access_token.rs b/src/models/personal_access_tokens/delete_personal_access_token.rs new file mode 100644 index 0000000..4627575 --- /dev/null +++ b/src/models/personal_access_tokens/delete_personal_access_token.rs @@ -0,0 +1,111 @@ +use crate::infrastructure::error::Error; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::command::CommandPayload; +use crate::models::users::defaults::*; +use crate::models::validatable::Validatable; +use crate::utils::text; +use bytes::{BufMut, Bytes, BytesMut}; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use std::str::from_utf8; + +/// `DeletePersonalAccessToken` command is used to delete a personal access token for the authenticated user. +/// It has additional payload: +/// - `name` - unique name of the token, must be between 3 and 30 characters long. The name will be always converted to lowercase and all whitespaces will be replaced with dots. +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct DeletePersonalAccessToken { + /// Unique name of the token, must be between 3 and 30 characters long. + pub name: String, +} + +impl CommandPayload for DeletePersonalAccessToken {} + +impl Default for DeletePersonalAccessToken { + fn default() -> Self { + DeletePersonalAccessToken { + name: "token".to_string(), + } + } +} + +impl Validatable for DeletePersonalAccessToken { + fn validate(&self) -> Result<(), Error> { + if self.name.is_empty() + || self.name.len() > MAX_PERSONAL_ACCESS_TOKEN_NAME_LENGTH + || self.name.len() < MIN_PERSONAL_ACCESS_TOKEN_NAME_LENGTH + { + return Err(Error::InvalidPersonalAccessTokenName); + } + + if !text::is_resource_name_valid(&self.name) { + return Err(Error::InvalidPersonalAccessTokenName); + } + + Ok(()) + } +} + +impl BytesSerializable for DeletePersonalAccessToken { + fn as_bytes(&self) -> Bytes { + let mut bytes = BytesMut::with_capacity(5 + self.name.len()); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u8(self.name.len() as u8); + bytes.put_slice(self.name.as_bytes()); + bytes.freeze() + } + + fn from_bytes(bytes: Bytes) -> Result { + if bytes.len() < 4 { + return Err(Error::InvalidCommand); + } + + let name_length = bytes[0]; + let name = from_utf8(&bytes[1..1 + name_length as usize])?.to_string(); + if name.len() != name_length as usize { + return Err(Error::InvalidCommand); + } + + let command = DeletePersonalAccessToken { name }; + command.validate()?; + Ok(command) + } +} + +impl Display for DeletePersonalAccessToken { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_be_serialized_as_bytes() { + let command = DeletePersonalAccessToken { + name: "test".to_string(), + }; + + let bytes = command.as_bytes(); + let name_length = bytes[0]; + let name = from_utf8(&bytes[1..1 + name_length as usize]).unwrap(); + assert!(!bytes.is_empty()); + assert_eq!(name, command.name); + } + + #[test] + fn should_be_deserialized_from_bytes() { + let name = "test"; + let mut bytes = BytesMut::new(); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u8(name.len() as u8); + bytes.put_slice(name.as_bytes()); + + let command = DeletePersonalAccessToken::from_bytes(bytes.freeze()); + assert!(command.is_ok()); + + let command = command.unwrap(); + assert_eq!(command.name, name); + } +} diff --git a/src/models/personal_access_tokens/get_personal_access_tokens.rs b/src/models/personal_access_tokens/get_personal_access_tokens.rs new file mode 100644 index 0000000..586ab64 --- /dev/null +++ b/src/models/personal_access_tokens/get_personal_access_tokens.rs @@ -0,0 +1,66 @@ +use crate::infrastructure::error::Error; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::command::CommandPayload; +use crate::models::validatable::Validatable; +use bytes::Bytes; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; + +/// `GetPersonalAccessTokens` command is used to get all personal access tokens for the authenticated user. +/// It has no additional payload. +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct GetPersonalAccessTokens {} + +impl CommandPayload for GetPersonalAccessTokens {} + +impl Validatable for GetPersonalAccessTokens { + fn validate(&self) -> Result<(), Error> { + Ok(()) + } +} + +impl BytesSerializable for GetPersonalAccessTokens { + fn as_bytes(&self) -> Bytes { + Bytes::new() + } + + fn from_bytes(bytes: Bytes) -> std::result::Result { + if !bytes.is_empty() { + return Err(Error::InvalidCommand); + } + + let command = GetPersonalAccessTokens {}; + command.validate()?; + Ok(GetPersonalAccessTokens {}) + } +} + +impl Display for GetPersonalAccessTokens { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_be_serialized_as_empty_bytes() { + let command = GetPersonalAccessTokens {}; + let bytes = command.as_bytes(); + assert!(bytes.is_empty()); + } + + #[test] + fn should_be_deserialized_from_empty_bytes() { + let command = GetPersonalAccessTokens::from_bytes(Bytes::new()); + assert!(command.is_ok()); + } + + #[test] + fn should_not_be_deserialized_from_empty_bytes() { + let command = GetPersonalAccessTokens::from_bytes(Bytes::from_static(&[0])); + assert!(command.is_err()); + } +} diff --git a/src/models/personal_access_tokens/login_with_personal_access_token.rs b/src/models/personal_access_tokens/login_with_personal_access_token.rs new file mode 100644 index 0000000..e0303a3 --- /dev/null +++ b/src/models/personal_access_tokens/login_with_personal_access_token.rs @@ -0,0 +1,103 @@ +use crate::infrastructure::error::Error; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::command::CommandPayload; +use crate::models::users::defaults::*; +use crate::models::validatable::Validatable; +use bytes::{BufMut, Bytes, BytesMut}; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use std::str::from_utf8; + +/// `LoginWithPersonalAccessToken` command is used to login the user with a personal access token, instead of the username and password. +/// It has additional payload: +/// - `token` - personal access token +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct LoginWithPersonalAccessToken { + /// Personal access token + pub token: String, +} + +impl CommandPayload for LoginWithPersonalAccessToken {} + +impl Default for LoginWithPersonalAccessToken { + fn default() -> Self { + LoginWithPersonalAccessToken { + token: "token".to_string(), + } + } +} + +impl Validatable for LoginWithPersonalAccessToken { + fn validate(&self) -> Result<(), Error> { + if self.token.is_empty() || self.token.len() > MAX_PAT_LENGTH { + return Err(Error::InvalidPersonalAccessToken); + } + + Ok(()) + } +} + +impl BytesSerializable for LoginWithPersonalAccessToken { + fn as_bytes(&self) -> Bytes { + let mut bytes = BytesMut::with_capacity(5 + self.token.len()); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u8(self.token.len() as u8); + bytes.put_slice(self.token.as_bytes()); + bytes.freeze() + } + + fn from_bytes(bytes: Bytes) -> Result { + if bytes.len() < 4 { + return Err(Error::InvalidCommand); + } + + let token_length = bytes[0]; + let token = from_utf8(&bytes[1..1 + token_length as usize])?.to_string(); + if token.len() != token_length as usize { + return Err(Error::InvalidCommand); + } + + let command = LoginWithPersonalAccessToken { token }; + command.validate()?; + Ok(command) + } +} + +impl Display for LoginWithPersonalAccessToken { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.token) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_be_serialized_as_bytes() { + let command = LoginWithPersonalAccessToken { + token: "test".to_string(), + }; + + let bytes = command.as_bytes(); + let token_length = bytes[0]; + let token = from_utf8(&bytes[1..1 + token_length as usize]).unwrap(); + assert!(!bytes.is_empty()); + assert_eq!(token, command.token); + } + + #[test] + fn should_be_deserialized_from_bytes() { + let token = "test"; + let mut bytes = BytesMut::new(); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u8(token.len() as u8); + bytes.put_slice(token.as_bytes()); + + let command = LoginWithPersonalAccessToken::from_bytes(bytes.freeze()); + assert!(command.is_ok()); + + let command = command.unwrap(); + assert_eq!(command.token, token); + } +} diff --git a/src/models/personal_access_tokens/mod.rs b/src/models/personal_access_tokens/mod.rs new file mode 100644 index 0000000..cf974e7 --- /dev/null +++ b/src/models/personal_access_tokens/mod.rs @@ -0,0 +1,4 @@ +pub mod create_personal_access_token; +pub mod delete_personal_access_token; +pub mod get_personal_access_tokens; +pub mod login_with_personal_access_token; diff --git a/src/models/sizeable.rs b/src/models/sizeable.rs new file mode 100644 index 0000000..43114d7 --- /dev/null +++ b/src/models/sizeable.rs @@ -0,0 +1,4 @@ +/// Trait for types that return their size in bytes. +pub trait Sizeable { + fn get_size_bytes(&self) -> u32; +} diff --git a/src/models/stats.rs b/src/models/stats.rs new file mode 100644 index 0000000..7f45101 --- /dev/null +++ b/src/models/stats.rs @@ -0,0 +1,61 @@ +use serde::{Deserialize, Serialize}; + +use crate::utils::byte_size::IggyByteSize; + +/// `Stats` represents the statistics and details of the server and running process. +#[derive(Debug, Serialize, Deserialize)] +pub struct Stats { + /// The unique identifier of the process. + pub process_id: u32, + /// The CPU usage of the process. + pub cpu_usage: f32, + /// The memory usage of the process. + pub memory_usage: IggyByteSize, + /// The total memory of the system. + pub total_memory: IggyByteSize, + /// The available memory of the system. + pub available_memory: IggyByteSize, + /// The run time of the process. + pub run_time: u64, + /// The start time of the process. + pub start_time: u64, + /// The total number of bytes read. + pub read_bytes: IggyByteSize, + /// The total number of bytes written. + pub written_bytes: IggyByteSize, + // / The total size of the messages in bytes. + // pub messages_size_bytes: IggyByteSize, + // /// The total number of streams. + // pub streams_count: u32, + // /// The total number of topics. + // pub topics_count: u32, + // /// The total number of partitions. + // pub partitions_count: u32, + // /// The total number of segments. + // pub segments_count: u32, + // /// The total number of messages. + // pub messages_count: u64, + /// The total number of connected clients. + pub clients_count: u32, + // /// The total number of consumer groups. + // pub consumer_groups_count: u32, + /// The name of the host. + pub hostname: String, + /// The details of the operating system. + pub os_name: String, + /// The version of the operating system. + pub os_version: String, + /// The version of the kernel. + pub kernel_version: String, +} + +// use xitca_web::{error::Error, handler::Responder, http::WebResponse, WebContext}; + +// impl<'r, C, B> Responder> for Stats { +// type Response = WebResponse; +// type Error = Error; +// async fn respond(self, ctx: WebContext<'r, C, B>) -> Result { +// // logic for generating response from Stats +// todo!() +// } +// } diff --git a/src/models/system/get_client.rs b/src/models/system/get_client.rs new file mode 100644 index 0000000..993c233 --- /dev/null +++ b/src/models/system/get_client.rs @@ -0,0 +1,87 @@ +use crate::infrastructure::error::Error; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::command::CommandPayload; +use crate::models::validatable::Validatable; +use bytes::{BufMut, Bytes, BytesMut}; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; + +/// `GetClient` command is used to get the information about a specific client by unique ID. +/// It has additional payload: +/// - `client_id` - unique ID (numeric) of the client. +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct GetClient { + /// Unique ID (numeric) of the client. + pub client_id: u32, +} + +impl CommandPayload for GetClient {} + +impl Default for GetClient { + fn default() -> Self { + GetClient { client_id: 1 } + } +} + +impl Validatable for GetClient { + fn validate(&self) -> Result<(), Error> { + if self.client_id == 0 { + return Err(Error::InvalidClientId); + } + + Ok(()) + } +} + +impl BytesSerializable for GetClient { + fn as_bytes(&self) -> Bytes { + let mut bytes = BytesMut::with_capacity(4); + bytes.put_u32_le(self.client_id); + bytes.freeze() + } + + fn from_bytes(bytes: Bytes) -> Result { + if bytes.len() != 4 { + return Err(Error::InvalidCommand); + } + + let client_id = u32::from_le_bytes(bytes.as_ref().try_into()?); + let command = GetClient { client_id }; + command.validate()?; + Ok(command) + } +} + +impl Display for GetClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.client_id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_be_serialized_as_bytes() { + let command = GetClient { client_id: 1 }; + + let bytes = command.as_bytes(); + let client_id = u32::from_le_bytes(bytes[..4].try_into().unwrap()); + + assert!(!bytes.is_empty()); + assert_eq!(client_id, command.client_id); + } + + #[test] + fn should_be_deserialized_from_bytes() { + let client_id = 1u32; + let mut bytes = BytesMut::with_capacity(4); + bytes.put_u32_le(client_id); + let command = GetClient::from_bytes(bytes.freeze()); + assert!(command.is_ok()); + + let command = command.unwrap(); + assert_eq!(command.client_id, client_id); + } +} diff --git a/src/models/system/get_clients.rs b/src/models/system/get_clients.rs new file mode 100644 index 0000000..6900be5 --- /dev/null +++ b/src/models/system/get_clients.rs @@ -0,0 +1,66 @@ +use crate::infrastructure::error::Error; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::command::CommandPayload; +use crate::models::validatable::Validatable; +use bytes::Bytes; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; + +/// `GetClients` command is used to get the information about all connected clients. +/// It has no additional payload. +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct GetClients {} + +impl CommandPayload for GetClients {} + +impl Validatable for GetClients { + fn validate(&self) -> Result<(), Error> { + Ok(()) + } +} + +impl BytesSerializable for GetClients { + fn as_bytes(&self) -> Bytes { + Bytes::new() + } + + fn from_bytes(bytes: Bytes) -> Result { + if !bytes.is_empty() { + return Err(Error::InvalidCommand); + } + + let command = GetClients {}; + command.validate()?; + Ok(GetClients {}) + } +} + +impl Display for GetClients { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_be_serialized_as_empty_bytes() { + let command = GetClients {}; + let bytes = command.as_bytes(); + assert!(bytes.is_empty()); + } + + #[test] + fn should_be_deserialized_from_empty_bytes() { + let command = GetClients::from_bytes(Bytes::new()); + assert!(command.is_ok()); + } + + #[test] + fn should_not_be_deserialized_from_empty_bytes() { + let command = GetClients::from_bytes(Bytes::from_static(&[0])); + assert!(command.is_err()); + } +} diff --git a/src/models/system/get_me.rs b/src/models/system/get_me.rs new file mode 100644 index 0000000..36b5c84 --- /dev/null +++ b/src/models/system/get_me.rs @@ -0,0 +1,66 @@ +use crate::infrastructure::error::Error; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::command::CommandPayload; +use crate::models::validatable::Validatable; +use bytes::Bytes; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; + +/// `GetMe` command is used to get the information connected client. +/// It has no additional payload. +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct GetMe {} + +impl CommandPayload for GetMe {} + +impl Validatable for GetMe { + fn validate(&self) -> Result<(), Error> { + Ok(()) + } +} + +impl BytesSerializable for GetMe { + fn as_bytes(&self) -> Bytes { + Bytes::new() + } + + fn from_bytes(bytes: Bytes) -> Result { + if !bytes.is_empty() { + return Err(Error::InvalidCommand); + } + + let command = GetMe {}; + command.validate()?; + Ok(GetMe {}) + } +} + +impl Display for GetMe { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_be_serialized_as_empty_bytes() { + let command = GetMe {}; + let bytes = command.as_bytes(); + assert!(bytes.is_empty()); + } + + #[test] + fn should_be_deserialized_from_empty_bytes() { + let command = GetMe::from_bytes(Bytes::new()); + assert!(command.is_ok()); + } + + #[test] + fn should_not_be_deserialized_from_empty_bytes() { + let command = GetMe::from_bytes(Bytes::from_static(&[0])); + assert!(command.is_err()); + } +} diff --git a/src/models/system/get_stats.rs b/src/models/system/get_stats.rs new file mode 100644 index 0000000..014e8ce --- /dev/null +++ b/src/models/system/get_stats.rs @@ -0,0 +1,66 @@ +use crate::infrastructure::error::Error; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::command::CommandPayload; +use crate::models::validatable::Validatable; +use bytes::Bytes; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; + +/// `GetStats` command is used to get the statistics about the system. +/// It has no additional payload. +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct GetStats {} + +impl CommandPayload for GetStats {} + +impl Validatable for GetStats { + fn validate(&self) -> Result<(), Error> { + Ok(()) + } +} + +impl BytesSerializable for GetStats { + fn as_bytes(&self) -> Bytes { + Bytes::new() + } + + fn from_bytes(bytes: Bytes) -> Result { + if !bytes.is_empty() { + return Err(Error::InvalidCommand); + } + + let command = GetStats {}; + command.validate()?; + Ok(GetStats {}) + } +} + +impl Display for GetStats { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_be_serialized_as_empty_bytes() { + let command = GetStats {}; + let bytes = command.as_bytes(); + assert!(bytes.is_empty()); + } + + #[test] + fn should_be_deserialized_from_empty_bytes() { + let command = GetStats::from_bytes(Bytes::new()); + assert!(command.is_ok()); + } + + #[test] + fn should_not_be_deserialized_from_empty_bytes() { + let command = GetStats::from_bytes(Bytes::from_static(&[0])); + assert!(command.is_err()); + } +} diff --git a/src/models/system/mod.rs b/src/models/system/mod.rs new file mode 100644 index 0000000..193d41b --- /dev/null +++ b/src/models/system/mod.rs @@ -0,0 +1,5 @@ +pub mod get_client; +pub mod get_clients; +pub mod get_me; +pub mod get_stats; +pub mod ping; diff --git a/src/models/system/ping.rs b/src/models/system/ping.rs new file mode 100644 index 0000000..b4d39ec --- /dev/null +++ b/src/models/system/ping.rs @@ -0,0 +1,67 @@ +use crate::infrastructure::error::Error; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::command::CommandPayload; +use crate::models::validatable::Validatable; +use bytes::Bytes; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; + +/// `Ping` command is used to check if the server is alive. +/// It has no additional payload. +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct Ping {} + +impl CommandPayload for Ping {} + +impl Validatable for Ping { + fn validate(&self) -> Result<(), Error> { + Ok(()) + } +} + +impl BytesSerializable for Ping { + fn as_bytes(&self) -> Bytes { + Bytes::new() + } + + fn from_bytes(bytes: Bytes) -> Result { + if !bytes.is_empty() { + tracing::warn!("Here7c1a >>>>>>>EMPTY"); + return Err(Error::InvalidCommand); + } + + let command = Ping {}; + command.validate()?; + Ok(command) + } +} + +impl Display for Ping { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_be_serialized_as_empty_bytes() { + let command = Ping {}; + let bytes = command.as_bytes(); + assert!(bytes.is_empty()); + } + + #[test] + fn should_be_deserialized_from_empty_bytes() { + let command = Ping::from_bytes(Bytes::new()); + assert!(command.is_ok()); + } + + #[test] + fn should_not_be_deserialized_from_empty_bytes() { + let command = Ping::from_bytes(Bytes::from_static(&[0])); + assert!(command.is_err()); + } +} diff --git a/src/models/tcp/client.rs b/src/models/tcp/client.rs new file mode 100644 index 0000000..7277469 --- /dev/null +++ b/src/models/tcp/client.rs @@ -0,0 +1,325 @@ +use crate::infrastructure::error::Error; +use crate::models::binary::binary_client::{BinaryClient, ClientState}; +use crate::models::client::Client; +// use crate::tcp::config::TcpClientConfig; +use async_trait::async_trait; +use bytes::{BufMut, Bytes, BytesMut}; +use std::fmt::Debug; +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Duration; +use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader, BufWriter}; +use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; +use tokio::net::TcpStream; +use tokio::sync::Mutex; +use tokio::time::sleep; +use tokio_native_tls::native_tls::TlsConnector; +use tokio_native_tls::TlsStream; +use tracing::log::trace; +use tracing::{error, info}; + +use super::config::TcpClientConfig; + +const REQUEST_INITIAL_BYTES_LENGTH: usize = 4; +const RESPONSE_INITIAL_BYTES_LENGTH: usize = 8; +const NAME: &str = "Iggy"; + +/// TCP client for interacting with the Iggy API. +/// It requires a valid server address. +#[derive(Debug)] +pub struct TcpClient { + pub(crate) server_address: SocketAddr, + pub(crate) stream: Mutex>>, + pub(crate) config: Arc, + pub(crate) state: Mutex, +} + +unsafe impl Send for TcpClient {} +unsafe impl Sync for TcpClient {} + +#[async_trait] +pub(crate) trait ConnectionStream: Debug + Sync + Send { + async fn read(&mut self, buf: &mut [u8]) -> Result; + async fn write(&mut self, buf: &[u8]) -> Result<(), Error>; + async fn flush(&mut self) -> Result<(), Error>; +} + +#[derive(Debug)] +struct TcpConnectionStream { + reader: BufReader, + writer: BufWriter, +} + +impl TcpConnectionStream { + pub fn new(stream: TcpStream) -> Self { + let (reader, writer) = stream.into_split(); + Self { + reader: BufReader::new(reader), + writer: BufWriter::new(writer), + } + } +} + +#[derive(Debug)] +struct TcpTlsConnectionStream { + stream: TlsStream, +} + +unsafe impl Send for TcpConnectionStream {} +unsafe impl Sync for TcpConnectionStream {} + +unsafe impl Send for TcpTlsConnectionStream {} +unsafe impl Sync for TcpTlsConnectionStream {} + +#[async_trait] +impl ConnectionStream for TcpConnectionStream { + async fn read(&mut self, buf: &mut [u8]) -> Result { + let result = self.reader.read_exact(buf).await; + if let Err(error) = result { + return Err(Error::from(error)); + } + + Ok(result.unwrap()) + } + + async fn write(&mut self, buf: &[u8]) -> Result<(), Error> { + Ok(self.writer.write_all(buf).await?) + } + + async fn flush(&mut self) -> Result<(), Error> { + Ok(self.writer.flush().await?) + } +} + +#[async_trait] +impl ConnectionStream for TcpTlsConnectionStream { + async fn read(&mut self, buf: &mut [u8]) -> Result { + let result = self.stream.read_exact(buf).await; + if let Err(error) = result { + return Err(Error::from(error)); + } + + Ok(result.unwrap()) + } + + async fn write(&mut self, buf: &[u8]) -> Result<(), Error> { + let result = self.stream.write_all(buf).await; + if let Err(error) = result { + return Err(Error::from(error)); + } + + Ok(()) + } + + async fn flush(&mut self) -> Result<(), Error> { + Ok(()) + } +} + +impl Default for TcpClient { + fn default() -> Self { + TcpClient::create(Arc::new(TcpClientConfig::default())).unwrap() + } +} + +#[async_trait] +impl Client for TcpClient { + async fn connect(&self) -> Result<(), Error> { + if self.get_state().await == ClientState::Connected { + return Ok(()); + } + + let tls_enabled = self.config.tls_enabled; + let mut retry_count = 0; + let connection_stream: Box; + let remote_address; + loop { + info!( + "{} client is connecting to server: {}...", + NAME, self.config.server_address + ); + + let connection = TcpStream::connect(self.server_address).await; + if connection.is_err() { + error!( + "Failed to connect to server: {}", + self.config.server_address + ); + if retry_count < self.config.reconnection_retries { + retry_count += 1; + info!( + "Retrying to connect to server ({}/{}): {} in: {} ms...", + retry_count, + self.config.reconnection_retries, + self.config.server_address, + self.config.reconnection_interval + ); + sleep(Duration::from_millis(self.config.reconnection_interval)).await; + continue; + } + + return Err(Error::NotConnected); + } + + let stream = connection.unwrap(); + remote_address = stream.peer_addr()?; + + if !tls_enabled { + connection_stream = Box::new(TcpConnectionStream::new(stream)); + break; + } + + let connector = + tokio_native_tls::TlsConnector::from(TlsConnector::builder().build().unwrap()); + let stream = tokio_native_tls::TlsConnector::connect( + &connector, + &self.config.tls_domain, + stream, + ) + .await + .unwrap(); + connection_stream = Box::new(TcpTlsConnectionStream { stream }); + break; + } + + self.stream.lock().await.replace(connection_stream); + self.set_state(ClientState::Connected).await; + + info!( + "{} client has connected to server: {}", + NAME, remote_address + ); + + Ok(()) + } + + async fn disconnect(&self) -> Result<(), Error> { + if self.get_state().await == ClientState::Disconnected { + return Ok(()); + } + + info!("{} client is disconnecting from server...", NAME); + self.set_state(ClientState::Disconnected).await; + self.stream.lock().await.take(); + info!("{} client has disconnected from server.", NAME); + Ok(()) + } +} + +#[async_trait] +impl BinaryClient for TcpClient { + async fn get_state(&self) -> ClientState { + *self.state.lock().await + } + + async fn set_state(&self, state: ClientState) { + *self.state.lock().await = state; + } + + async fn send_with_response(&self, command: u32, payload: Bytes) -> Result { + if self.get_state().await == ClientState::Disconnected { + return Err(Error::NotConnected); + } + + let mut stream = self.stream.lock().await; + if let Some(stream) = stream.as_mut() { + let payload_length = payload.len() + REQUEST_INITIAL_BYTES_LENGTH; + trace!("Sending a TCP request..."); + stream.write(&(payload_length as u32).to_le_bytes()).await?; + stream.write(&command.to_le_bytes()).await?; + stream.write(&payload).await?; + stream.flush().await?; + trace!("Sent a TCP request, waiting for a response..."); + + let mut response_buffer = [0u8; RESPONSE_INITIAL_BYTES_LENGTH]; + let read_bytes = stream.read(&mut response_buffer).await?; + if read_bytes != RESPONSE_INITIAL_BYTES_LENGTH { + error!("Received an invalid or empty response."); + return Err(Error::EmptyResponse); + } + + let status = u32::from_le_bytes(response_buffer[..4].try_into().unwrap()); + let length = u32::from_le_bytes(response_buffer[4..].try_into().unwrap()); + return self.handle_response(status, length, stream.as_mut()).await; + } + + error!("Cannot send data. Client is not connected."); + Err(Error::NotConnected) + } +} + +impl TcpClient { + /// Create a new TCP client for the provided server address. + pub fn new(server_address: &str) -> Result { + Self::create(Arc::new(TcpClientConfig { + server_address: server_address.to_string(), + ..Default::default() + })) + } + + /// Create a new TCP client for the provided server address using TLS. + pub fn new_tls(server_address: &str, domain: &str) -> Result { + Self::create(Arc::new(TcpClientConfig { + server_address: server_address.to_string(), + tls_enabled: true, + tls_domain: domain.to_string(), + ..Default::default() + })) + } + + /// Create a new TCP client based on the provided configuration. + pub fn create(config: Arc) -> Result { + let server_address = config.server_address.parse::()?; + + Ok(Self { + config, + server_address, + stream: Mutex::new(None), + state: Mutex::new(ClientState::Disconnected), + }) + } + + async fn handle_response( + &self, + status: u32, + length: u32, + stream: &mut dyn ConnectionStream, + ) -> Result { + if status != 0 { + // TEMP: See https://github.com/iggy-rs/iggy/pull/604 for context. + // if status == Error::TopicIdAlreadyExists as u32 + // // || status == Error::TopicNameAlreadyExists as u32 + // // || status == Error::StreamIdAlreadyExists as u32 + // // || status == Error::StreamNameAlreadyExists as u32 + // || status == Error::UserAlreadyExists as u32 + // // || status == Error::PersonalAccessTokenAlreadyExists as u32 + // // || status == Error::ConsumerGroupIdAlreadyExists as u32 + // // || status == Error::ConsumerGroupNameAlreadyExists as u32 + // { + // tracing::debug!( + // "Received a server resource already exists response: {} ({})", + // status, + // Error::from_code_as_string(status) + // ) + // } else { + // error!( + // "Received an invalid response with status: {} ({}).", + // status, + // Error::from_code_as_string(status) + // ); + // } + + return Err(Error::InvalidResponse(status)); + } + + trace!("Status: OK. Response length: {}", length); + if length <= 1 { + return Ok(Bytes::new()); + } + + let mut response_buffer = BytesMut::with_capacity(length as usize); + response_buffer.put_bytes(0, length as usize); + stream.read(&mut response_buffer).await?; + Ok(response_buffer.freeze()) + } +} diff --git a/src/models/tcp/config.rs b/src/models/tcp/config.rs new file mode 100644 index 0000000..1de1968 --- /dev/null +++ b/src/models/tcp/config.rs @@ -0,0 +1,26 @@ +/// Configuration for the TCP client. +#[derive(Debug, Clone)] +pub struct TcpClientConfig { + /// The address of the Iggy server. + pub server_address: String, + /// The number of retries when connecting to the server. + pub reconnection_retries: u32, + /// The interval between retries when connecting to the server. + pub reconnection_interval: u64, + /// Whether to use TLS when connecting to the server. + pub tls_enabled: bool, + /// The domain to use for TLS when connecting to the server. + pub tls_domain: String, +} + +impl Default for TcpClientConfig { + fn default() -> TcpClientConfig { + TcpClientConfig { + server_address: "127.0.0.1:8090".to_string(), + reconnection_retries: 3, + reconnection_interval: 1000, + tls_enabled: false, + tls_domain: "localhost".to_string(), + } + } +} diff --git a/src/models/tcp/mod.rs b/src/models/tcp/mod.rs new file mode 100644 index 0000000..dbdd363 --- /dev/null +++ b/src/models/tcp/mod.rs @@ -0,0 +1,11 @@ +pub mod client; +pub mod config; +// pub mod consumer_groups; +// pub mod consumer_offsets; +// pub mod messages; +// pub mod partitions; +// pub mod personal_access_tokens; +// pub mod streams; +// pub mod system; +// pub mod topics; +// pub mod users; diff --git a/src/models/tcp/personal_access_tokens.rs b/src/models/tcp/personal_access_tokens.rs new file mode 100644 index 0000000..492b702 --- /dev/null +++ b/src/models/tcp/personal_access_tokens.rs @@ -0,0 +1,42 @@ +use crate::models::binary; +use crate::models::client::PersonalAccessTokenClient; +use crate::infrastructure::error::Error; +use crate::models::identity_info::IdentityInfo; +use crate::models::personal_access_token::{PersonalAccessTokenInfo, RawPersonalAccessToken}; +use crate::models::personal_access_tokens::create_personal_access_token::CreatePersonalAccessToken; +use crate::models::personal_access_tokens::delete_personal_access_token::DeletePersonalAccessToken; +use crate::models::personal_access_tokens::get_personal_access_tokens::GetPersonalAccessTokens; +use crate::models::personal_access_tokens::login_with_personal_access_token::LoginWithPersonalAccessToken; +use crate::models::tcp::client::TcpClient; +use async_trait::async_trait; + +#[async_trait] +impl PersonalAccessTokenClient for TcpClient { + async fn get_personal_access_tokens( + &self, + command: &GetPersonalAccessTokens, + ) -> Result, Error> { + binary::personal_access_tokens::get_personal_access_tokens(self, command).await + } + + async fn create_personal_access_token( + &self, + command: &CreatePersonalAccessToken, + ) -> Result { + binary::personal_access_tokens::create_personal_access_token(self, command).await + } + + async fn delete_personal_access_token( + &self, + command: &DeletePersonalAccessToken, + ) -> Result<(), Error> { + binary::personal_access_tokens::delete_personal_access_token(self, command).await + } + + async fn login_with_personal_access_token( + &self, + command: &LoginWithPersonalAccessToken, + ) -> Result { + binary::personal_access_tokens::login_with_personal_access_token(self, command).await + } +} diff --git a/src/models/tcp/system.rs b/src/models/tcp/system.rs new file mode 100644 index 0000000..0d12098 --- /dev/null +++ b/src/models/tcp/system.rs @@ -0,0 +1,35 @@ +use crate::models::binary; +use crate::models::client::SystemClient; +use crate::infrastructure::error::Error; +use crate::models::client_info::{ClientInfo, ClientInfoDetails}; +use crate::models::stats::Stats; +use crate::models::system::get_client::GetClient; +use crate::models::system::get_clients::GetClients; +use crate::models::system::get_me::GetMe; +use crate::models::system::get_stats::GetStats; +use crate::models::system::ping::Ping; +use crate::models::tcp::client::TcpClient; +use async_trait::async_trait; + +#[async_trait] +impl SystemClient for TcpClient { + async fn get_stats(&self, command: &GetStats) -> Result { + binary::system::get_stats(self, command).await + } + + async fn get_me(&self, command: &GetMe) -> Result { + binary::system::get_me(self, command).await + } + + async fn get_client(&self, command: &GetClient) -> Result { + binary::system::get_client(self, command).await + } + + async fn get_clients(&self, command: &GetClients) -> Result, Error> { + binary::system::get_clients(self, command).await + } + + async fn ping(&self, command: &Ping) -> Result<(), Error> { + binary::system::ping(self, command).await + } +} diff --git a/src/models/tcp/users.rs b/src/models/tcp/users.rs new file mode 100644 index 0000000..d63db53 --- /dev/null +++ b/src/models/tcp/users.rs @@ -0,0 +1,55 @@ +use crate::infrastructure::error::Error; +use crate::models::binary; +use crate::models::client::UserClient; +use crate::models::identity_info::IdentityInfo; +use crate::models::tcp::client::TcpClient; +use crate::models::user_info::{UserInfo, UserInfoDetails}; +use crate::models::users::change_password::ChangePassword; +use crate::models::users::create_user::CreateUser; +use crate::models::users::delete_user::DeleteUser; +use crate::models::users::get_user::GetUser; +use crate::models::users::get_users::GetUsers; +use crate::models::users::login_user::LoginUser; +use crate::models::users::logout_user::LogoutUser; +use crate::models::users::update_permissions::UpdatePermissions; +use crate::models::users::update_user::UpdateUser; +use async_trait::async_trait; + +#[async_trait] +impl UserClient for TcpClient { + async fn get_user(&self, command: &GetUser) -> Result { + binary::users::get_user(self, command).await + } + + async fn get_users(&self, command: &GetUsers) -> Result, Error> { + binary::users::get_users(self, command).await + } + + async fn create_user(&self, command: &CreateUser) -> Result<(), Error> { + binary::users::create_user(self, command).await + } + + async fn delete_user(&self, command: &DeleteUser) -> Result<(), Error> { + binary::users::delete_user(self, command).await + } + + async fn update_user(&self, command: &UpdateUser) -> Result<(), Error> { + binary::users::update_user(self, command).await + } + + async fn update_permissions(&self, command: &UpdatePermissions) -> Result<(), Error> { + binary::users::update_permissions(self, command).await + } + + async fn change_password(&self, command: &ChangePassword) -> Result<(), Error> { + binary::users::change_password(self, command).await + } + + async fn login_user(&self, command: &LoginUser) -> Result { + binary::users::login_user(self, command).await + } + + async fn logout_user(&self, command: &LogoutUser) -> Result<(), Error> { + binary::users::logout_user(self, command).await + } +} diff --git a/src/models/user_info.rs b/src/models/user_info.rs new file mode 100644 index 0000000..aae53de --- /dev/null +++ b/src/models/user_info.rs @@ -0,0 +1,50 @@ +use crate::models::permissions::Permissions; +use crate::models::user_status::UserStatus; +use serde::{Deserialize, Serialize}; + +use std::sync::atomic::AtomicU32; + +/// `UserId` represents the unique identifier (numeric) of the user. +pub type UserId = u32; +/// `AtomicUserId` represents the unique identifier (numeric) of the user +/// which can be safely modified concurrently across threads +pub type AtomicUserId = AtomicU32; + +/// `UserInfo` represents the basic information about the user. +/// It consists of the following fields: +/// - `id`: the unique identifier (numeric) of the user. +/// - `created_at`: the timestamp when the user was created. +/// - `status`: the status of the user. +/// - `username`: the username of the user. +#[derive(Debug, Serialize, Deserialize)] +pub struct UserInfo { + /// The unique identifier (numeric) of the user. + pub id: UserId, + /// The timestamp when the user was created. + pub created_at: u64, + /// The status of the user. + pub status: UserStatus, + /// The username of the user. + pub username: String, +} + +/// `UserInfoDetails` represents the detailed information about the user. +/// It consists of the following fields: +/// - `id`: the unique identifier (numeric) of the user. +/// - `created_at`: the timestamp when the user was created. +/// - `status`: the status of the user. +/// - `username`: the username of the user. +/// - `permissions`: the optional permissions of the user. +#[derive(Debug, Serialize, Deserialize)] +pub struct UserInfoDetails { + /// The unique identifier (numeric) of the user. + pub id: UserId, + /// The timestamp when the user was created. + pub created_at: u64, + /// The status of the user. + pub status: UserStatus, + /// The username of the user. + pub username: String, + /// The optional permissions of the user. + pub permissions: Option, +} diff --git a/src/models/user_status.rs b/src/models/user_status.rs new file mode 100644 index 0000000..b7bfc6d --- /dev/null +++ b/src/models/user_status.rs @@ -0,0 +1,54 @@ +use crate::infrastructure::error::Error; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use std::str::FromStr; + +/// `UserStatus` represents the status of the user. +#[derive(Debug, Serialize, Deserialize, PartialEq, Default, Clone, Copy)] +#[serde(rename_all = "snake_case")] +pub enum UserStatus { + /// The user is active. + #[default] + Active, + /// The user is inactive. + Inactive, +} + +impl FromStr for UserStatus { + type Err = Error; + fn from_str(input: &str) -> Result { + match input { + "active" => Ok(UserStatus::Active), + "inactive" => Ok(UserStatus::Inactive), + _ => Err(Error::InvalidUserStatus), + } + } +} + +impl Display for UserStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UserStatus::Active => write!(f, "active"), + UserStatus::Inactive => write!(f, "inactive"), + } + } +} + +impl UserStatus { + /// Returns the code of the user status. + pub fn as_code(&self) -> u8 { + match self { + UserStatus::Active => 1, + UserStatus::Inactive => 2, + } + } + + /// Returns the user status from the code. + pub fn from_code(code: u8) -> Result { + match code { + 1 => Ok(UserStatus::Active), + 2 => Ok(UserStatus::Inactive), + _ => Err(Error::InvalidCommand), + } + } +} diff --git a/src/models/users/change_password.rs b/src/models/users/change_password.rs new file mode 100644 index 0000000..b27e72e --- /dev/null +++ b/src/models/users/change_password.rs @@ -0,0 +1,164 @@ +use crate::infrastructure::error::Error; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::command::CommandPayload; +use crate::models::identifier::Identifier; +use crate::models::users::defaults::*; +use crate::models::validatable::Validatable; +use bytes::{BufMut, Bytes, BytesMut}; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use std::str::from_utf8; + +/// `ChangePassword` command is used to change a user's password. +/// It has additional payload: +/// - `user_id` - unique user ID (numeric or name). +/// - `current_password` - current password, must be between 3 and 100 characters long. +/// - `new_password` - new password, must be between 3 and 100 characters long. +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct ChangePassword { + /// Unique user ID (numeric or name). + #[serde(skip)] + pub user_id: Identifier, + /// Current password, must be between 3 and 100 characters long. + pub current_password: String, + /// New password, must be between 3 and 100 characters long. + pub new_password: String, +} + +impl CommandPayload for ChangePassword {} + +impl Default for ChangePassword { + fn default() -> Self { + ChangePassword { + user_id: Identifier::default(), + current_password: "secret".to_string(), + new_password: "topsecret".to_string(), + } + } +} + +impl Validatable for ChangePassword { + fn validate(&self) -> Result<(), Error> { + if self.current_password.is_empty() + || self.current_password.len() > MAX_PASSWORD_LENGTH + || self.current_password.len() < MIN_PASSWORD_LENGTH + { + return Err(Error::InvalidPassword); + } + + if self.new_password.is_empty() + || self.new_password.len() > MAX_PASSWORD_LENGTH + || self.new_password.len() < MIN_PASSWORD_LENGTH + { + return Err(Error::InvalidPassword); + } + + Ok(()) + } +} + +impl BytesSerializable for ChangePassword { + fn as_bytes(&self) -> Bytes { + let user_id_bytes = self.user_id.as_bytes(); + let mut bytes = BytesMut::new(); + bytes.put_slice(&user_id_bytes); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u8(self.current_password.len() as u8); + bytes.put_slice(self.current_password.as_bytes()); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u8(self.new_password.len() as u8); + bytes.put_slice(self.new_password.as_bytes()); + bytes.freeze() + } + + fn from_bytes(bytes: Bytes) -> Result { + if bytes.len() < 9 { + return Err(Error::InvalidCommand); + } + + let user_id = Identifier::from_bytes(bytes.clone())?; + let mut position = user_id.get_size_bytes() as usize; + let current_password_length = bytes[position]; + position += 1; + let current_password = + from_utf8(&bytes[position..position + current_password_length as usize])?.to_string(); + position += current_password_length as usize; + let new_password_length = bytes[position]; + position += 1; + let new_password = + from_utf8(&bytes[position..position + new_password_length as usize])?.to_string(); + + let command = ChangePassword { + user_id, + current_password, + new_password, + }; + command.validate()?; + Ok(command) + } +} + +impl Display for ChangePassword { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}|{}|{}", + self.user_id, self.current_password, self.new_password + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_be_serialized_as_bytes() { + let command = ChangePassword { + user_id: Identifier::numeric(1).unwrap(), + current_password: "user".to_string(), + new_password: "secret".to_string(), + }; + + let bytes = command.as_bytes(); + let user_id = Identifier::from_bytes(bytes.clone()).unwrap(); + let mut position = user_id.get_size_bytes() as usize; + let current_password_length = bytes[position]; + position += 1; + let current_password = + from_utf8(&bytes[position..position + current_password_length as usize]).unwrap(); + position += current_password_length as usize; + let new_password_length = bytes[position]; + position += 1; + let new_password = + from_utf8(&bytes[position..position + new_password_length as usize]).unwrap(); + + assert!(!bytes.is_empty()); + assert_eq!(user_id, command.user_id); + assert_eq!(current_password, command.current_password); + assert_eq!(new_password, command.new_password); + } + + #[test] + fn should_be_deserialized_from_bytes() { + let user_id = Identifier::numeric(1).unwrap(); + let current_password = "secret"; + let new_password = "topsecret"; + let mut bytes = BytesMut::new(); + bytes.put_slice(&user_id.as_bytes()); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u8(current_password.len() as u8); + bytes.put_slice(current_password.as_bytes()); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u8(new_password.len() as u8); + bytes.put_slice(new_password.as_bytes()); + + let command = ChangePassword::from_bytes(bytes.freeze()); + assert!(command.is_ok()); + + let command = command.unwrap(); + assert_eq!(command.user_id, user_id); + assert_eq!(command.current_password, current_password); + assert_eq!(command.new_password, new_password); + } +} diff --git a/src/models/users/create_user.rs b/src/models/users/create_user.rs new file mode 100644 index 0000000..caa2a98 --- /dev/null +++ b/src/models/users/create_user.rs @@ -0,0 +1,257 @@ +use crate::infrastructure::error::Error; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::command::CommandPayload; +use crate::models::permissions::Permissions; +use crate::models::user_status::UserStatus; +use crate::models::users::defaults::*; +use crate::models::validatable::Validatable; +use crate::utils::text; +use bytes::{BufMut, Bytes, BytesMut}; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use std::str::from_utf8; + +/// `CreateUser` command is used to create a new user. +/// It has additional payload: +/// - `username` - unique name of the user, must be between 3 and 50 characters long. The name will be always converted to lowercase and all whitespaces will be replaced with dots. +/// - `password` - password of the user, must be between 3 and 100 characters long. +/// - `status` - status of the user, can be either `active` or `inactive`. +/// - `permissions` - optional permissions of the user. If not provided, user will have no permissions. +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct CreateUser { + /// Unique name of the user, must be between 3 and 50 characters long. + pub username: String, + /// Password of the user, must be between 3 and 100 characters long. + pub password: String, + /// Status of the user, can be either `active` or `inactive`. + pub status: UserStatus, + /// Optional permissions of the user. If not provided, user will have no permissions. + pub permissions: Option, +} + +impl CommandPayload for CreateUser {} + +impl Default for CreateUser { + fn default() -> Self { + CreateUser { + username: "user".to_string(), + password: "secret".to_string(), + status: UserStatus::Active, + permissions: None, + } + } +} + +impl Validatable for CreateUser { + fn validate(&self) -> Result<(), Error> { + if self.username.is_empty() + || self.username.len() > MAX_USERNAME_LENGTH + || self.username.len() < MIN_USERNAME_LENGTH + { + return Err(Error::InvalidUsername); + } + + if !text::is_resource_name_valid(&self.username) { + return Err(Error::InvalidUsername); + } + + if self.password.is_empty() + || self.password.len() > MAX_PASSWORD_LENGTH + || self.password.len() < MIN_PASSWORD_LENGTH + { + return Err(Error::InvalidPassword); + } + + Ok(()) + } +} + +impl BytesSerializable for CreateUser { + fn as_bytes(&self) -> Bytes { + let mut bytes = BytesMut::with_capacity(2 + self.username.len() + self.password.len()); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u8(self.username.len() as u8); + bytes.put_slice(self.username.as_bytes()); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u8(self.password.len() as u8); + bytes.put_slice(self.password.as_bytes()); + bytes.put_u8(self.status.as_code()); + if let Some(permissions) = &self.permissions { + bytes.put_u8(1); + let permissions = permissions.as_bytes(); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u32_le(permissions.len() as u32); + bytes.put_slice(&permissions); + } else { + bytes.put_u8(0); + } + bytes.freeze() + } + + fn from_bytes(bytes: Bytes) -> Result { + if bytes.len() < 10 { + return Err(Error::InvalidCommand); + } + + let username_length = bytes[0]; + let username = from_utf8(&bytes[1..1 + username_length as usize])?.to_string(); + if username.len() != username_length as usize { + return Err(Error::InvalidCommand); + } + + let mut position = 1 + username_length as usize; + let password_length = bytes[position]; + position += 1; + let password = + from_utf8(&bytes[position..position + password_length as usize])?.to_string(); + if password.len() != password_length as usize { + return Err(Error::InvalidCommand); + } + + position += password_length as usize; + let status = UserStatus::from_code(bytes[position])?; + position += 1; + let has_permissions = bytes[position]; + if has_permissions > 1 { + return Err(Error::InvalidCommand); + } + + position += 1; + let permissions = if has_permissions == 1 { + let permissions_length = u32::from_le_bytes(bytes[position..position + 4].try_into()?); + position += 4; + Some(Permissions::from_bytes( + bytes.slice(position..position + permissions_length as usize), + )?) + } else { + None + }; + + let command = CreateUser { + username, + password, + status, + permissions, + }; + command.validate()?; + Ok(command) + } +} + +impl Display for CreateUser { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let permissions = if let Some(permissions) = &self.permissions { + permissions.to_string() + } else { + "no_permissions".to_string() + }; + write!( + f, + "{}|{}|{}|{}", + self.username, self.password, self.status, permissions + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::permissions::GlobalPermissions; + + #[test] + fn should_be_serialized_as_bytes() { + let command = CreateUser { + username: "user".to_string(), + password: "secret".to_string(), + status: UserStatus::Active, + permissions: Some(Permissions { + global: GlobalPermissions { + manage_servers: false, + read_servers: true, + manage_users: false, + read_users: true, + manage_streams: false, + read_streams: true, + manage_topics: false, + read_topics: true, + poll_messages: true, + send_messages: true, + }, + streams: None, + }), + }; + + let bytes = command.as_bytes(); + let username_length = bytes[0]; + let username = from_utf8(&bytes[1..1 + username_length as usize]).unwrap(); + let mut position = 1 + username_length as usize; + let password_length = bytes[position]; + position += 1; + let password = from_utf8(&bytes[position..position + password_length as usize]).unwrap(); + position += password_length as usize; + let status = UserStatus::from_code(bytes[position]).unwrap(); + position += 1; + let has_permissions = bytes[3 + username_length as usize + password_length as usize]; + position += 1; + + let permissions_length = + u32::from_le_bytes(bytes[position..position + 4].try_into().unwrap()); + position += 4; + let permissions = + Permissions::from_bytes(bytes.slice(position..position + permissions_length as usize)) + .unwrap(); + + assert!(!bytes.is_empty()); + assert_eq!(username, command.username); + assert_eq!(password, command.password); + assert_eq!(status, command.status); + assert_eq!(has_permissions, 1); + assert_eq!(permissions, command.permissions.unwrap()); + } + + #[test] + fn should_be_deserialized_from_bytes() { + let username = "user"; + let password = "secret"; + let status = UserStatus::Active; + let has_permissions = 1u8; + let permissions = Permissions { + global: GlobalPermissions { + manage_servers: false, + read_servers: true, + manage_users: false, + read_users: true, + manage_streams: false, + read_streams: true, + manage_topics: false, + read_topics: true, + poll_messages: true, + send_messages: true, + }, + streams: None, + }; + let mut bytes = BytesMut::new(); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u8(username.len() as u8); + bytes.put_slice(username.as_bytes()); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u8(password.len() as u8); + bytes.put_slice(password.as_bytes()); + bytes.put_u8(status.as_code()); + bytes.put_u8(has_permissions); + let permissions_bytes = permissions.as_bytes(); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u32_le(permissions_bytes.len() as u32); + bytes.put_slice(&permissions_bytes); + + let command = CreateUser::from_bytes(bytes.freeze()); + assert!(command.is_ok()); + + let command = command.unwrap(); + assert_eq!(command.username, username); + assert_eq!(command.password, password); + assert_eq!(command.status, status); + assert!(command.permissions.is_some()); + assert_eq!(command.permissions.unwrap(), permissions); + } +} diff --git a/src/models/users/defaults.rs b/src/models/users/defaults.rs new file mode 100644 index 0000000..28047ee --- /dev/null +++ b/src/models/users/defaults.rs @@ -0,0 +1,10 @@ +pub const MAX_USERNAME_LENGTH: usize = 50; +pub const MIN_USERNAME_LENGTH: usize = 3; +pub const MAX_PASSWORD_LENGTH: usize = 100; +pub const MIN_PASSWORD_LENGTH: usize = 3; +pub const MAX_PAT_LENGTH: usize = 100; +pub const MAX_PERSONAL_ACCESS_TOKEN_NAME_LENGTH: usize = 30; +pub const MIN_PERSONAL_ACCESS_TOKEN_NAME_LENGTH: usize = 3; +pub const DEFAULT_ROOT_USER_ID: u32 = 1; +pub const DEFAULT_ROOT_USERNAME: &str = "nigig"; +pub const DEFAULT_ROOT_PASSWORD: &str = "nigig"; diff --git a/src/models/users/delete_user.rs b/src/models/users/delete_user.rs new file mode 100644 index 0000000..502d2ac --- /dev/null +++ b/src/models/users/delete_user.rs @@ -0,0 +1,78 @@ +use crate::infrastructure::error::Error; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::command::CommandPayload; +use crate::models::identifier::Identifier; +use crate::models::validatable::Validatable; +use bytes::Bytes; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; + +/// `DeleteUser` command is used to delete a user by unique ID. +/// It has additional payload: +/// - `user_id` - unique user ID (numeric or name). +#[derive(Debug, Serialize, Deserialize, PartialEq, Default)] +pub struct DeleteUser { + /// Unique user ID (numeric or name). + #[serde(skip)] + pub user_id: Identifier, +} + +impl CommandPayload for DeleteUser {} + +impl Validatable for DeleteUser { + fn validate(&self) -> Result<(), Error> { + Ok(()) + } +} + +impl BytesSerializable for DeleteUser { + fn as_bytes(&self) -> Bytes { + self.user_id.as_bytes() + } + + fn from_bytes(bytes: Bytes) -> Result { + if bytes.len() < 3 { + return Err(Error::InvalidCommand); + } + + let user_id = Identifier::from_bytes(bytes)?; + let command = DeleteUser { user_id }; + command.validate()?; + Ok(command) + } +} + +impl Display for DeleteUser { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.user_id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_be_serialized_as_bytes() { + let command = DeleteUser { + user_id: Identifier::numeric(1).unwrap(), + }; + + let bytes = command.as_bytes(); + let user_id = Identifier::from_bytes(bytes.clone()).unwrap(); + + assert!(!bytes.is_empty()); + assert_eq!(user_id, command.user_id); + } + + #[test] + fn should_be_deserialized_from_bytes() { + let user_id = Identifier::numeric(1).unwrap(); + let bytes = user_id.as_bytes(); + let command = DeleteUser::from_bytes(bytes); + assert!(command.is_ok()); + + let command = command.unwrap(); + assert_eq!(command.user_id, user_id); + } +} diff --git a/src/models/users/get_user.rs b/src/models/users/get_user.rs new file mode 100644 index 0000000..5009db6 --- /dev/null +++ b/src/models/users/get_user.rs @@ -0,0 +1,78 @@ +use crate::infrastructure::error::Error; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::command::CommandPayload; +use crate::models::identifier::Identifier; +use crate::models::validatable::Validatable; +use bytes::Bytes; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; + +/// `GetUser` command is used to retrieve the information about a user by unique ID. +/// It has additional payload: +/// - `user_id` - unique user ID (numeric or name). +#[derive(Debug, Serialize, Deserialize, PartialEq, Default)] +pub struct GetUser { + #[serde(skip)] + /// Unique user ID (numeric or name). + pub user_id: Identifier, +} + +impl CommandPayload for GetUser {} + +impl Validatable for GetUser { + fn validate(&self) -> Result<(), Error> { + Ok(()) + } +} + +impl BytesSerializable for GetUser { + fn as_bytes(&self) -> Bytes { + self.user_id.as_bytes() + } + + fn from_bytes(bytes: Bytes) -> Result { + if bytes.len() < 3 { + return Err(Error::InvalidCommand); + } + + let user_id = Identifier::from_bytes(bytes)?; + let command = GetUser { user_id }; + command.validate()?; + Ok(command) + } +} + +impl Display for GetUser { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.user_id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_be_serialized_as_bytes() { + let command = GetUser { + user_id: Identifier::numeric(1).unwrap(), + }; + + let bytes = command.as_bytes(); + let user_id = Identifier::from_bytes(bytes.clone()).unwrap(); + + assert!(!bytes.is_empty()); + assert_eq!(user_id, command.user_id); + } + + #[test] + fn should_be_deserialized_from_bytes() { + let user_id = Identifier::numeric(1).unwrap(); + let bytes = user_id.as_bytes(); + let command = GetUser::from_bytes(bytes); + assert!(command.is_ok()); + + let command = command.unwrap(); + assert_eq!(command.user_id, user_id); + } +} diff --git a/src/models/users/get_users.rs b/src/models/users/get_users.rs new file mode 100644 index 0000000..2815e4c --- /dev/null +++ b/src/models/users/get_users.rs @@ -0,0 +1,68 @@ +use crate::infrastructure::error::Error; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::command::CommandPayload; +use crate::models::validatable::Validatable; +use bytes::Bytes; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; + +/// `GetUsers` command is used to retrieve the information about all users. +/// It has no additional payload. +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct GetUsers {} + +impl CommandPayload for GetUsers {} + +impl Validatable for GetUsers { + fn validate(&self) -> Result<(), Error> { + Ok(()) + } +} + +impl BytesSerializable for GetUsers { + fn as_bytes(&self) -> Bytes { + Bytes::new() + } + + fn from_bytes(bytes: Bytes) -> Result { + if !bytes.is_empty() { + return Err(Error::InvalidCommand); + } + + let command = GetUsers {}; + command.validate()?; + Ok(GetUsers {}) + } +} + +impl Display for GetUsers { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "") + } +} + +#[cfg(test)] +mod tests { + use bytes::Bytes; + + use super::*; + + #[test] + fn should_be_serialized_as_empty_bytes() { + let command = GetUsers {}; + let bytes = command.as_bytes(); + assert!(bytes.is_empty()); + } + + #[test] + fn should_be_deserialized_from_empty_bytes() { + let command = GetUsers::from_bytes(Bytes::new()); + assert!(command.is_ok()); + } + + #[test] + fn should_not_be_deserialized_from_empty_bytes() { + let command = GetUsers::from_bytes(Bytes::from_static(&[0])); + assert!(command.is_err()); + } +} diff --git a/src/models/users/login_user.rs b/src/models/users/login_user.rs new file mode 100644 index 0000000..4ddf2de --- /dev/null +++ b/src/models/users/login_user.rs @@ -0,0 +1,148 @@ +use crate::infrastructure::error::Error; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::command::CommandPayload; +use crate::models::users::defaults::*; +use crate::models::validatable::Validatable; +use crate::utils::text; +use bytes::{BufMut, Bytes, BytesMut}; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use std::str::from_utf8; + +/// `LoginUser` command is used to login a user by username and password. +/// It has additional payload: +/// - `username` - username, must be between 3 and 50 characters long. +/// - `password` - password, must be between 3 and 100 characters long. +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct LoginUser { + /// Username, must be between 3 and 50 characters long. + pub username: String, + /// Password, must be between 3 and 100 characters long. + pub password: String, +} + +impl CommandPayload for LoginUser {} + +impl Default for LoginUser { + fn default() -> Self { + LoginUser { + username: "user".to_string(), + password: "secret".to_string(), + } + } +} + +impl Validatable for LoginUser { + fn validate(&self) -> Result<(), Error> { + if self.username.is_empty() + || self.username.len() > MAX_USERNAME_LENGTH + || self.username.len() < MIN_USERNAME_LENGTH + { + return Err(Error::InvalidUsername); + } + + if !text::is_resource_name_valid(&self.username) { + return Err(Error::InvalidUsername); + } + + if self.password.is_empty() + || self.password.len() > MAX_PASSWORD_LENGTH + || self.password.len() < MIN_PASSWORD_LENGTH + { + return Err(Error::InvalidPassword); + } + + Ok(()) + } +} + +impl BytesSerializable for LoginUser { + fn as_bytes(&self) -> Bytes { + let mut bytes = BytesMut::with_capacity(2 + self.username.len() + self.password.len()); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u8(self.username.len() as u8); + bytes.put_slice(self.username.as_bytes()); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u8(self.password.len() as u8); + bytes.put_slice(self.password.as_bytes()); + bytes.freeze() + } + + fn from_bytes(bytes: Bytes) -> Result { + if bytes.len() < 4 { + return Err(Error::InvalidCommand); + } + + let username_length = bytes[0]; + let username = from_utf8(&bytes[1..=(username_length as usize)])?.to_string(); + if username.len() != username_length as usize { + return Err(Error::InvalidCommand); + } + + let password_length = bytes[1 + username_length as usize]; + let password = from_utf8( + &bytes[2 + username_length as usize + ..2 + username_length as usize + password_length as usize], + )? + .to_string(); + if password.len() != password_length as usize { + return Err(Error::InvalidCommand); + } + + let command = LoginUser { username, password }; + command.validate()?; + Ok(command) + } +} + +impl Display for LoginUser { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}|{}", self.username, self.password) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_be_serialized_as_bytes() { + let command = LoginUser { + username: "user".to_string(), + password: "secret".to_string(), + }; + + let bytes = command.as_bytes(); + let username_length = bytes[0]; + let username = from_utf8(&bytes[1..=(username_length as usize)]).unwrap(); + let password_length = bytes[1 + username_length as usize]; + let password = from_utf8( + &bytes[2 + username_length as usize + ..2 + username_length as usize + password_length as usize], + ) + .unwrap(); + + assert!(!bytes.is_empty()); + assert_eq!(username, command.username); + assert_eq!(password, command.password); + } + + #[test] + fn should_be_deserialized_from_bytes() { + let username = "user"; + let password = "secret"; + let mut bytes = BytesMut::new(); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u8(username.len() as u8); + bytes.put_slice(username.as_bytes()); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u8(password.len() as u8); + bytes.put_slice(password.as_bytes()); + let command = LoginUser::from_bytes(bytes.freeze()); + assert!(command.is_ok()); + + let command = command.unwrap(); + assert_eq!(command.username, username); + assert_eq!(command.password, password); + } +} diff --git a/src/models/users/logout_user.rs b/src/models/users/logout_user.rs new file mode 100644 index 0000000..73794fd --- /dev/null +++ b/src/models/users/logout_user.rs @@ -0,0 +1,66 @@ +use crate::infrastructure::error::Error; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::command::CommandPayload; +use crate::models::validatable::Validatable; +use bytes::Bytes; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; + +/// `LogoutUser` command is used to logout the authenticated user. +/// It has no additional payload. +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct LogoutUser {} + +impl CommandPayload for LogoutUser {} + +impl Validatable for LogoutUser { + fn validate(&self) -> Result<(), Error> { + Ok(()) + } +} + +impl BytesSerializable for LogoutUser { + fn as_bytes(&self) -> Bytes { + Bytes::new() + } + + fn from_bytes(bytes: Bytes) -> Result { + if !bytes.is_empty() { + return Err(Error::InvalidCommand); + } + + let command = LogoutUser {}; + command.validate()?; + Ok(command) + } +} + +impl Display for LogoutUser { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_be_serialized_as_empty_bytes() { + let command = LogoutUser {}; + let bytes = command.as_bytes(); + assert!(bytes.is_empty()); + } + + #[test] + fn should_be_deserialized_from_empty_bytes() { + let command = LogoutUser::from_bytes(Bytes::new()); + assert!(command.is_ok()); + } + + #[test] + fn should_not_be_deserialized_from_empty_bytes() { + let command = LogoutUser::from_bytes(Bytes::from_static(&[0])); + assert!(command.is_err()); + } +} diff --git a/src/models/users/mod.rs b/src/models/users/mod.rs new file mode 100644 index 0000000..6916a37 --- /dev/null +++ b/src/models/users/mod.rs @@ -0,0 +1,10 @@ +pub mod change_password; +pub mod create_user; +pub mod defaults; +pub mod delete_user; +pub mod get_user; +pub mod get_users; +pub mod login_user; +pub mod logout_user; +pub mod update_permissions; +pub mod update_user; diff --git a/src/models/users/update_permissions.rs b/src/models/users/update_permissions.rs new file mode 100644 index 0000000..39f2afe --- /dev/null +++ b/src/models/users/update_permissions.rs @@ -0,0 +1,158 @@ +use crate::infrastructure::error::Error; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::command::CommandPayload; +use crate::models::identifier::Identifier; +use crate::models::permissions::Permissions; +use crate::models::validatable::Validatable; +use bytes::{BufMut, Bytes, BytesMut}; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; + +/// `UpdatePermissions` command is used to update a user's permissions. +/// It has additional payload: +/// - `user_id` - unique user ID (numeric or name). +/// - `permissions` - new permissions (optional) +#[derive(Debug, Serialize, Deserialize, PartialEq, Default)] +pub struct UpdatePermissions { + /// Unique user ID (numeric or name). + #[serde(skip)] + pub user_id: Identifier, + /// New permissions if `None` is provided, then the existing user's permissions will be removed. + pub permissions: Option, +} + +impl CommandPayload for UpdatePermissions {} + +impl Validatable for UpdatePermissions { + fn validate(&self) -> Result<(), Error> { + Ok(()) + } +} + +impl BytesSerializable for UpdatePermissions { + fn as_bytes(&self) -> Bytes { + let user_id_bytes = self.user_id.as_bytes(); + let mut bytes = BytesMut::new(); + bytes.put_slice(&user_id_bytes); + if let Some(permissions) = &self.permissions { + bytes.put_u8(1); + bytes.put_u32_le(permissions.as_bytes().len() as u32); + bytes.put_slice(&permissions.as_bytes()); + } else { + bytes.put_u8(0); + } + + bytes.freeze() + } + + fn from_bytes(bytes: Bytes) -> Result { + if bytes.len() < 4 { + return Err(Error::InvalidCommand); + } + + let user_id = Identifier::from_bytes(bytes.clone())?; + let mut position = user_id.get_size_bytes() as usize; + let has_permissions = bytes[position]; + if has_permissions > 1 { + return Err(Error::InvalidCommand); + } + + position += 1; + let permissions = if has_permissions == 1 { + let permissions_length = + u32::from_le_bytes(bytes[position..position + 4].try_into().unwrap()); + position += 4; + let permissions = Permissions::from_bytes( + bytes.slice(position..position + permissions_length as usize), + )?; + Some(permissions) + } else { + None + }; + + let command = UpdatePermissions { + user_id, + permissions, + }; + command.validate()?; + Ok(command) + } +} + +impl Display for UpdatePermissions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let permissions = if let Some(permissions) = &self.permissions { + permissions.to_string() + } else { + "no_permissions".to_string() + }; + write!(f, "{}|{}", self.user_id, permissions) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::permissions::GlobalPermissions; + + #[test] + fn should_be_serialized_as_bytes() { + let command = UpdatePermissions { + user_id: Identifier::numeric(1).unwrap(), + permissions: Some(get_permissions()), + }; + let bytes = command.as_bytes(); + let user_id = Identifier::from_bytes(bytes.clone()).unwrap(); + let mut position = user_id.get_size_bytes() as usize; + let has_permissions = bytes[position]; + position += 1; + let permissions_length = + u32::from_le_bytes(bytes[position..position + 4].try_into().unwrap()); + position += 4; + let permissions = + Permissions::from_bytes(bytes.slice(position..position + permissions_length as usize)) + .unwrap(); + + assert!(!bytes.is_empty()); + assert_eq!(user_id, command.user_id); + assert_eq!(has_permissions, 1); + assert_eq!(permissions, command.permissions.unwrap()); + } + + #[test] + fn should_be_deserialized_from_bytes() { + let user_id = Identifier::numeric(1).unwrap(); + let permissions = get_permissions(); + let permissions_bytes = permissions.as_bytes(); + let mut bytes = BytesMut::new(); + bytes.put_slice(&user_id.as_bytes()); + bytes.put_u8(1); + bytes.put_u32_le(permissions_bytes.len() as u32); + bytes.put_slice(&permissions_bytes); + + let command = UpdatePermissions::from_bytes(bytes.freeze()); + assert!(command.is_ok()); + + let command = command.unwrap(); + assert_eq!(command.user_id, user_id); + assert_eq!(command.permissions.unwrap(), permissions); + } + + fn get_permissions() -> Permissions { + Permissions { + global: GlobalPermissions { + manage_servers: true, + read_servers: true, + manage_users: true, + read_users: true, + manage_streams: false, + read_streams: true, + manage_topics: false, + read_topics: true, + poll_messages: true, + send_messages: false, + }, + streams: None, + } + } +} diff --git a/src/models/users/update_user.rs b/src/models/users/update_user.rs new file mode 100644 index 0000000..7580f86 --- /dev/null +++ b/src/models/users/update_user.rs @@ -0,0 +1,186 @@ +use crate::infrastructure::error::Error; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::command::CommandPayload; +use crate::models::identifier::Identifier; +use crate::models::user_status::UserStatus; +use crate::models::users::defaults::*; +use crate::models::validatable::Validatable; +use crate::utils::text; +use bytes::{BufMut, Bytes, BytesMut}; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use std::str::from_utf8; + +/// `UpdateUser` command is used to update a user's username and status. +/// It has additional payload: +/// - `user_id` - unique user ID (numeric or name). +/// - `username` - new username (optional), if provided, must be between 3 and 50 characters long. +/// - `status` - new status (optional) +#[derive(Debug, Serialize, Deserialize, PartialEq, Default)] +pub struct UpdateUser { + #[serde(skip)] + pub user_id: Identifier, + pub username: Option, + pub status: Option, +} + +impl CommandPayload for UpdateUser {} + +impl Validatable for UpdateUser { + fn validate(&self) -> Result<(), Error> { + if self.username.is_none() { + return Ok(()); + } + + let username = self.username.as_ref().unwrap(); + if username.is_empty() + || username.len() > MAX_USERNAME_LENGTH + || username.len() < MIN_USERNAME_LENGTH + { + return Err(Error::InvalidUsername); + } + + if !text::is_resource_name_valid(username) { + return Err(Error::InvalidUsername); + } + + Ok(()) + } +} + +impl BytesSerializable for UpdateUser { + fn as_bytes(&self) -> Bytes { + let user_id_bytes = self.user_id.as_bytes(); + let mut bytes = BytesMut::new(); + bytes.put_slice(&user_id_bytes); + if let Some(username) = &self.username { + bytes.put_u8(1); + #[allow(clippy::cast_possible_truncation)] + bytes.put_u8(username.len() as u8); + bytes.put_slice(username.as_bytes()); + } else { + bytes.put_u8(0); + } + if let Some(status) = &self.status { + bytes.put_u8(1); + bytes.put_u8(status.as_code()); + } else { + bytes.put_u8(0); + } + + bytes.freeze() + } + + fn from_bytes(bytes: Bytes) -> Result { + if bytes.len() < 5 { + return Err(Error::InvalidCommand); + } + + let user_id = Identifier::from_bytes(bytes.clone())?; + let mut position = user_id.get_size_bytes() as usize; + let has_username = bytes[position]; + if has_username > 1 { + return Err(Error::InvalidCommand); + } + + position += 1; + let username = if has_username == 1 { + let username_length = bytes[position]; + position += 1; + let username = + from_utf8(&bytes[position..position + username_length as usize])?.to_string(); + position += username_length as usize; + Some(username) + } else { + None + }; + + let has_status = bytes[position]; + if has_status > 1 { + return Err(Error::InvalidCommand); + } + + let status = if has_status == 1 { + position += 1; + let status = UserStatus::from_code(bytes[position])?; + Some(status) + } else { + None + }; + + let command = UpdateUser { + user_id, + username, + status, + }; + command.validate()?; + Ok(command) + } +} + +impl Display for UpdateUser { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let username = self.username.as_deref().unwrap_or(""); + let status = self + .status + .as_ref() + .map_or_else(String::new, |s| s.to_string()); + write!(f, "{}|{username}|{status}", self.user_id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_be_serialized_as_bytes() { + let command = UpdateUser { + user_id: Identifier::numeric(1).unwrap(), + username: Some("user".to_string()), + status: Some(UserStatus::Active), + }; + + let bytes = command.as_bytes(); + let user_id = Identifier::from_bytes(bytes.clone()).unwrap(); + let mut position = user_id.get_size_bytes() as usize; + let has_username = bytes[position]; + position += 1; + let username_length = bytes[position]; + position += 1; + let username = from_utf8(&bytes[position..position + username_length as usize]).unwrap(); + position += username_length as usize; + let has_status = bytes[position]; + position += 1; + let status = UserStatus::from_code(bytes[position]).unwrap(); + + assert!(!bytes.is_empty()); + assert_eq!(user_id, command.user_id); + assert_eq!(has_username, 1); + assert_eq!(username, command.username.unwrap()); + assert_eq!(has_status, 1); + assert_eq!(status, command.status.unwrap()); + } + + #[test] + fn should_be_deserialized_from_bytes() { + let user_id = Identifier::numeric(1).unwrap(); + let username = "user"; + let status = UserStatus::Active; + let mut bytes = BytesMut::new(); + bytes.put_slice(&user_id.as_bytes()); + bytes.put_u8(1); + bytes.put_u8(username.len() as u8); + bytes.put_slice(username.as_bytes()); + bytes.put_u8(1); + bytes.put_u8(status.as_code()); + + let command = UpdateUser::from_bytes(bytes.freeze()); + assert!(command.is_ok()); + + let command = command.unwrap(); + assert_eq!(command.user_id, user_id); + assert_eq!(command.username.unwrap(), username); + assert_eq!(command.status.unwrap(), status); + } +} diff --git a/src/models/validatable.rs b/src/models/validatable.rs new file mode 100644 index 0000000..ae2d9ca --- /dev/null +++ b/src/models/validatable.rs @@ -0,0 +1,4 @@ +/// A trait for validating a type. +pub trait Validatable { + fn validate(&self) -> Result<(), E>; +} diff --git a/src/mqtt/mod.rs b/src/mqtt/mod.rs new file mode 100644 index 0000000..3051416 --- /dev/null +++ b/src/mqtt/mod.rs @@ -0,0 +1 @@ +pub mod mqtt_server; diff --git a/src/mqtt/mqtt_server.rs b/src/mqtt/mqtt_server.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/quic/listener.rs b/src/quic/listener.rs new file mode 100644 index 0000000..52b8829 --- /dev/null +++ b/src/quic/listener.rs @@ -0,0 +1,127 @@ +use crate::binary::command; +use crate::infrastructure::clients::client_manager::Transport; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::SharedSystem; +use crate::models::bytes_serializable::BytesSerializable; +use crate::models::command::Command; +use crate::quic::quic_sender::QuicSender; +use crate::server_error::ServerError; + +use std::net::SocketAddr; +use std::sync::Arc; + +use anyhow::{anyhow, Context}; +use bytes::Bytes; +use quinn::{Connection, Endpoint, RecvStream, SendStream}; +use tracing::{debug, error, info}; + +// const MAX_HEADERS_SIZE: u32 = 100 * 1000; +pub const MAX_PAYLOAD_SIZE: u32 = 10 * 1000 * 1000; + +const LISTENERS_COUNT: u32 = 10; +const INITIAL_BYTES_LENGTH: usize = 4; + +pub fn start(endpoint: Endpoint, system: SharedSystem) { + for _ in 0..LISTENERS_COUNT { + let endpoint = endpoint.clone(); + let system = system.clone(); + tokio::spawn(async move { + while let Some(incoming_connection) = endpoint.accept().await { + info!( + "Incoming connection from client: {}", + incoming_connection.remote_address() + ); + let system = system.clone(); + tokio::spawn(async move { + if let Err(error) = handle_connection(incoming_connection, system).await { + error!("Connection has failed: {error}"); + } + }); + } + }); + } +} + +async fn handle_connection( + incoming_connection: quinn::Connecting, + system: SharedSystem, +) -> Result<(), ServerError> { + let connection = incoming_connection.await?; + let address = connection.remote_address(); + info!("Client has connected: {address}"); + let client_id = system.read().add_client(&address, Transport::Quic).await; + let session = Arc::new(Session::from_client_id(client_id, address)); + + while let Some(stream) = accept_stream(&connection, &system, &address).await? { + let system = system.clone(); + let session = session.clone(); + + let handle_stream_task = async move { + if let Err(err) = handle_stream(stream, system, session).await { + error!("Error when handling QUIC stream: {:?}", err) + } + }; + let _handle = tokio::spawn(handle_stream_task); + } + Ok(()) +} + +type BiStream = (SendStream, RecvStream); + +async fn accept_stream( + connection: &Connection, + system: &SharedSystem, + address: &SocketAddr, +) -> Result, ServerError> { + match connection.accept_bi().await { + Err(quinn::ConnectionError::ApplicationClosed { .. }) => { + info!("Connection closed"); + system.read().delete_client(address).await; + Ok(None) + } + Err(error) => { + error!("Error when handling QUIC stream: {:?}", error); + system.read().delete_client(address).await; + Err(error.into()) + } + Ok(stream) => Ok(Some(stream)), + } +} + +async fn handle_stream( + stream: BiStream, + system: SharedSystem, + session: impl AsRef, +) -> anyhow::Result<()> { + let (send_stream, mut recv_stream) = stream; + // TODO: read to BytesMut instead of Vec + let request = recv_stream + .read_to_end(MAX_PAYLOAD_SIZE as usize) + .await + .with_context(|| "Error when reading the QUIC request.")?; + + if request.len() < INITIAL_BYTES_LENGTH { + return Err(anyhow!( + "Unable to read the QUIC request length, expected: {INITIAL_BYTES_LENGTH} bytes, received: {} bytes.", + request.len() + )); + } + + debug!("Trying to read command..."); + let length = request[..INITIAL_BYTES_LENGTH] + .try_into() + .map(u32::from_le_bytes) + .unwrap_or_default(); + let command = Command::from_bytes(Bytes::copy_from_slice(&request[INITIAL_BYTES_LENGTH..])) + .with_context(|| "Error when reading the QUIC request command.")?; + + debug!("Received a QUIC command: {command}, payload size: {length}"); + + let mut sender = QuicSender { + send: send_stream, + recv: recv_stream, + }; + command::handle(&command, &mut sender, session.as_ref(), system.clone()) + .await + .with_context(|| "Error when handling the QUIC request.") +} diff --git a/src/quic/mod.rs b/src/quic/mod.rs new file mode 100644 index 0000000..cc4bd13 --- /dev/null +++ b/src/quic/mod.rs @@ -0,0 +1,3 @@ +pub mod listener; +pub mod quic_sender; +pub mod quic_server; diff --git a/src/quic/quic_sender.rs b/src/quic/quic_sender.rs new file mode 100644 index 0000000..e7f6fdc --- /dev/null +++ b/src/quic/quic_sender.rs @@ -0,0 +1,54 @@ +use crate::binary::sender::Sender; +use crate::infrastructure::error::Error; +use async_trait::async_trait; +use quinn::{RecvStream, SendStream}; +use tracing::debug; + +const STATUS_OK: &[u8] = &[0; 4]; + +#[derive(Debug)] +pub struct QuicSender { + pub(crate) send: SendStream, + pub(crate) recv: RecvStream, +} + +unsafe impl Send for QuicSender {} +unsafe impl Sync for QuicSender {} + +#[async_trait] +impl Sender for QuicSender { + async fn read(&mut self, buffer: &mut [u8]) -> Result { + let read_bytes = self.recv.read(buffer).await; + if let Err(error) = read_bytes { + return Err(Error::from(error)); + } + + Ok(read_bytes.unwrap().unwrap()) + } + + async fn send_empty_ok_response(&mut self) -> Result<(), Error> { + self.send_ok_response(&[]).await + } + + async fn send_ok_response(&mut self, payload: &[u8]) -> Result<(), Error> { + self.send_response(STATUS_OK, payload).await + } + + async fn send_error_response(&mut self, error: Error) -> Result<(), Error> { + self.send_response(&error.as_code().to_le_bytes(), &[]) + .await + } +} + +impl QuicSender { + async fn send_response(&mut self, status: &[u8], payload: &[u8]) -> Result<(), Error> { + debug!("Sending response with status: {:?}...", status); + let length = (payload.len() as u32).to_le_bytes(); + self.send + .write_all(&[status, &length, payload].as_slice().concat()) + .await?; + self.send.finish().await?; + debug!("Sent response with status: {:?}", status); + Ok(()) + } +} diff --git a/src/quic/quic_server.rs b/src/quic/quic_server.rs new file mode 100644 index 0000000..6003d32 --- /dev/null +++ b/src/quic/quic_server.rs @@ -0,0 +1,78 @@ +use crate::configs::quic::QuicConfig; +// use crate::configs::resource_quota::byte_unit::rust_decimal::prelude::Zero; +use crate::infrastructure::systems::system::SharedSystem; +use crate::quic::listener; +use anyhow::Result; +use quinn::{Endpoint, IdleTimeout, VarInt}; +use std::error::Error; +use std::fs::File; +use std::io::BufReader; +use std::net::SocketAddr; +use std::sync::Arc; +use tracing::info; +/// Starts the QUIC server. +/// Returns the address the server is listening on. +pub fn start(config: QuicConfig, system: SharedSystem) -> SocketAddr { + info!("Initializing Nigig QUIC server..."); + let quic_config = configure_quic(&config); + if let Err(error) = quic_config { + panic!("Error when configuring QUIC: {:?}", error); + } + + let endpoint = Endpoint::server(quic_config.unwrap(), config.address.parse().unwrap()).unwrap(); + let addr = endpoint.local_addr().unwrap(); + listener::start(endpoint, system); + info!("Nigig QUIC server has started on: {:?}", addr); + addr +} + +fn configure_quic(config: &QuicConfig) -> Result> { + let (certificate, key) = match config.certificate.self_signed { + true => generate_self_signed_cert()?, + false => load_certificates(&config.certificate.cert_file, &config.certificate.key_file)?, + }; + + let mut server_config = quinn::ServerConfig::with_single_cert(certificate, key)?; + let mut transport = quinn::TransportConfig::default(); + transport.initial_mtu(config.initial_mtu.as_u64() as u16); + transport.send_window(config.send_window.as_u64()); + transport.receive_window(VarInt::try_from(config.receive_window.as_u64())?); + transport.datagram_send_buffer_size(config.datagram_send_buffer_size.as_u64() as usize); + transport.max_concurrent_bidi_streams(VarInt::try_from(config.max_concurrent_bidi_streams)?); + if !config.keep_alive_interval.is_zero() { + transport.keep_alive_interval(Some(config.keep_alive_interval.get_duration())); + } + if !config.max_idle_timeout.is_zero() { + let max_idle_timeout = IdleTimeout::try_from(config.max_idle_timeout.get_duration())?; + transport.max_idle_timeout(Some(max_idle_timeout)); + } + + server_config.transport_config(Arc::new(transport)); + Ok(server_config) +} + +fn generate_self_signed_cert( +) -> Result<(Vec, rustls::PrivateKey), Box> { + let certificate = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let certificate_der = certificate.serialize_der().unwrap(); + let private_key = certificate.serialize_private_key_der(); + let private_key = rustls::PrivateKey(private_key); + let cert_chain = vec![rustls::Certificate(certificate_der)]; + Ok((cert_chain, private_key)) +} + +fn load_certificates( + cert_file: &str, + key_file: &str, +) -> Result<(Vec, rustls::PrivateKey), Box> { + let mut cert_chain_reader = BufReader::new(File::open(cert_file)?); + let certs = rustls_pemfile::certs(&mut cert_chain_reader) + .map(|x| rustls::Certificate(x.unwrap().to_vec())) + .collect(); + let mut key_reader = BufReader::new(File::open(key_file)?); + let mut keys = rustls_pemfile::rsa_private_keys(&mut key_reader) + .map(|x| rustls::PrivateKey(x.unwrap().secret_pkcs1_der().to_vec())) + .collect::>(); + let key = rustls::PrivateKey(keys.remove(0).0); + Ok((certs, key)) +} diff --git a/src/server_error.rs b/src/server_error.rs new file mode 100644 index 0000000..8d89107 --- /dev/null +++ b/src/server_error.rs @@ -0,0 +1,67 @@ +use quinn::{ConnectionError, ReadToEndError, WriteError}; +use std::array::TryFromSliceError; +use thiserror::Error; +use tokio::io; + +// use xitca_web::{ +// bytes::Bytes, +// http::{StatusCode, WebResponse}, WebContext, +// }; + +// use std::{convert::Infallible, error, fmt}; + +#[derive(Debug, Error)] +pub enum ServerError { + #[error("IO error")] + IoError(#[from] io::Error), + #[error("System error")] + SystemError(#[from] crate::infrastructure::error::Error), + #[error("Connection error")] + ConnectionError(#[from] ConnectionError), + #[error("Invalid configuration provider: {0}")] + InvalidConfigurationProvider(String), + #[error("Cannot load configuration: {0}")] + CannotLoadConfiguration(String), + #[error("Invalid configuration")] + InvalidConfiguration, + #[error("SDK error")] + SdkError(#[from] iggy::error::Error), + #[error("Write error")] + WriteError(#[from] WriteError), + #[error("Read to end error")] + ReadToEndError(#[from] ReadToEndError), + #[error("Try from slice error")] + TryFromSliceError(#[from] TryFromSliceError), + #[error("Logging filter reload failure")] + FilterReloadFailure, + #[error("Logging stdout reload failure")] + StdoutReloadFailure, + #[error("Logging file reload failure")] + FileReloadFailure, + #[error("Cache config validation failure: {0}")] + CacheConfigValidationFailure(String), + #[error("error from /v2")] + Index2, + #[error("Command length error: {0}")] + CommandLengthError(String), +} + +// use xitca_web::{ +// bytes::Bytes, +// // dev::service::Service, +// // error::Error, +// // handler::handler_service, +// http::{StatusCode, WebResponse}, +// // route::get, +// // App, +// WebContext, +// }; +// // error_impl is an attribute macro for http response generation +// #[xitca_web::codegen::error_impl] +// impl ServerError { +// async fn call(&self, ctx: WebContext<'_, C>) -> WebResponse { +// let mut res = ctx.into_response(Bytes::new()); +// *res.status_mut() = StatusCode::BAD_REQUEST; +// res +// } +// } diff --git a/src/tcp/connection_handler.rs b/src/tcp/connection_handler.rs new file mode 100644 index 0000000..488ada5 --- /dev/null +++ b/src/tcp/connection_handler.rs @@ -0,0 +1,383 @@ +use crate::binary::command; +use crate::binary::sender::Sender; +use crate::infrastructure::clients::client_manager::Transport; +use crate::infrastructure::session::Session; +use crate::infrastructure::systems::system::SharedSystem; +use crate::models::command::Command; +use crate::server_error::ServerError; +// use iggy::bytes_serializable::BytesSerializable; +use bytes::{BufMut, BytesMut}; +use serde::{Deserialize, Serialize}; +use std::io::ErrorKind; +use std::net::SocketAddr; + +// use std::net::SocketAddr; +use crate::models::bytes_serializable::BytesSerializable; +use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader}; +use tokio::net::TcpStream; +use tracing::{debug, error, info, warn}; + +const INITIAL_BYTES_LENGTH: usize = 4; + +pub(crate) async fn handle_conn( + address: SocketAddr, + sender: &mut dyn Sender, + system: SharedSystem, +) -> Result<(), ServerError> { + let client_id = system.read().add_client(&address, Transport::Tcp).await; + + let session = Session::from_client_id(client_id, address); + let mut initial_buffer = [0u8; INITIAL_BYTES_LENGTH]; + loop { + let read_length = sender.read(&mut initial_buffer).await?; + if read_length != INITIAL_BYTES_LENGTH { + return Err(ServerError::CommandLengthError(format!( + "Unable to read the TCP request length, expected: {INITIAL_BYTES_LENGTH} bytes, received: {read_length} bytes." + ))); + } + + let length = u32::from_le_bytes(initial_buffer); + debug!("Received a TCP request, length: {length}"); + let mut command_buffer = BytesMut::with_capacity(length as usize); + command_buffer.put_bytes(0, length as usize); + sender.read(&mut command_buffer).await?; + let command = Command::from_bytes(command_buffer.freeze())?; + debug!("Received a TCP command: {command}, payload size: {length}"); + command::handle(&command, sender, &session, system.clone()).await?; + debug!("Sent a TCP response."); + } +} +pub(crate) fn handle_error(error: ServerError) { + match error { + ServerError::IoError(error) => match error.kind() { + ErrorKind::UnexpectedEof => { + info!("Connection has been closed."); + } + ErrorKind::ConnectionAborted => { + info!("Connection has been aborted."); + } + ErrorKind::ConnectionRefused => { + info!("Connection has been refused."); + } + ErrorKind::ConnectionReset => { + info!("Connection has been reset."); + } + ErrorKind::InvalidInput => { + info!("Invalid Input."); + } + ErrorKind::InvalidData => { + info!("Invalid data."); + } + ErrorKind::Other => { + info!("Connection has been other."); + } + ErrorKind::NotFound => { + info!("Not Found."); + } + ErrorKind::PermissionDenied => { + info!("Permission Denied."); + } + ErrorKind::NotConnected => { + info!("Not Connected."); + } + ErrorKind::AddrInUse => { + info!("Address in use."); + } + ErrorKind::AddrNotAvailable => { + info!("Address not available."); + } + ErrorKind::BrokenPipe => { + info!("Broken Pipe."); + } + ErrorKind::AlreadyExists => { + info!("Address already exists."); + } + ErrorKind::WouldBlock => { + info!("Would block."); + } + ErrorKind::TimedOut => { + info!("Timed Out."); + } + ErrorKind::WriteZero => { + info!("Write zero."); + } + ErrorKind::Interrupted => { + info!("Connection interrupted."); + } + ErrorKind::Unsupported => { + info!("Unsupported."); + } + ErrorKind::OutOfMemory => { + info!("Out of Memory."); + } + // ErrorKind::StorageFull => todo!(), + // ErrorKind::NotSeekable => todo!(), + // ErrorKind::FilesystemQuotaExceeded => todo!(), + // ErrorKind::FileTooLarge => todo!(), + // ErrorKind::ResourceBusy => todo!(), + // ErrorKind::ExecutableFileBusy => todo!(), + // ErrorKind::Deadlock => todo!(), + // ErrorKind::CrossesDevices => todo!(), + // ErrorKind::TooManyLinks => todo!(), + // ErrorKind::InvalidFilename => todo!(), + // ErrorKind::ArgumentListTooLong => todo!(), + // ErrorKind::NotADirectory => todo!(), + // ErrorKind::IsADirectory => todo!(), + // ErrorKind::DirectoryNotEmpty => todo!(), + // ErrorKind::ReadOnlyFilesystem => todo!(), + // ErrorKind::FilesystemLoop => todo!(), + // ErrorKind::StaleNetworkFileHandle => todo!(), + // ErrorKind::NetworkDown => todo!(), + // ErrorKind::HostUnreachable => todo!(), + // ErrorKind::NetworkUnreachable => todo!(), + // _ => todo!(), + _ => { + error!("Connection has failed>>>: {error}"); + } + }, + ServerError::SystemError(error) => { + error!("Connection has failed: [[[[[{error}]]]]]"); + } + _ => { + error!("Connection has failed<<<<: {error}"); + } + } +} + +pub async fn handle_connection(mut socket: TcpStream) -> Result<(), ServerError> { + let (read_stream, mut write_stream) = socket.split(); + let mut read_stream = BufReader::new(read_stream); + + let mut buffer = Vec::new(); + + loop { + let mut buf = vec![0; 1024]; // Read buffer + let bytes_read = read_stream.read(&mut buf).await?; + + if bytes_read == 0 { + log::warn!("EOF received"); + Response::default(); + break; + } + + buffer.extend(&buf[..bytes_read]); + + // Check if the buffer contains a complete line + while let Some(newline_position) = buffer.iter().position(|&x| x == b'\n') { + let line = buffer.drain(..=newline_position).collect::>(); + + // Convert the line to a UTF-8 string + let data = String::from_utf8(line.clone()).unwrap(); // Handle potential errors in a safer way + tracing::info!("[Incoming] - {}", data); + + let (response, close) = match Request::from_data(&line) { + Ok(request) => (Response::new(is_prime(request.number())), false), + Err(e) => { + warn!("Received a malformed request. Sending back a malformed response and closing connection: {:?}", e); + (Response::default(), true) + } + }; + + let res_bytes = response.to_bytes(); + write_stream.write_all(&res_bytes).await?; + write_stream.write_all(b"\n").await?; + + if close { + break; + } + } + } + Ok(()) +} +// pub(crate) async fn handle_connection( +// address: SocketAddr, +// // sender: &mut dyn Sender, +// // system: SharedSystem, +// ) -> Result<(), ServerError> { +// // let client_id = system +// // .read() +// // .await +// // .add_client(&address, Transport::Tcp) +// // .await; + +// // let mut session = Session::from_client_id(client_id, address); +// let mut initial_buffer = [0u8; INITIAL_BYTES_LENGTH]; +// loop { +// let read_length = sender.read(&mut initial_buffer).await?; +// if read_length != INITIAL_BYTES_LENGTH { +// error!( +// "Unable to read the TCP request length, expected: {INITIAL_BYTES_LENGTH} bytes, received: {read_length} bytes.", +// ); +// continue; +// } + +// let length = u32::from_le_bytes(initial_buffer); +// debug!("Received a TCP request, length: {length}"); +// let mut command_buffer = vec![0u8; length as usize]; +// sender.read(&mut command_buffer).await?; +// let command = Command::from_bytes(&command_buffer)?; +// debug!("Received a TCP command: {command}, payload size: {length}"); +// let result = command::handle(&command, sender, &mut session, system.clone()).await; +// if result.is_err() { +// error!("Error when handling the TCP request: {:?}", result.err()); +// continue; +// } +// debug!("Sent a TCP response."); +// } +// } + +#[derive(Debug, Default, Deserialize)] +struct Request { + method: String, + number: f64, +} + +#[derive(Debug)] +enum RequestError { + InvalidMethod(String), + InvalidNumber(f64), + // InvalidUTF8, + DeserializationError(serde_json::Error), + Utf8Err(std::str::Utf8Error), +} + +impl From for RequestError { + fn from(e: serde_json::Error) -> Self { + Self::DeserializationError(e) + } +} +impl From for RequestError { + fn from(e: std::str::Utf8Error) -> Self { + Self::Utf8Err(e) + } +} + +impl std::fmt::Display for RequestError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for RequestError {} + +const VALID_METHOD: &str = "isPrime"; + +impl Request { + // fn from_slice(data: &[u8]) -> Result { + // let request: Self = serde_json::from_slice(data)?; + // request.validate()?; + // Ok(request) + // } + // fn from_str(data: &str) -> Result { + // let request: Self = serde_json::from_str(data)?; + // request.validate()?; + // Ok(request) + // } + fn from_data(data: &[u8]) -> Result { + let data_str = std::str::from_utf8(data)?; + let request: Self = serde_json::from_str(data_str)?; + request.validate()?; + Ok(request) + } + + fn validate(&self) -> Result<(), RequestError> { + use RequestError::*; + if self.method != VALID_METHOD { + return Err(InvalidMethod(self.method.clone())); + } + if self.number != (self.number as i64) as f64 { + return Err(InvalidNumber(self.number)); + } + + Ok(()) + } + + fn number(&self) -> i64 { + self.number as i64 + } +} + +#[derive(Debug, Default, Serialize)] +struct Response { + method: String, + prime: bool, +} + +impl Response { + fn new(prime: bool) -> Self { + Self { + method: VALID_METHOD.into(), + prime: prime, + } + } + + fn to_bytes(&self) -> Vec { + serde_json::to_vec(self).unwrap() + } +} + +/// From https://docs.rs/primes/latest/src/primes/lib.rs.html +fn firstfac(x: i64) -> i64 { + if x % 2 == 0 { + return 2; + }; + + for n in (1..).map(|m| 2 * m + 1).take_while(|m| m * m <= x) { + if x % n == 0 { + return n; + }; + } + // No factor found. It must be prime. + x +} + +fn is_prime(n: i64) -> bool { + if n <= 1 { + return false; + } + firstfac(n) == n +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_request_deserialize() { + let data = b"{\"method\":\"isPrime\",\"number\":42}"; + let request = Request::from_data(data).unwrap(); + + assert_eq!(request.method, "isPrime"); + assert_eq!(request.number(), 42); + } + + #[test] + fn test_response_serialize() { + let resp = Response::new(true); + let data = resp.to_bytes(); + let expected = b"{\"method\":\"isPrime\",\"prime\":true}"; + + assert_eq!(&data, expected); + } + + #[test] + fn test_is_prime_negative() { + let number = -1; + assert_eq!(is_prime(number), false); + } + + #[test] + fn test_is_prime_zero() { + let number = 0; + assert_eq!(is_prime(number), false); + } + + #[test] + fn test_is_prime_positive() { + let number = 13; + assert_eq!(is_prime(number), true); + + let number = 16; + assert_eq!(is_prime(number), false); + } +} diff --git a/src/tcp/mod.rs b/src/tcp/mod.rs new file mode 100644 index 0000000..b3f8951 --- /dev/null +++ b/src/tcp/mod.rs @@ -0,0 +1,5 @@ +pub mod connection_handler; +pub mod sender; +pub mod tcp_listener; +pub mod tcp_sender; +pub mod tcp_server; diff --git a/src/tcp/sender.rs b/src/tcp/sender.rs new file mode 100644 index 0000000..e485507 --- /dev/null +++ b/src/tcp/sender.rs @@ -0,0 +1,60 @@ +use crate::infrastructure::error::Error; + +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use tracing::debug; + +const STATUS_OK: &[u8] = &[0; 4]; + +pub(crate) async fn read(stream: &mut T, buffer: &mut [u8]) -> Result +where + T: AsyncRead + AsyncWrite + Unpin, +{ + tracing::warn!("Here6a1"); + let read_bytes = stream.read_exact(buffer).await; + // let t = std::str::from_utf8(read_bytes.unwrap()).unwrap(); + tracing::warn!("Here6a2: {:?}", read_bytes); + // let read_bytes = stream.read_exact(buffer).await; + if let Err(error) = read_bytes { + return Err(Error::from(error)); + } + + Ok(read_bytes.unwrap()) +} + +pub(crate) async fn send_empty_ok_response(stream: &mut T) -> Result<(), Error> +where + T: AsyncRead + AsyncWrite + Unpin, +{ + send_ok_response(stream, &[]).await +} + +pub(crate) async fn send_ok_response(stream: &mut T, payload: &[u8]) -> Result<(), Error> +where + T: AsyncRead + AsyncWrite + Unpin, +{ + send_response(stream, STATUS_OK, payload).await +} + +pub(crate) async fn send_error_response(stream: &mut T, error: Error) -> Result<(), Error> +where + T: AsyncRead + AsyncWrite + Unpin, +{ + send_response(stream, &error.as_code().to_le_bytes(), &[]).await +} + +pub(crate) async fn send_response( + stream: &mut T, + status: &[u8], + payload: &[u8], +) -> Result<(), Error> +where + T: AsyncRead + AsyncWrite + Unpin, +{ + debug!("Sending response with status: {:?}...", status); + let length = (payload.len() as u32).to_le_bytes(); + stream + .write_all(&[status, &length, payload].as_slice().concat()) + .await?; + debug!("Sent response with status: {:?}", status); + Ok(()) +} diff --git a/src/tcp/tcp_listener.rs b/src/tcp/tcp_listener.rs new file mode 100644 index 0000000..ad3fd38 --- /dev/null +++ b/src/tcp/tcp_listener.rs @@ -0,0 +1,91 @@ +use std::net::SocketAddr; + +use crate::configs::tcp::TcpConfig; +use crate::infrastructure::systems::system::SharedSystem; +use crate::tcp::connection_handler::{handle_conn, handle_connection, handle_error}; +use crate::tcp::tcp_sender::TcpSender; +// use crate::tcp::tcp_sender::TcpSender; //::connection_handler; +use tokio::net::TcpListener; +use tokio::sync::oneshot; +use tracing::{error, info}; + +pub async fn start(address: &str, system: SharedSystem) -> SocketAddr { + let address = address.to_string(); + let (tx, rx) = oneshot::channel(); + tokio::spawn(async move { + let listener = TcpListener::bind(&address) + .await + .expect("Unable to start TCP TLS server."); + + let local_addr = listener + .local_addr() + .expect("Failed to get local address for TCP listener"); + + tx.send(local_addr).unwrap_or_else(|_| { + panic!( + "Failed to send the local address {:?} for TCP listener", + local_addr + ) + }); + + loop { + match listener.accept().await { + Ok((stream, address)) => { + info!("Accepted new TCP connection: {}", address); + let system = system.clone(); + let mut sender = TcpSender { stream }; + tokio::spawn(async move { + if let Err(error) = handle_conn(address, &mut sender, system.clone()).await + { + handle_error(error); + system.read().delete_client(&address).await; + } + }); + } + Err(error) => error!("Unable to accept TCP socket, error: {}", error), + } + } + }); + match rx.await { + Ok(addr) => addr, + Err(_) => panic!("Failed to get the local address for TCP listener"), + } +} + +pub async fn start2(config: &TcpConfig, system: SharedSystem) { + // pub async fn run(config: TcpConfig) -> Result<(), Box> { + // let addr = format!("127.0.0.1:{}", &config.address); + let addr = format!("{}", &config.address); + info!("Listening on address: {}", addr); + tokio::spawn(async move { + let listener = TcpListener::bind(addr.clone()).await; + if listener.is_err() { + // error!("Here {}", listener.); + panic!("Unable to start TCP server on addr {}.", addr); + } + + // let listener = TcpListener::bind(addr).await?; + let listener = listener.unwrap(); + loop { + match listener.accept().await { + Ok((stream, address)) => { + info!("Accepted new TCP connection: {}", address); + let _system = system.clone(); + // let mut sender = TcpSender { stream }; + + tokio::spawn(async move { + // _ = connection_handler::handle_connection(stream).await; + if let Err(error) = + // handle_connection(address, &mut sender, system.clone()).await + handle_connection(stream).await + { + handle_error(error); + // system.read().await.delete_client(&address).await; + } + }); + } + Err(error) => tracing::error!("Unable to accept TCP socket, error: {}", error), + } + } + }); +} diff --git a/src/tcp/tcp_sender.rs b/src/tcp/tcp_sender.rs new file mode 100644 index 0000000..99fe930 --- /dev/null +++ b/src/tcp/tcp_sender.rs @@ -0,0 +1,34 @@ +// use crate::binary::sender::Sender; +use crate::binary::sender::Sender; +use crate::infrastructure::error::Error; +use crate::tcp::sender; +use async_trait::async_trait; +use tokio::net::TcpStream; + +#[derive(Debug)] +pub struct TcpSender { + pub(crate) stream: TcpStream, +} + +unsafe impl Send for TcpSender {} +unsafe impl Sync for TcpSender {} + +#[async_trait] +impl Sender for TcpSender { + async fn read(&mut self, buffer: &mut [u8]) -> Result { + tracing::warn!("Here6a"); + sender::read(&mut self.stream, buffer).await + } + + async fn send_empty_ok_response(&mut self) -> Result<(), Error> { + sender::send_empty_ok_response(&mut self.stream).await + } + + async fn send_ok_response(&mut self, payload: &[u8]) -> Result<(), Error> { + sender::send_ok_response(&mut self.stream, payload).await + } + + async fn send_error_response(&mut self, error: Error) -> Result<(), Error> { + sender::send_error_response(&mut self.stream, error).await + } +} diff --git a/src/tcp/tcp_server.rs b/src/tcp/tcp_server.rs new file mode 100644 index 0000000..6b0037e --- /dev/null +++ b/src/tcp/tcp_server.rs @@ -0,0 +1,97 @@ +use std::net::SocketAddr; + +use tracing::info; + +use crate::configs::tcp::TcpConfig; +use crate::infrastructure::systems::system::SharedSystem; +use crate::tcp::tcp_listener; + +/// Starts the TCP server. +/// Returns the address the server is listening on. +pub async fn start(config: TcpConfig, system: SharedSystem) -> SocketAddr { + let server_name = if config.tls.enabled { + "Iggy TCP TLS" + } else { + "Iggy TCP" + }; + info!("Initializing {server_name} server..."); + let addr = match config.tls.enabled { + // true => tcp_tls_listener::start(&config.address, config.tls, system).await, + true => tcp_listener::start(&config.address, system).await, + false => tcp_listener::start(&config.address, system).await, + }; + info!("{server_name} server has started on: {:?}", addr); + addr +} + +// /// Starts the TCP server. +// /// Returns the address the server is listening on. +// pub async fn start(config: TcpConfig, system: SharedSystem) -> SocketAddr { +// let server_name = if config.tls.enabled { +// "Nigig TCP TLS" +// } else { +// "Nigig TCP" +// }; +// info!("Initializing {server_name} server..."); +// // let addr: SocketAddr = match config.tls.enabled { +// // true => tcp_listener::start(&config.address, system).await; +// // false => tcp_listener::start(&config.address, system).await; +// // }; +// let addr = match config.tls.enabled { +// // true => tcp_tls_listener::start(&config.address, config.tls, system).await, +// true => tcp_listener::start(&config.address, system).await, +// false => tcp_listener::start(&config.address, system).await, +// }; +// info!("{server_name} server has started on: {:?}", addr); +// addr +// } + +// pub async fn start2(config: TcpConfig, system: SharedSystem) -> SocketAddr { +// let server_name = if config.tls.enabled { +// "Iggy TCP TLS" +// } else { +// "Iggy TCP" +// }; +// info!("Initializing {server_name} server..."); +// let addr = match config.tls.enabled { +// true => { +// // tcp_tls_listener::start(&config.address, config.tls, system).await +// tcp_listener::start(&config, system).await; +// } +// false => { +// // tcp_listener::start(&config.address, system).await +// tcp_listener::start(&config, system).await; +// } +// }; +// info!("{server_name} server has started on: {:?}", addr); +// addr +// } + +// pub fn start(address: &str, system: SharedSystem) { +// let address = address.to_string(); +// tokio::spawn(async move { +// let listener = TcpListener::bind(address.clone()).await; +// if listener.is_err() { +// panic!("Unable to start TCP server on addr {}.", address); +// } +// let listener = listener.unwrap(); +// loop { +// match listener.accept().await { +// Ok((stream, address)) => { +// info!("Accepted new TCP connection: {}", address); +// let system = system.clone(); +// let mut sender = TcpSender { stream }; +// tokio::spawn(async move { +// if let Err(error) = +// handle_connection(address, &mut sender, system.clone()).await +// { +// handle_error(error); +// system.read().await.delete_client(&address).await; +// } +// }); +// } +// Err(error) => error!("Unable to accept TCP socket, error: {}", error), +// } +// } +// }); +// } diff --git a/src/utils/byte_size.rs b/src/utils/byte_size.rs new file mode 100644 index 0000000..b0f7712 --- /dev/null +++ b/src/utils/byte_size.rs @@ -0,0 +1,243 @@ +use crate::infrastructure::error::Error; + +use super::duration::IggyDuration; +use byte_unit::{Byte, UnitType}; +use core::fmt; +use serde::{Deserialize, Serialize}; +use std::{ops::Add, str::FromStr}; + +/// A struct for representing byte sizes with various utility functions. +/// +/// This struct uses `Byte` from `byte_unit` crate. +/// It also implements serialization and deserialization via the `serde` crate. +/// +/// # Example +/// +/// ``` +/// use iggy::utils::byte_size::IggyByteSize; +/// use std::str::FromStr; +/// +/// let size = IggyByteSize::from(568_000_000_u64); +/// assert_eq!(568_000_000, size.as_bytes_u64()); +/// assert_eq!("568 MB", size.as_human_string()); +/// assert_eq!("568 MB", format!("{}", size)); +/// +/// let size = IggyByteSize::from(0_u64); +/// assert_eq!("unlimited", size.as_human_string_with_zero_as_unlimited()); +/// assert_eq!("0 B", size.as_human_string()); +/// assert_eq!(0, size.as_bytes_u64()); +/// +/// let size = IggyByteSize::from_str("1 GB").unwrap(); +/// assert_eq!(1_000_000_000, size.as_bytes_u64()); +/// assert_eq!("1 GB", size.as_human_string()); +/// assert_eq!("1 GB", format!("{}", size)); +/// ``` +#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] +pub struct IggyByteSize(Byte); + +impl Default for IggyByteSize { + fn default() -> Self { + Self(Byte::from_u64(0)) + } +} + +impl IggyByteSize { + /// Returns the byte size as a `u64`. + pub fn as_bytes_u64(&self) -> u64 { + self.0.as_u64() + } + + /// Returns a human-readable string representation of the byte size using decimal units. + pub fn as_human_string(&self) -> String { + self.0.get_appropriate_unit(UnitType::Decimal).to_string() + } + + /// Returns a human-readable string representation of the byte size. + /// Returns "unlimited" if the size is zero. + pub fn as_human_string_with_zero_as_unlimited(&self) -> String { + if self.as_bytes_u64() == 0 { + return "unlimited".to_string(); + } + self.0.get_appropriate_unit(UnitType::Decimal).to_string() + } + + /// Calculates the throughput based on the provided duration and returns a human-readable string. + pub(crate) fn _as_human_throughput_string(&self, duration: &IggyDuration) -> String { + if duration.is_zero() { + return "0 B/s".to_string(); + } + let seconds = duration.as_secs_f64(); + let normalized_bytes_per_second = Self::from((self.as_bytes_u64() as f64 / seconds) as u64); + format!("{}/s", normalized_bytes_per_second) + } +} + +/// Converts a `u64` bytes to `IggyByteSize`. +impl From for IggyByteSize { + fn from(byte_size: u64) -> Self { + IggyByteSize(Byte::from_u64(byte_size)) + } +} + +/// Converts an `Option` bytes to `IggyByteSize`. +impl From> for IggyByteSize { + fn from(byte_size: Option) -> Self { + match byte_size { + Some(value) => IggyByteSize(Byte::from_u64(value)), + None => IggyByteSize(Byte::from_u64(0)), + } + } +} + +impl FromStr for IggyByteSize { + type Err = Error; + + fn from_str(s: &str) -> Result { + if matches!(s, "0" | "unlimited" | "Unlimited" | "none" | "None") { + Ok(IggyByteSize(Byte::from_u64(0))) + } else { + Ok(IggyByteSize(Byte::from_str(s)?)) + } + } +} + +impl PartialEq for IggyByteSize { + fn eq(&self, other: &u64) -> bool { + self.as_bytes_u64() == *other + } +} + +impl PartialOrd for IggyByteSize { + fn partial_cmp(&self, other: &u64) -> Option { + self.as_bytes_u64().partial_cmp(other) + } +} + +impl fmt::Display for IggyByteSize { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_human_string()) + } +} + +impl Add for IggyByteSize { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + IggyByteSize(Byte::from_u64(self.as_bytes_u64() + rhs.as_bytes_u64())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_u64_ok() { + let byte_size = IggyByteSize::from(123456789); + assert_eq!(byte_size.as_bytes_u64(), 123456789); + } + + #[test] + fn test_from_u64_zero() { + let byte_size = IggyByteSize::from(0); + assert_eq!(byte_size.as_bytes_u64(), 0); + } + + #[test] + fn test_from_str_ok() { + let byte_size = IggyByteSize::from_str("123456789").unwrap(); + assert_eq!(byte_size.as_bytes_u64(), 123456789); + } + + #[test] + fn test_from_str_zero() { + let byte_size = IggyByteSize::from_str("0").unwrap(); + assert_eq!(byte_size.as_bytes_u64(), 0); + } + + #[test] + fn test_from_str_invalid() { + let byte_size = IggyByteSize::from_str("invalid"); + assert!(byte_size.is_err()); + } + + #[test] + fn test_from_str_gigabyte() { + let byte_size = IggyByteSize::from_str("1 GiB").unwrap(); + assert_eq!(byte_size.as_bytes_u64(), 1024 * 1024 * 1024); + + let byte_size = IggyByteSize::from_str("1 GB").unwrap(); + assert_eq!(byte_size.as_bytes_u64(), 1000 * 1000 * 1000); + } + + #[test] + fn test_from_str_megabyte() { + let byte_size = IggyByteSize::from_str("1 MiB").unwrap(); + assert_eq!(byte_size.as_bytes_u64(), 1024 * 1024); + + let byte_size = IggyByteSize::from_str("1 MB").unwrap(); + assert_eq!(byte_size.as_bytes_u64(), 1000 * 1000); + } + + #[test] + fn test_to_human_string_ok() { + let byte_size = IggyByteSize::from(1_073_000_000); + assert_eq!(byte_size.as_human_string(), "1.073 GB"); + } + + #[test] + fn test_to_human_string_zero() { + let byte_size = IggyByteSize::from(0); + assert_eq!(byte_size.as_human_string(), "0 B"); + } + + #[test] + fn test_to_human_string_special_zero() { + let byte_size = IggyByteSize::from(0); + assert_eq!( + byte_size.as_human_string_with_zero_as_unlimited(), + "unlimited" + ); + } + + #[test] + fn test_throughput_ok() { + let byte_size = IggyByteSize::from(1_073_000_000); + let duration = IggyDuration::from_str("10s").unwrap(); + assert_eq!( + byte_size._as_human_throughput_string(&duration), + "107.3 MB/s" + ); + } + + #[test] + fn test_throughput_zero_size() { + let byte_size = IggyByteSize::from(0); + let duration = IggyDuration::from_str("10s").unwrap(); + assert_eq!(byte_size._as_human_throughput_string(&duration), "0 B/s"); + } + + #[test] + fn test_throughput_zero_duration() { + let byte_size = IggyByteSize::from(1_073_000_000); + let duration = IggyDuration::from_str("0s").unwrap(); + assert_eq!(byte_size._as_human_throughput_string(&duration), "0 B/s"); + } + + #[test] + fn test_throughput_very_low() { + let byte_size = IggyByteSize::from(8); + let duration = IggyDuration::from_str("1s").unwrap(); + assert_eq!(byte_size._as_human_throughput_string(&duration), "8 B/s"); + } + + #[test] + fn test_throughput_very_high() { + let byte_size = IggyByteSize::from(u64::MAX); + let duration = IggyDuration::from_str("1s").unwrap(); + assert_eq!( + byte_size._as_human_throughput_string(&duration), + "18.446744073709553 EB/s" + ); + } +} diff --git a/src/utils/checksum.rs b/src/utils/checksum.rs new file mode 100644 index 0000000..6223028 --- /dev/null +++ b/src/utils/checksum.rs @@ -0,0 +1,3 @@ +pub fn calculate(data: &[u8]) -> u32 { + crc32fast::hash(data) +} diff --git a/src/utils/crypto.rs b/src/utils/crypto.rs new file mode 100644 index 0000000..f11124d --- /dev/null +++ b/src/utils/crypto.rs @@ -0,0 +1,95 @@ +use crate::infrastructure::error::Error; +use crate::utils::text; +use aes_gcm::aead::generic_array::GenericArray; +use aes_gcm::aead::{Aead, OsRng}; +use aes_gcm::{AeadCore, Aes256Gcm, KeyInit}; +use std::fmt::Debug; + +pub trait Encryptor: Send + Sync + Debug { + fn encrypt(&self, data: &[u8]) -> Result, Error>; + fn decrypt(&self, data: &[u8]) -> Result, Error>; +} + +pub struct Aes256GcmEncryptor { + cipher: Aes256Gcm, +} + +unsafe impl Send for Aes256GcmEncryptor {} +unsafe impl Sync for Aes256GcmEncryptor {} + +impl Debug for Aes256GcmEncryptor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Encryptor").finish() + } +} + +impl Aes256GcmEncryptor { + pub fn new(key: &[u8]) -> Result { + if key.len() != 32 { + return Err(Error::InvalidEncryptionKey); + } + Ok(Self { + cipher: Aes256Gcm::new(GenericArray::from_slice(key)), + }) + } + + pub fn from_base64_key(key: &str) -> Result { + Self::new(&text::from_base64_as_bytes(key)?) + } +} + +impl Encryptor for Aes256GcmEncryptor { + fn encrypt(&self, data: &[u8]) -> Result, Error> { + let nonce = Aes256Gcm::generate_nonce(&mut OsRng); + let encrypted_data = self.cipher.encrypt(&nonce, data); + if encrypted_data.is_err() { + return Err(Error::CannotEncryptData); + } + let payload = [&nonce, encrypted_data.unwrap().as_slice()].concat(); + Ok(payload) + } + + fn decrypt(&self, data: &[u8]) -> Result, Error> { + let nonce = GenericArray::from_slice(&data[0..12]); + let payload = self.cipher.decrypt(nonce, &data[12..]); + if payload.is_err() { + return Err(Error::CannotDecryptData); + } + Ok(payload.unwrap()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn given_the_same_key_data_should_be_encrypted_and_decrypted_correctly() { + let key = [1; 32]; + let encryptor = Aes256GcmEncryptor::new(&key).unwrap(); + let data = b"Hello World!"; + let encrypted_data = encryptor.encrypt(data); + assert!(encrypted_data.is_ok()); + let encrypted_data = encrypted_data.unwrap(); + let decrypted_data = encryptor.decrypt(&encrypted_data); + assert!(decrypted_data.is_ok()); + let decrypted_data = decrypted_data.unwrap(); + assert_eq!(data, decrypted_data.as_slice()); + } + + #[test] + fn given_the_invalid_key_data_should_not_be_decrypted_correctly() { + let first_key = [1; 32]; + let second_key = [2; 32]; + let first_encryptor = Aes256GcmEncryptor::new(&first_key).unwrap(); + let second_encryptor = Aes256GcmEncryptor::new(&second_key).unwrap(); + let data = b"Hello World!"; + let encrypted_data = first_encryptor.encrypt(data); + assert!(encrypted_data.is_ok()); + let encrypted_data = encrypted_data.unwrap(); + let decrypted_data = second_encryptor.decrypt(&encrypted_data); + assert!(decrypted_data.is_err()); + let error = decrypted_data.err().unwrap(); + assert_eq!(error.as_code(), Error::CannotDecryptData.as_code()); + } +} diff --git a/src/utils/duration.rs b/src/utils/duration.rs new file mode 100644 index 0000000..7144697 --- /dev/null +++ b/src/utils/duration.rs @@ -0,0 +1,139 @@ +use humantime::format_duration; +use serde::{Deserialize, Serialize}; +use std::{ + fmt::{Display, Formatter}, + str::FromStr, + time::Duration, +}; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct IggyDuration { + duration: Duration, +} + +impl IggyDuration { + pub fn new(duration: Duration) -> IggyDuration { + IggyDuration { duration } + } + + pub fn as_human_time_string(&self) -> String { + format!("{}", format_duration(self.duration)) + } + + pub fn as_secs(&self) -> u32 { + self.duration.as_secs() as u32 + } + + pub fn as_secs_f64(&self) -> f64 { + self.duration.as_secs_f64() + } + + pub fn as_micros(&self) -> u64 { + self.duration.as_micros() as u64 + } + + pub fn get_duration(&self) -> Duration { + self.duration + } + + pub fn is_zero(&self) -> bool { + self.duration.as_secs() == 0 + } +} + +impl FromStr for IggyDuration { + type Err = humantime::DurationError; + + fn from_str(s: &str) -> Result { + let s = &s.to_lowercase(); + if s == "0" || s == "unlimited" || s == "disabled" || s == "none" { + Ok(IggyDuration { + duration: Duration::new(0, 0), + }) + } else { + Ok(IggyDuration { + duration: humantime::parse_duration(s)?, + }) + } + } +} + +impl Display for IggyDuration { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_human_time_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Duration; + + #[test] + fn test_new() { + let duration = Duration::new(60, 0); // 60 seconds + let iggy_duration = IggyDuration::new(duration); + assert_eq!(iggy_duration.as_secs(), 60); + } + + #[test] + fn test_as_human_time_string() { + let duration = Duration::new(3661, 0); // 1 hour, 1 minute and 1 second + let iggy_duration = IggyDuration::new(duration); + assert_eq!(iggy_duration.as_human_time_string(), "1h 1m 1s"); + } + + #[test] + fn test_long_duration_as_human_time_string() { + let duration = Duration::new(36611233, 0); // 1year 1month 28days 1hour 13minutes 37seconds + let iggy_duration = IggyDuration::new(duration); + assert_eq!( + iggy_duration.as_human_time_string(), + "1year 1month 28days 1h 13m 37s" + ); + } + + #[test] + fn test_from_str() { + let iggy_duration: IggyDuration = "1h 1m 1s".parse().unwrap(); + assert_eq!(iggy_duration.as_secs(), 3661); + } + + #[test] + fn test_display() { + let duration = Duration::new(3661, 0); + let iggy_duration = IggyDuration::new(duration); + let duration_string = format!("{}", iggy_duration); + assert_eq!(duration_string, "1h 1m 1s"); + } + + #[test] + fn test_invalid_duration() { + let result: Result = "1 hour and 30 minutes".parse(); + assert!(result.is_err()); + } + + #[test] + fn test_zero_seconds_duration() { + let iggy_duration: IggyDuration = "0s".parse().unwrap(); + assert_eq!(iggy_duration.as_secs(), 0); + } + + #[test] + fn test_zero_duration() { + let iggy_duration: IggyDuration = "0".parse().unwrap(); + assert_eq!(iggy_duration.as_secs(), 0); + } + + #[test] + fn test_unlimited() { + let iggy_duration: IggyDuration = "unlimited".parse().unwrap(); + assert_eq!(iggy_duration.as_secs(), 0); + } + + #[test] + fn test_disabled() { + let iggy_duration: IggyDuration = "disabled".parse().unwrap(); + assert_eq!(iggy_duration.as_secs(), 0); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..097622c --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,6 @@ +pub mod byte_size; +pub mod checksum; +pub mod crypto; +pub mod duration; +pub mod text; +pub mod timestamp; diff --git a/src/utils/text.rs b/src/utils/text.rs new file mode 100644 index 0000000..3446425 --- /dev/null +++ b/src/utils/text.rs @@ -0,0 +1,34 @@ +use crate::infrastructure::error::Error; +use base64::engine::general_purpose; +use base64::Engine; +use lazy_static::lazy_static; +use regex::Regex; + +lazy_static! { + static ref RESOURCE_NAME_REGEX: Regex = Regex::new(r"^[\w\.\-\s]+$").unwrap(); +} + +pub fn to_lowercase_non_whitespace(value: &str) -> String { + value + .to_lowercase() + .split_whitespace() + .collect::>() + .join(".") +} + +pub fn is_resource_name_valid(value: &str) -> bool { + RESOURCE_NAME_REGEX.is_match(value) +} + +pub fn from_base64_as_bytes(value: &str) -> Result, Error> { + let result = general_purpose::STANDARD.decode(value); + if result.is_err() { + return Err(Error::InvalidFormat); + } + + Ok(result.unwrap()) +} + +pub fn as_base64(value: &[u8]) -> String { + general_purpose::STANDARD.encode(value) +} diff --git a/src/utils/timestamp.rs b/src/utils/timestamp.rs new file mode 100644 index 0000000..a666888 --- /dev/null +++ b/src/utils/timestamp.rs @@ -0,0 +1,71 @@ +use chrono::{DateTime, Local, Utc}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +#[derive(Debug)] +pub struct NigigTimeStamp(SystemTime); + +impl Default for NigigTimeStamp { + fn default() -> Self { + Self(SystemTime::now()) + } +} + +impl NigigTimeStamp { + pub fn now() -> Self { + NigigTimeStamp::default() + } + + pub fn to_secs(&self) -> u64 { + self.0.duration_since(UNIX_EPOCH).unwrap().as_secs() + } + + pub fn to_micros(&self) -> u64 { + self.0.duration_since(UNIX_EPOCH).unwrap().as_micros() as u64 + } + + pub fn to_string(&self, format: &str) -> String { + DateTime::::from(self.0).format(format).to_string() + } + + pub fn to_local(&self, format: &str) -> String { + DateTime::::from(self.0).format(format).to_string() + } +} + +impl From for NigigTimeStamp { + fn from(timestamp: u64) -> Self { + NigigTimeStamp(UNIX_EPOCH + Duration::from_micros(timestamp)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_timestamp_get() { + let timestamp = NigigTimeStamp::now(); + assert!(timestamp.to_micros() > 0); + } + + #[test] + fn test_timestamp_to_micros() { + let timestamp = NigigTimeStamp::from(1663472051111); + assert_eq!(timestamp.to_micros(), 1663472051111); + } + + #[test] + fn test_timestamp_to_string() { + let timestamp = NigigTimeStamp::from(1694968446131680); + assert_eq!( + timestamp.to_string("%Y-%m-%d %H:%M:%S"), + "2023-09-17 16:34:06" + ); + } + + #[test] + fn test_timestamp_from_u64() { + let timestamp = NigigTimeStamp::from(1663472051111); + assert_eq!(timestamp.to_micros(), 1663472051111); + } +} diff --git a/src/webtransport/mod.rs b/src/webtransport/mod.rs new file mode 100644 index 0000000..e69de29