first commit
This commit is contained in:
commit
2051b972f1
199 changed files with 22566 additions and 0 deletions
1
597cc37e-54eb-4b64-ba07-3cfca59fc881.svg
Normal file
1
597cc37e-54eb-4b64-ba07-3cfca59fc881.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><title>telephone</title><path d="M0,256C0,114.61,114.61,0,256,0S512,114.63,512,256,397.38,512,256,512,0,397.39,0,256Z" fill="#6e6e6e"/><path d="M170,114c5.26,1.33,8.49,5,11.33,9.38,11.22,17.39,22.67,34.64,33.94,52,4.3,6.6,3.91,12.77-1.61,18.38-3.36,3.39-7.34,6.15-11.12,9.1-3.06,2.37-6.38,4.43-9.41,6.87-8.17,6.67-10.68,15.18-7.81,25.31,2.66,9.44,7.76,17.72,12.89,25.94,17,27.16,40.12,47.61,68.62,62a67.56,67.56,0,0,0,11.64,4.31c8.35,2.33,15.37-.17,20.76-6.84,3.05-3.81,5.65-8,8.51-12A98.7,98.7,0,0,1,314,300c6.58-7.42,14.72-8.38,23.09-2.91q23.2,15.16,46.33,30.37c1.77,1.17,3.49,2.46,5.34,3.5,4.5,2.54,7.91,5.93,9.27,11.09v7.2a3,3,0,0,0-.36.72C395,363.13,389,374.72,381,385.29c-3.2,4.21-6.82,8.39-12.37,9.24-9.76,1.44-19.58,3.21-29.39,3.43-34.27.73-65.85-9.28-95.48-25.65-39.86-22-71.79-52.69-96.38-91-15.85-24.67-27-51.26-31.51-80.37-.8-5.1-1.32-10.27-2-15.38,0-6.09,0-12.18,0-18.31.14-.8.36-1.58.49-2.38.78-5.76,1.53-11.51,2.33-17.27.73-5.53,2.77-10.43,7.19-13.92a130,130,0,0,1,13.19-9.58A85.55,85.55,0,0,1,163.33,114Z" fill="#fff" fill-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 1.1 KiB |
4242
Cargo.lock
generated
Normal file
4242
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
201
Cargo.toml
Normal file
201
Cargo.toml
Normal file
|
@ -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"
|
0
README.md
Normal file
0
README.md
Normal file
136
configs/server.json
Normal file
136
configs/server.json
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
397
configs/server.toml
Normal file
397
configs/server.toml
Normal file
|
@ -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"
|
87
extra.txt
Normal file
87
extra.txt
Normal file
|
@ -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 <new_limit>
|
||||||
|
|
||||||
|
$ 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
|
8
src/args.rs
Normal file
8
src/args.rs
Normal file
|
@ -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,
|
||||||
|
}
|
104
src/binary/command.rs
Normal file
104
src/binary/command.rs
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
src/binary/handlers/mod.rs
Normal file
8
src/binary/handlers/mod.rs
Normal file
|
@ -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;
|
|
@ -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(())
|
||||||
|
}
|
|
@ -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(())
|
||||||
|
}
|
|
@ -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(())
|
||||||
|
}
|
|
@ -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(())
|
||||||
|
}
|
4
src/binary/handlers/personal_access_tokens/mod.rs
Normal file
4
src/binary/handlers/personal_access_tokens/mod.rs
Normal file
|
@ -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;
|
27
src/binary/handlers/system/get_client_handler.rs
Normal file
27
src/binary/handlers/system/get_client_handler.rs
Normal file
|
@ -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(())
|
||||||
|
}
|
21
src/binary/handlers/system/get_clients_handler.rs
Normal file
21
src/binary/handlers/system/get_clients_handler.rs
Normal file
|
@ -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(())
|
||||||
|
}
|
27
src/binary/handlers/system/get_me_handler.rs
Normal file
27
src/binary/handlers/system/get_me_handler.rs
Normal file
|
@ -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(())
|
||||||
|
}
|
21
src/binary/handlers/system/get_stats_handler.rs
Normal file
21
src/binary/handlers/system/get_stats_handler.rs
Normal file
|
@ -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(())
|
||||||
|
}
|
5
src/binary/handlers/system/mod.rs
Normal file
5
src/binary/handlers/system/mod.rs
Normal file
|
@ -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;
|
16
src/binary/handlers/system/ping_handler.rs
Normal file
16
src/binary/handlers/system/ping_handler.rs
Normal file
|
@ -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(())
|
||||||
|
}
|
27
src/binary/handlers/users/change_password_handler.rs
Normal file
27
src/binary/handlers/users/change_password_handler.rs
Normal file
|
@ -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(())
|
||||||
|
}
|
28
src/binary/handlers/users/create_user_handler.rs
Normal file
28
src/binary/handlers/users/create_user_handler.rs
Normal file
|
@ -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(())
|
||||||
|
}
|
20
src/binary/handlers/users/delete_user_handler.rs
Normal file
20
src/binary/handlers/users/delete_user_handler.rs
Normal file
|
@ -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(())
|
||||||
|
}
|
21
src/binary/handlers/users/get_user_handler.rs
Normal file
21
src/binary/handlers/users/get_user_handler.rs
Normal file
|
@ -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(())
|
||||||
|
}
|
21
src/binary/handlers/users/get_users_handler.rs
Normal file
21
src/binary/handlers/users/get_users_handler.rs
Normal file
|
@ -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(())
|
||||||
|
}
|
24
src/binary/handlers/users/login_user_handler.rs
Normal file
24
src/binary/handlers/users/login_user_handler.rs
Normal file
|
@ -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(())
|
||||||
|
}
|
21
src/binary/handlers/users/logout_user_handler.rs
Normal file
21
src/binary/handlers/users/logout_user_handler.rs
Normal file
|
@ -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(())
|
||||||
|
}
|
9
src/binary/handlers/users/mod.rs
Normal file
9
src/binary/handlers/users/mod.rs
Normal file
|
@ -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;
|
22
src/binary/handlers/users/update_permissions_handler.rs
Normal file
22
src/binary/handlers/users/update_permissions_handler.rs
Normal file
|
@ -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(())
|
||||||
|
}
|
27
src/binary/handlers/users/update_user_handler.rs
Normal file
27
src/binary/handlers/users/update_user_handler.rs
Normal file
|
@ -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(())
|
||||||
|
}
|
266
src/binary/mapper.rs
Normal file
266
src/binary/mapper.rs
Normal file
|
@ -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<u8> {
|
||||||
|
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<u8> {
|
||||||
|
// 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<u8> {
|
||||||
|
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<RwLock<Client>>]) -> Vec<u8> {
|
||||||
|
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<u8> {
|
||||||
|
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<u8> {
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
for user in users {
|
||||||
|
extend_user(user, &mut bytes);
|
||||||
|
}
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map_identity_info(user_id: UserId) -> Vec<u8> {
|
||||||
|
let mut bytes = Vec::with_capacity(4);
|
||||||
|
bytes.put_u32_le(user_id);
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map_raw_pat(token: &str) -> Vec<u8> {
|
||||||
|
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<u8> {
|
||||||
|
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<u8> {
|
||||||
|
// let messages_count = polled_messages.messages.len() as u32;
|
||||||
|
// let messages_size = polled_messages
|
||||||
|
// .messages
|
||||||
|
// .iter()
|
||||||
|
// .map(|message| message.get_size_bytes())
|
||||||
|
// .sum::<u32>();
|
||||||
|
|
||||||
|
// 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<u8> {
|
||||||
|
// 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<u8> {
|
||||||
|
// let mut bytes = Vec::new();
|
||||||
|
// for stream in streams {
|
||||||
|
// extend_stream(stream, &mut bytes).await;
|
||||||
|
// }
|
||||||
|
// bytes
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub async fn map_topics(topics: &[&Topic]) -> Vec<u8> {
|
||||||
|
// let mut bytes = Vec::new();
|
||||||
|
// for topic in topics {
|
||||||
|
// extend_topic(topic, &mut bytes).await;
|
||||||
|
// }
|
||||||
|
// bytes
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub async fn map_topic(topic: &Topic) -> Vec<u8> {
|
||||||
|
// 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<u8> {
|
||||||
|
// 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<ConsumerGroup>]) -> Vec<u8> {
|
||||||
|
// 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<u8>) {
|
||||||
|
// 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<u8>) {
|
||||||
|
// 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<u8>) {
|
||||||
|
// 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<u8>) {
|
||||||
|
// 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<u8>) {
|
||||||
|
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<u8>) {
|
||||||
|
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<u8>) {
|
||||||
|
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));
|
||||||
|
}
|
4
src/binary/mod.rs
Normal file
4
src/binary/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod command;
|
||||||
|
pub mod handlers;
|
||||||
|
pub mod mapper;
|
||||||
|
pub mod sender;
|
10
src/binary/sender.rs
Normal file
10
src/binary/sender.rs
Normal file
|
@ -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<usize, Error>;
|
||||||
|
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>;
|
||||||
|
}
|
16
src/build.rs
Normal file
16
src/build.rs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
use std::error;
|
||||||
|
use vergen::EmitBuilder;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn error::Error>> {
|
||||||
|
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(())
|
||||||
|
}
|
176
src/channels/commands/clean_messages.rs
Normal file
176
src/channels/commands/clean_messages.rs
Normal file
|
@ -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<CleanMessagesCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct CleanMessagesCommand;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct CleanMessagesExecutor;
|
||||||
|
|
||||||
|
impl MessagesCleaner {
|
||||||
|
pub fn new(config: &MessageCleanerConfig, sender: Sender<CleanMessagesCommand>) -> 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<CleanMessagesCommand> 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<CleanMessagesCommand>,
|
||||||
|
) {
|
||||||
|
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<CleanMessagesCommand>,
|
||||||
|
) {
|
||||||
|
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<Option<DeletedSegments>, 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,
|
||||||
|
}))
|
||||||
|
}
|
149
src/channels/commands/clean_personal_access_tokens.rs
Normal file
149
src/channels/commands/clean_personal_access_tokens.rs
Normal file
|
@ -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<CleanPersonalAccessTokensCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct CleanPersonalAccessTokensCommand;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct CleanPersonalAccessTokensExecutor;
|
||||||
|
|
||||||
|
impl PersonalAccessTokenCleaner {
|
||||||
|
pub fn new(
|
||||||
|
config: &PersonalAccessTokenCleanerConfig,
|
||||||
|
sender: Sender<CleanPersonalAccessTokensCommand>,
|
||||||
|
) -> 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<CleanPersonalAccessTokensCommand> 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::<Vec<_>>();
|
||||||
|
|
||||||
|
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<CleanPersonalAccessTokensCommand>,
|
||||||
|
) {
|
||||||
|
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<CleanPersonalAccessTokensCommand>,
|
||||||
|
) {
|
||||||
|
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.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
3
src/channels/commands/mod.rs
Normal file
3
src/channels/commands/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// pub mod clean_messages;
|
||||||
|
pub mod clean_personal_access_tokens;
|
||||||
|
// pub mod save_messages;
|
98
src/channels/commands/save_messages.rs
Normal file
98
src/channels/commands/save_messages.rs
Normal file
|
@ -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<SaveMessagesCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<SaveMessagesCommand>) -> 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<SaveMessagesCommand> 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<SaveMessagesCommand>,
|
||||||
|
) {
|
||||||
|
let messages_saver = MessagesSaver::new(&config.message_saver, sender);
|
||||||
|
messages_saver.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_command_consumer(
|
||||||
|
mut self,
|
||||||
|
system: SharedSystem,
|
||||||
|
_config: &ServerConfig,
|
||||||
|
receiver: Receiver<SaveMessagesCommand>,
|
||||||
|
) {
|
||||||
|
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.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
32
src/channels/handler.rs
Normal file
32
src/channels/handler.rs
Normal file
|
@ -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<C, E>(&mut self, mut executor: E) -> Self
|
||||||
|
where
|
||||||
|
E: ServerCommand<C> + 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
src/channels/mod.rs
Normal file
3
src/channels/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod commands;
|
||||||
|
pub mod handler;
|
||||||
|
pub mod server_command;
|
24
src/channels/server_command.rs
Normal file
24
src/channels/server_command.rs
Normal file
|
@ -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<C> {
|
||||||
|
async fn execute(&mut self, system: &SharedSystem, command: C);
|
||||||
|
|
||||||
|
fn start_command_sender(
|
||||||
|
&mut self,
|
||||||
|
system: SharedSystem,
|
||||||
|
config: &ServerConfig,
|
||||||
|
sender: Sender<C>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fn start_command_consumer(
|
||||||
|
self,
|
||||||
|
system: SharedSystem,
|
||||||
|
config: &ServerConfig,
|
||||||
|
receiver: Receiver<C>,
|
||||||
|
);
|
||||||
|
}
|
266
src/configs/config_provider.rs
Normal file
266
src/configs/config_provider.rs
Normal file
|
@ -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<ServerConfig, ServerError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<String, TomlValue>, 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<String>,
|
||||||
|
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<FigmentValue> = 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::<i64>() {
|
||||||
|
return FigmentValue::from(int_val);
|
||||||
|
}
|
||||||
|
if let Ok(float_val) = value.parse::<f64>() {
|
||||||
|
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<FigmentMap<Profile, Dict>, 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<String> = 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<Box<dyn ConfigProvider>, 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<P: AsRef<Path>>(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<ServerConfig, ServerError> {
|
||||||
|
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<ServerConfig, figment::Error> =
|
||||||
|
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
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
286
src/configs/defaults.rs
Normal file
286
src/configs/defaults.rs
Normal file
|
@ -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(),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
287
src/configs/displays.rs
Normal file
287
src/configs/displays.rs
Normal file
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
129
src/configs/http.rs
Normal file
129
src/configs/http.rs
Normal file
|
@ -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<String>,
|
||||||
|
pub allowed_origins: Vec<String>,
|
||||||
|
pub allowed_headers: Vec<String>,
|
||||||
|
pub exposed_headers: Vec<String>,
|
||||||
|
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<String>,
|
||||||
|
pub valid_audiences: Vec<String>,
|
||||||
|
#[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<Algorithm, Error> {
|
||||||
|
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<DecodingKey, Error> {
|
||||||
|
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<EncodingKey, Error> {
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
src/configs/mod.rs
Normal file
13
src/configs/mod.rs
Normal file
|
@ -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;
|
47
src/configs/mqtt.rs
Normal file
47
src/configs/mqtt.rs
Normal file
|
@ -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<LastWill>,
|
||||||
|
// 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,
|
||||||
|
}
|
29
src/configs/quic.rs
Normal file
29
src/configs/quic.rs
Normal file
|
@ -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,
|
||||||
|
}
|
183
src/configs/resource_quota.rs
Normal file
183
src/configs/resource_quota.rs
Normal file
|
@ -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<Self, Self::Err> {
|
||||||
|
if s.ends_with('%') {
|
||||||
|
match s.trim_end_matches('%').parse::<u8>() {
|
||||||
|
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<E>(self, value: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
MemoryResourceQuota::from_str(value).map_err(de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for MemoryResourceQuota {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
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<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
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<MemoryResourceQuota, String> = "25%".parse();
|
||||||
|
assert_eq!(parsed, Ok(MemoryResourceQuota::Percentage(25)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_percentage() {
|
||||||
|
let parsed: Result<MemoryResourceQuota, String> = "125%".parse();
|
||||||
|
assert_eq!(
|
||||||
|
parsed,
|
||||||
|
Err("Percentage cannot be greater than 100".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_memory() {
|
||||||
|
let parsed: Result<MemoryResourceQuota, String> = "4 GB".parse();
|
||||||
|
assert_eq!(
|
||||||
|
parsed,
|
||||||
|
Ok(MemoryResourceQuota::Bytes(Byte::from_str("4GB").unwrap()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_memory() {
|
||||||
|
let parsed: Result<MemoryResourceQuota, String> = "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<MemoryResourceQuota, serde_json::Error> =
|
||||||
|
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<MemoryResourceQuota, serde_json::Error> =
|
||||||
|
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<MemoryResourceQuota, serde_json::Error> =
|
||||||
|
serde_json::from_str(json_data);
|
||||||
|
assert!(deserialized.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_invalid_percentage() {
|
||||||
|
let json_data = "\"125%\"";
|
||||||
|
let deserialized: Result<MemoryResourceQuota, serde_json::Error> =
|
||||||
|
serde_json::from_str(json_data);
|
||||||
|
assert!(deserialized.is_err());
|
||||||
|
}
|
||||||
|
}
|
64
src/configs/server.rs
Normal file
64
src/configs/server.rs
Normal file
|
@ -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<SystemConfig>,
|
||||||
|
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<ServerConfig, ServerError> {
|
||||||
|
let server_config = config_provider.load_config().await?;
|
||||||
|
server_config.validate()?;
|
||||||
|
Ok(server_config)
|
||||||
|
}
|
||||||
|
}
|
166
src/configs/system.rs
Normal file
166
src/configs/system.rs
Normal file
|
@ -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
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
}
|
15
src/configs/tcp.rs
Normal file
15
src/configs/tcp.rs
Normal file
|
@ -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,
|
||||||
|
}
|
145
src/configs/validators.rs
Normal file
145
src/configs/validators.rs
Normal file
|
@ -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<ServerError> 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<ServerError> 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<ServerError> 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<ServerError> 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<ServerError> 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<ServerError> 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<ServerError> 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<ServerError> 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(())
|
||||||
|
}
|
||||||
|
}
|
42
src/http/axum_http/diagnostics.rs
Normal file
42
src/http/axum_http/diagnostics.rs
Normal file
|
@ -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<SocketAddr>,
|
||||||
|
mut request: Request<Body>,
|
||||||
|
next: Next,
|
||||||
|
) -> Result<Response, StatusCode> {
|
||||||
|
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
|
||||||
|
}
|
182
src/http/axum_http/http_server.rs
Normal file
182
src/http/axum_http/http_server.rs
Normal file
|
@ -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::<SocketAddr>(),
|
||||||
|
)
|
||||||
|
.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::<SocketAddr>())
|
||||||
|
.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::<Vec<_>>();
|
||||||
|
|
||||||
|
let exposed_headers = config
|
||||||
|
.exposed_headers
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.parse().unwrap())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
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::<Vec<_>>();
|
||||||
|
|
||||||
|
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<AppState> {
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
33
src/http/axum_http/jwt/cleaner.rs
Normal file
33
src/http/axum_http/jwt/cleaner.rs
Normal file
|
@ -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<AppState>) {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
37
src/http/axum_http/jwt/json_web_token.rs
Normal file
37
src/http/axum_http/jwt/json_web_token.rs
Normal file
|
@ -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,
|
||||||
|
}
|
269
src/http/axum_http/jwt/jwt_manager.rs
Normal file
269
src/http/axum_http/jwt/jwt_manager.rs
Normal file
|
@ -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<String>,
|
||||||
|
pub valid_issuers: Vec<String>,
|
||||||
|
pub clock_skew: IggyDuration,
|
||||||
|
pub key: DecodingKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct JwtManager {
|
||||||
|
issuer: IssuerOptions,
|
||||||
|
validator: ValidatorOptions,
|
||||||
|
tokens_storage: TokenStorage,
|
||||||
|
revoked_tokens: RwLock<HashMap<String, u64>>,
|
||||||
|
validations: HashMap<Algorithm, Validation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JwtManager {
|
||||||
|
pub fn new(
|
||||||
|
issuer: IssuerOptions,
|
||||||
|
validator: ValidatorOptions,
|
||||||
|
db: Arc<Db>,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
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<Db>) -> Result<Self, Error> {
|
||||||
|
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<GeneratedTokens, Error> {
|
||||||
|
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::<JwtClaims>(&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<GeneratedTokens, Error> {
|
||||||
|
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<TokenData<JwtClaims>, 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::<JwtClaims>(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)
|
||||||
|
}
|
||||||
|
}
|
69
src/http/axum_http/jwt/middleware.rs
Normal file
69
src/http/axum_http/jwt/middleware.rs
Normal file
|
@ -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<Arc<AppState>>,
|
||||||
|
mut request: Request<Body>,
|
||||||
|
next: Next,
|
||||||
|
) -> Result<Response, StatusCode> {
|
||||||
|
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::<RequestDetails>().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)
|
||||||
|
}
|
6
src/http/axum_http/jwt/mod.rs
Normal file
6
src/http/axum_http/jwt/mod.rs
Normal file
|
@ -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;
|
73
src/http/axum_http/jwt/refresh_token.rs
Normal file
73
src/http/axum_http/jwt/refresh_token.rs
Normal file
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
193
src/http/axum_http/jwt/storage.rs
Normal file
193
src/http/axum_http/jwt/storage.rs
Normal file
|
@ -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<Db>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TokenStorage {
|
||||||
|
pub fn new(db: Arc<Db>) -> Self {
|
||||||
|
Self { db }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_refresh_token(&self, token_hash: &str) -> Result<RefreshToken, Error> {
|
||||||
|
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::<RefreshToken>(&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<Vec<RefreshToken>, Error> {
|
||||||
|
let key = format!("{REFRESH_TOKENS_KEY_PREFIX}:");
|
||||||
|
let refresh_tokens: Result<Vec<RefreshToken>, 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::<RefreshToken>(&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<Vec<RevokedAccessToken>, Error> {
|
||||||
|
let key = format!("{REVOKED_ACCESS_TOKENS_KEY_PREFIX}:");
|
||||||
|
let revoked_tokens: Result<Vec<RevokedAccessToken>, 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::<RevokedAccessToken>(&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}")
|
||||||
|
}
|
||||||
|
}
|
26
src/http/axum_http/metrics.rs
Normal file
26
src/http/axum_http/metrics.rs
Normal file
|
@ -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<Arc<AppState>>,
|
||||||
|
request: Request<Body>,
|
||||||
|
next: Next,
|
||||||
|
) -> Result<Response, StatusCode> {
|
||||||
|
state.system.read().metrics.increment_http_requests();
|
||||||
|
Ok(next.run(request).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn xmetrics<E>(next: &mut Next<E>, ctx: WebContext<'_, AppState>) -> Result<Response<()>, E> {
|
||||||
|
// ctx.state().system.read().metrics.increment_http_requests();
|
||||||
|
// next.call(ctx)
|
||||||
|
// }
|
9
src/http/axum_http/mod.rs
Normal file
9
src/http/axum_http/mod.rs
Normal file
|
@ -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;
|
82
src/http/axum_http/system.rs
Normal file
82
src/http/axum_http/system.rs
Normal file
|
@ -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<AppState>, 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<Arc<AppState>>) -> Result<String, CustomError> {
|
||||||
|
let system = state.system.read();
|
||||||
|
Ok(system.metrics.get_formatted_output())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_stats(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Extension(identity): Extension<Identity>,
|
||||||
|
) -> Result<Json<Stats>, 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<Arc<AppState>>,
|
||||||
|
// Extension(identity): Extension<Identity>,
|
||||||
|
// Path(client_id): Path<u32>,
|
||||||
|
// ) -> Result<Json<ClientInfoDetails>, 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<Arc<AppState>>,
|
||||||
|
// Extension(identity): Extension<Identity>,
|
||||||
|
// ) -> Result<Json<Vec<ClientInfo>>, 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 {
|
||||||
|
|
||||||
|
// }
|
||||||
|
// }
|
50
src/http/axum_http/testserver.rs
Normal file
50
src/http/axum_http/testserver.rs
Normal file
|
@ -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::<SocketAddr>(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to start HTTP server");
|
||||||
|
});
|
||||||
|
|
||||||
|
address
|
||||||
|
}
|
203
src/http/axum_http/users.rs
Normal file
203
src/http/axum_http/users.rs
Normal file
|
@ -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<AppState>) -> 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<Arc<AppState>>,
|
||||||
|
Extension(identity): Extension<Identity>,
|
||||||
|
Path(user_id): Path<String>,
|
||||||
|
) -> Result<Json<UserInfoDetails>, 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<Arc<AppState>>,
|
||||||
|
Extension(identity): Extension<Identity>,
|
||||||
|
) -> Result<Json<Vec<UserInfo>>, 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<Arc<AppState>>,
|
||||||
|
Extension(identity): Extension<Identity>,
|
||||||
|
Json(command): Json<CreateUser>,
|
||||||
|
) -> Result<StatusCode, CustomError> {
|
||||||
|
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<Arc<AppState>>,
|
||||||
|
Extension(identity): Extension<Identity>,
|
||||||
|
Path(user_id): Path<String>,
|
||||||
|
Json(mut command): Json<UpdateUser>,
|
||||||
|
) -> Result<StatusCode, CustomError> {
|
||||||
|
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<Arc<AppState>>,
|
||||||
|
Extension(identity): Extension<Identity>,
|
||||||
|
Path(user_id): Path<String>,
|
||||||
|
Json(mut command): Json<UpdatePermissions>,
|
||||||
|
) -> Result<StatusCode, CustomError> {
|
||||||
|
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<Arc<AppState>>,
|
||||||
|
Extension(identity): Extension<Identity>,
|
||||||
|
Path(user_id): Path<String>,
|
||||||
|
Json(mut command): Json<ChangePassword>,
|
||||||
|
) -> Result<StatusCode, CustomError> {
|
||||||
|
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<Arc<AppState>>,
|
||||||
|
Extension(identity): Extension<Identity>,
|
||||||
|
Path(user_id): Path<String>,
|
||||||
|
) -> Result<StatusCode, CustomError> {
|
||||||
|
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<Arc<AppState>>,
|
||||||
|
Json(command): Json<LoginUser>,
|
||||||
|
) -> Result<Json<IdentityInfo>, 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<Arc<AppState>>,
|
||||||
|
Extension(identity): Extension<Identity>,
|
||||||
|
Json(command): Json<LogoutUser>,
|
||||||
|
) -> Result<StatusCode, CustomError> {
|
||||||
|
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<Arc<AppState>>,
|
||||||
|
Json(command): Json<RefreshToken>,
|
||||||
|
) -> Result<Json<IdentityInfo>, 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,
|
||||||
|
}
|
147
src/http/error.rs
Normal file
147
src/http/error.rs
Normal file
|
@ -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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<WebContext<'r, C>> for CustomError {
|
||||||
|
type Response = WebResponse;
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
async fn call(&self, ctx: WebContext<'r, C>) -> Result<Self::Response, Self::Error> {
|
||||||
|
xitca_web::error::BadRequest.call(ctx).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<C> From<CustomError> for Error<C> {
|
||||||
|
fn from(e: CustomError) -> Self {
|
||||||
|
Error::from_service(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub async fn error_handler<S, C, Res>(s: &S, ctx: WebContext<'_, C>) -> Result<Res, Error<C>>
|
||||||
|
where
|
||||||
|
S: for<'r> Service<WebContext<'r, C>, Response = Res, Error = Error<C>>,
|
||||||
|
{
|
||||||
|
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::<XitcaCustomError>() {
|
||||||
|
// match e {
|
||||||
|
// XitcaCustomError::NigigServerError => {}
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
e
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn catch_panic<S, C>(service: &S, ctx: WebContext<'_, C>) -> Result<WebResponse, Error<C>>
|
||||||
|
where
|
||||||
|
S: for<'r> Service<WebContext<'r, C>, Response = WebResponse, Error = Error<C>>,
|
||||||
|
{
|
||||||
|
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<C> type and
|
||||||
|
// the default catch all convertor would help up construct a http response.
|
||||||
|
.map_err(|_| Internal)?
|
||||||
|
}
|
||||||
|
// #[error_impl]
|
||||||
|
// impl CustomError {
|
||||||
|
// async fn call<C>(&self, ctx: WebContext<'_, C>) -> WebResponse {
|
||||||
|
// // logic of generating a response from your error.
|
||||||
|
// }
|
||||||
|
// }
|
264
src/http/mapper.rs
Normal file
264
src/http/mapper.rs
Normal file
|
@ -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<UserInfo> {
|
||||||
|
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<RwLock<Client>>],
|
||||||
|
// ) -> Vec<iggy::models::client_info::ClientInfo> {
|
||||||
|
// 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<iggy::models::stream::Stream> {
|
||||||
|
// 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<iggy::models::topic::Topic> {
|
||||||
|
// 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<UserInfo> {
|
||||||
|
// 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<PersonalAccessTokenInfo> {
|
||||||
|
// 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<ConsumerGroup>],
|
||||||
|
// ) -> Vec<iggy::models::consumer_group::ConsumerGroup> {
|
||||||
|
// 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,
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// }),
|
||||||
|
// }
|
||||||
|
// }
|
5
src/http/mod.rs
Normal file
5
src/http/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod axum_http;
|
||||||
|
pub mod error;
|
||||||
|
pub mod mapper;
|
||||||
|
pub mod shared;
|
||||||
|
pub mod xitcav_http;
|
38
src/http/shared.rs
Normal file
38
src/http/shared.rs
Normal file
|
@ -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<JwtManager> for AppState {
|
||||||
|
// fn borrow(&self) -> &JwtManager {
|
||||||
|
// &self.jwt_manager
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl Borrow<SharedSystem> 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,
|
||||||
|
}
|
50
src/http/xitcav_http/diagnostics.rs
Normal file
50
src/http/xitcav_http/diagnostics.rs
Normal file
|
@ -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<E, C>(
|
||||||
|
// ext: &RequestExt<()>,
|
||||||
|
next: &mut Next<E>,
|
||||||
|
mut ctx: WebContext<'_, C>,
|
||||||
|
) -> Result<WebResponse<()>, E>
|
||||||
|
where
|
||||||
|
// S: for<'r> Service<WebContext<'r, C>, Response = WebResponse, Error = Error<C>>,
|
||||||
|
C: Borrow<AppState>, // 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
|
||||||
|
// })
|
||||||
|
}
|
71
src/http/xitcav_http/error.rs
Normal file
71
src/http/xitcav_http/error.rs
Normal file
|
@ -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<C> is the main error type xitca-web uses and at some point XitcaCustomError would
|
||||||
|
// need to be converted to it.
|
||||||
|
impl<C> From<XitcaCustomError> for Error<C> {
|
||||||
|
fn from(e: XitcaCustomError) -> Self {
|
||||||
|
Error::from_service(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #[error_impl]
|
||||||
|
// impl XitcaCustomError {
|
||||||
|
// async fn call<C>(&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<WebContext<'r, C>> for XitcaCustomError {
|
||||||
|
type Response = WebResponse;
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
async fn call(&self, ctx: WebContext<'r, C>) -> Result<Self::Response, Self::Error> {
|
||||||
|
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, C, Res>(s: &S, ctx: WebContext<'_, C>) -> Result<Res, Error<C>>
|
||||||
|
where
|
||||||
|
S: for<'r> Service<WebContext<'r, C>, Response = Res, Error = Error<C>>,
|
||||||
|
{
|
||||||
|
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::<XitcaCustomError>() {
|
||||||
|
// match e {
|
||||||
|
// XitcaCustomError::NigigServerError => {}
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
e
|
||||||
|
})
|
||||||
|
}
|
264
src/http/xitcav_http/http_server.rs
Normal file
264
src/http/xitcav_http/http_server.rs
Normal file
|
@ -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::<Vec<_>>();
|
||||||
|
|
||||||
|
let exposed_headers = config
|
||||||
|
.exposed_headers
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.parse().unwrap())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
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::<Vec<_>>();
|
||||||
|
|
||||||
|
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<AppState> {
|
||||||
|
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<AppState> = 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<RequestExt<h1::RequestBody>>,
|
||||||
|
) -> Result<Response<ResponseBody>, Infallible> {
|
||||||
|
Ok(Response::builder()
|
||||||
|
.header(CONTENT_TYPE, TEXT_UTF8)
|
||||||
|
.body("Hello World from Http/1!".into())
|
||||||
|
.unwrap())
|
||||||
|
}
|
||||||
|
async fn handler_h3(
|
||||||
|
_: Request<RequestExt<h3::RequestBody>>,
|
||||||
|
) -> Result<Response<ResponseBody>, Box<dyn std::error::Error>> {
|
||||||
|
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::Certificate>, rustls::PrivateKey), Box<dyn Error>> {
|
||||||
|
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::Certificate>, rustls::PrivateKey), Box<dyn Error>> {
|
||||||
|
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::<Vec<_>>();
|
||||||
|
let key = rustls::PrivateKey(keys.remove(0).0);
|
||||||
|
Ok((certs, key))
|
||||||
|
}
|
||||||
|
fn h3_config(config: &HttpConfig) -> io::Result<ServerConfig> {
|
||||||
|
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)))
|
||||||
|
}
|
81
src/http/xitcav_http/jwt/middlewarex.rs
Normal file
81
src/http/xitcav_http/jwt/middlewarex.rs
Normal file
|
@ -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<S, C, Res>(
|
||||||
|
service: &S,
|
||||||
|
mut ctx: WebContext<'_, C>,
|
||||||
|
) -> Result<Res, Error<C>>
|
||||||
|
where
|
||||||
|
S: for<'r> Service<WebContext<'r, C>, Response = Res, Error = Error<C>>,
|
||||||
|
C: Borrow<AppState>, // 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::<RequestDetails>().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
|
||||||
|
})
|
||||||
|
}
|
1
src/http/xitcav_http/jwt/mod.rs
Normal file
1
src/http/xitcav_http/jwt/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod middlewarex;
|
22
src/http/xitcav_http/metrics.rs
Normal file
22
src/http/xitcav_http/metrics.rs
Normal file
|
@ -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<E, C>(next: &mut Next<E>, ctx: WebContext<'_, C>) -> Result<WebResponse<()>, E>
|
||||||
|
where
|
||||||
|
// S: for<'r> Service<WebContext<'r, C>, Response = WebResponse, Error = Error<C>>,
|
||||||
|
C: Borrow<AppState>, // 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
|
||||||
|
})
|
||||||
|
}
|
7
src/http/xitcav_http/mod.rs
Normal file
7
src/http/xitcav_http/mod.rs
Normal file
|
@ -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;
|
327
src/http/xitcav_http/request_limits.rs
Normal file
327
src/http/xitcav_http/request_limits.rs
Normal file
|
@ -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<S, C>(
|
||||||
|
service: &S,
|
||||||
|
ctx: WebContext<'_, C>,
|
||||||
|
) -> Result<WebResponse, Error<C>>
|
||||||
|
where
|
||||||
|
S: for<'r> Service<WebContext<'r, C>, Response = WebResponse, Error = Error<C>>,
|
||||||
|
{
|
||||||
|
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<S>(service: &S, conn: Stream) -> Result<S::Response, S::Error>
|
||||||
|
where
|
||||||
|
S: Service<Stream, Response = ()>,
|
||||||
|
{
|
||||||
|
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<T> = std::result::Result<T, ()>;
|
||||||
|
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>(T);
|
||||||
|
|
||||||
|
impl<T: fmt::Display> fmt::Display for Sens<T> {
|
||||||
|
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<SocketAddr, Client>,
|
||||||
|
sinners: HashMap<IpAddr, Sinner>,
|
||||||
|
// 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<bool> {
|
||||||
|
// let mut server = Server::from_token();
|
||||||
|
// Ok(server.client_read(*addr))
|
||||||
|
// }
|
133
src/http/xitcav_http/system.rs
Normal file
133
src/http/xitcav_http/system.rs
Normal file
|
@ -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<C: 'static + Borrow<AppState>>(
|
||||||
|
// app: NestApp<C>,
|
||||||
|
// // state: Arc<AppState>,
|
||||||
|
// metrics_config: &HttpMetricsConfig,
|
||||||
|
// ) -> NestApp<C> {
|
||||||
|
// 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<usize> {
|
||||||
|
// 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<C: 'static>(app: NestApp<C>) -> NestApp<C> {
|
||||||
|
// 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<Arc<AppState>>) -> NestApp<Arc<AppState>> {
|
||||||
|
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<AppState>>,
|
||||||
|
// ExtensionRef(state): ExtensionRef<'_, Arc<AppState>>>,
|
||||||
|
// ctx: &WebContext<'_, Arc<AppState>>>,
|
||||||
|
) -> Result<String, CustomError> {
|
||||||
|
// let system = ctx.state().system.read();
|
||||||
|
let system = state.system.read();
|
||||||
|
Ok(system.metrics.get_formatted_output())
|
||||||
|
}
|
||||||
|
// pub fn app<C: 'static>() -> NestApp<C> {
|
||||||
|
// App::new().at(
|
||||||
|
// "/index",
|
||||||
|
// handler_service(|_: &WebContext<'_, usize>| async { NAME }),
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
pub async fn get_stats(
|
||||||
|
StateRef(state): StateRef<'_, Arc<AppState>>,
|
||||||
|
ExtensionRef(identity): ExtensionRef<'_, Identity>,
|
||||||
|
) -> Result<Json<Stats>, 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<AppState>, metrics_config: &HttpMetricsConfig) {
|
||||||
|
// ) -> Router<object::ServiceObject> {
|
||||||
|
// 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<RequestExt<h1::RequestBody>>,
|
||||||
|
) -> Result<Response<ResponseBody>, Infallible> {
|
||||||
|
Ok(Response::builder()
|
||||||
|
.header(CONTENT_TYPE, TEXT_UTF8)
|
||||||
|
.body("Hello World from Http/1!".into())
|
||||||
|
.unwrap())
|
||||||
|
}
|
||||||
|
async fn handler_h3(
|
||||||
|
_: Request<RequestExt<h3::RequestBody>>,
|
||||||
|
) -> Result<Response<ResponseBody>, Box<dyn std::error::Error>> {
|
||||||
|
Response::builder()
|
||||||
|
.status(200)
|
||||||
|
.version(Version::HTTP_3)
|
||||||
|
.header(CONTENT_TYPE, TEXT_UTF8)
|
||||||
|
.body("Hello World from Http/3!".into())
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
231
src/http/xitcav_http/users.rs
Normal file
231
src/http/xitcav_http/users.rs
Normal file
|
@ -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<AppState>) -> 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<Arc<AppState>>) -> NestApp<Arc<AppState>> {
|
||||||
|
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<AppState>>,
|
||||||
|
ExtensionRef(identity): ExtensionRef<'_, Identity>,
|
||||||
|
PathRef(user_id): PathRef<'_>,
|
||||||
|
) -> Result<Json<UserInfoDetails>, 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<AppState>>,
|
||||||
|
ExtensionRef(identity): ExtensionRef<'_, Identity>,
|
||||||
|
) -> Result<Json<Vec<UserInfo>>, 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<AppState>>,
|
||||||
|
ExtensionRef(identity): ExtensionRef<'_, Identity>,
|
||||||
|
Json(command): Json<CreateUser>,
|
||||||
|
) -> Result<StatusCode, CustomError> {
|
||||||
|
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<AppState>>,
|
||||||
|
ExtensionRef(identity): ExtensionRef<'_, Identity>,
|
||||||
|
PathRef(user_id): PathRef<'_>,
|
||||||
|
Json(mut command): Json<UpdateUser>,
|
||||||
|
) -> Result<StatusCode, CustomError> {
|
||||||
|
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<AppState>>,
|
||||||
|
ExtensionRef(identity): ExtensionRef<'_, Identity>,
|
||||||
|
PathRef(user_id): PathRef<'_>,
|
||||||
|
Json(mut command): Json<UpdatePermissions>,
|
||||||
|
) -> Result<StatusCode, CustomError> {
|
||||||
|
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<AppState>>,
|
||||||
|
ExtensionRef(identity): ExtensionRef<'_, Identity>,
|
||||||
|
PathRef(user_id): PathRef<'_>,
|
||||||
|
Json(mut command): Json<ChangePassword>,
|
||||||
|
) -> Result<StatusCode, CustomError> {
|
||||||
|
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<AppState>>,
|
||||||
|
ExtensionRef(identity): ExtensionRef<'_, Identity>,
|
||||||
|
PathRef(user_id): PathRef<'_>,
|
||||||
|
) -> Result<StatusCode, CustomError> {
|
||||||
|
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<AppState>>,
|
||||||
|
Json(command): Json<LoginUser>,
|
||||||
|
) -> Result<Json<IdentityInfo>, 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<AppState>>,
|
||||||
|
ExtensionRef(identity): ExtensionRef<'_, Identity>,
|
||||||
|
Json(command): Json<LogoutUser>,
|
||||||
|
) -> Result<StatusCode, CustomError> {
|
||||||
|
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<AppState>>,
|
||||||
|
Json(command): Json<RefreshToken>,
|
||||||
|
) -> Result<Json<IdentityInfo>, 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,
|
||||||
|
}
|
0
src/iggy/mod.rs
Normal file
0
src/iggy/mod.rs
Normal file
113
src/infrastructure/cache/buffer.rs
vendored
Normal file
113
src/infrastructure/cache/buffer.rs
vendored
Normal file
|
@ -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<T: Sizeable + Debug> {
|
||||||
|
current_size: u64,
|
||||||
|
buffer: VecDeque<T>,
|
||||||
|
memory_tracker: Arc<CacheMemoryTracker>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> SmartCache<T>
|
||||||
|
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<T> {
|
||||||
|
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<Item = T>) {
|
||||||
|
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<T> Index<usize> for SmartCache<T>
|
||||||
|
where
|
||||||
|
T: Sizeable + Clone + Debug,
|
||||||
|
{
|
||||||
|
type Output = T;
|
||||||
|
|
||||||
|
fn index(&self, index: usize) -> &Self::Output {
|
||||||
|
&self.buffer[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Sizeable + Clone + Debug> Default for SmartCache<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
101
src/infrastructure/cache/memory_tracker.rs
vendored
Normal file
101
src/infrastructure/cache/memory_tracker.rs
vendored
Normal file
|
@ -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<Arc<CacheMemoryTracker>> = None;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CacheMemoryTracker {
|
||||||
|
used_memory_bytes: AtomicU64,
|
||||||
|
limit_bytes: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageSize = u64;
|
||||||
|
|
||||||
|
impl CacheMemoryTracker {
|
||||||
|
pub fn initialize(config: &CacheConfig) -> Option<Arc<CacheMemoryTracker>> {
|
||||||
|
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<Arc<CacheMemoryTracker>> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
2
src/infrastructure/cache/mod.rs
vendored
Normal file
2
src/infrastructure/cache/mod.rs
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod buffer;
|
||||||
|
pub mod memory_tracker;
|
126
src/infrastructure/clients/client_manager.rs
Normal file
126
src/infrastructure/clients/client_manager.rs
Normal file
|
@ -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<u32, Arc<RwLock<Client>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Client {
|
||||||
|
pub client_id: u32,
|
||||||
|
pub user_id: Option<u32>,
|
||||||
|
pub address: SocketAddr,
|
||||||
|
pub transport: Transport,
|
||||||
|
// pub consumer_groups: Vec<ConsumerGroup>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[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<Arc<RwLock<Client>>, 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<Arc<RwLock<Client>>, 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<Arc<RwLock<Client>>> {
|
||||||
|
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<Arc<RwLock<Client>>> {
|
||||||
|
let id = hash::calculate_32(address.to_string().as_bytes());
|
||||||
|
self.clients.remove(&id)
|
||||||
|
}
|
||||||
|
}
|
1
src/infrastructure/clients/mod.rs
Normal file
1
src/infrastructure/clients/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod client_manager;
|
68
src/infrastructure/diagnostics/metrics.rs
Normal file
68
src/infrastructure/diagnostics/metrics.rs
Normal file
|
@ -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: <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);
|
||||||
|
}
|
||||||
|
}
|
1
src/infrastructure/diagnostics/mod.rs
Normal file
1
src/infrastructure/diagnostics/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod metrics;
|
332
src/infrastructure/error.rs
Normal file
332
src/infrastructure/error.rs
Normal file
|
@ -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")
|
||||||
|
// }
|
||||||
|
}
|
11
src/infrastructure/mod.rs
Normal file
11
src/infrastructure/mod.rs
Normal file
|
@ -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;
|
1
src/infrastructure/persistence/mod.rs
Normal file
1
src/infrastructure/persistence/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod persister;
|
75
src/infrastructure/persistence/persister.rs
Normal file
75
src/infrastructure/persistence/persister.rs
Normal file
|
@ -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(())
|
||||||
|
}
|
||||||
|
}
|
2
src/infrastructure/personal_access_tokens/mod.rs
Normal file
2
src/infrastructure/personal_access_tokens/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod personal_access_token;
|
||||||
|
pub mod storage;
|
|
@ -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<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<u32>) -> (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));
|
||||||
|
}
|
||||||
|
}
|
210
src/infrastructure/personal_access_tokens/storage.rs
Normal file
210
src/infrastructure/personal_access_tokens/storage.rs
Normal file
|
@ -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<Db>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilePersonalAccessTokenStorage {
|
||||||
|
pub fn new(db: Arc<Db>) -> 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<Vec<PersonalAccessToken>, 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::<PersonalAccessToken>(&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<Vec<PersonalAccessToken>, 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<PersonalAccessToken, Error> {
|
||||||
|
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::<PersonalAccessToken>(&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<PersonalAccessToken, Error> {
|
||||||
|
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<PersonalAccessToken> 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)
|
||||||
|
}
|
65
src/infrastructure/session.rs
Normal file
65
src/infrastructure/session.rs
Normal file
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
189
src/infrastructure/storage.rs
Normal file
189
src/infrastructure/storage.rs
Normal file
|
@ -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<T>: 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<SystemInfo> {}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait UserStorage: Storage<User> {
|
||||||
|
async fn load_by_id(&self, id: UserId) -> Result<User, Error>;
|
||||||
|
async fn load_by_username(&self, username: &str) -> Result<User, Error>;
|
||||||
|
async fn load_all(&self) -> Result<Vec<User>, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait PersonalAccessTokenStorage: Storage<PersonalAccessToken> {
|
||||||
|
async fn load_all(&self) -> Result<Vec<PersonalAccessToken>, Error>;
|
||||||
|
async fn load_for_user(&self, user_id: UserId) -> Result<Vec<PersonalAccessToken>, Error>;
|
||||||
|
async fn load_by_token(&self, token: &str) -> Result<PersonalAccessToken, Error>;
|
||||||
|
async fn load_by_name(&self, user_id: UserId, name: &str)
|
||||||
|
-> Result<PersonalAccessToken, Error>;
|
||||||
|
async fn delete_for_user(&self, user_id: UserId, name: &str) -> Result<(), Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SystemStorage {
|
||||||
|
pub info: Arc<dyn SystemInfoStorage>,
|
||||||
|
pub user: Arc<dyn UserStorage>,
|
||||||
|
pub personal_access_token: Arc<dyn PersonalAccessTokenStorage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SystemStorage {
|
||||||
|
pub fn new(db: Arc<Db>) -> 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<SystemInfo> 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<User> 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<User, Error> {
|
||||||
|
Ok(User::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_by_username(&self, _username: &str) -> Result<User, Error> {
|
||||||
|
Ok(User::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_all(&self) -> Result<Vec<User>, Error> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Storage<PersonalAccessToken> 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<Vec<PersonalAccessToken>, Error> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_for_user(&self, _user_id: UserId) -> Result<Vec<PersonalAccessToken>, Error> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_by_token(&self, _token: &str) -> Result<PersonalAccessToken, Error> {
|
||||||
|
Ok(PersonalAccessToken::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_by_name(
|
||||||
|
&self,
|
||||||
|
_user_id: UserId,
|
||||||
|
_name: &str,
|
||||||
|
) -> Result<PersonalAccessToken, Error> {
|
||||||
|
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 {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
98
src/infrastructure/systems/clients.rs
Normal file
98
src/infrastructure/systems/clients.rs
Normal file
|
@ -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<Arc<RwLock<Client>>, 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<Vec<Arc<RwLock<Client>>>, 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())
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue