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