first commit

This commit is contained in:
aOK 2024-06-11 21:48:59 +03:00
commit 2051b972f1
199 changed files with 22566 additions and 0 deletions

View 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

File diff suppressed because it is too large Load diff

201
Cargo.toml Normal file
View 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
View file

136
configs/server.json Normal file
View 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
View 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
View 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
View 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
View 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
}
}
}

View 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;

View 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::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(())
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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(())
}

View 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;

View 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(())
}

View 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(())
}

View 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(())
}

View 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(())
}

View 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;

View 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(())
}

View 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(())
}

View 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(())
}

View 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(())
}

View 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(())
}

View 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(())
}

View 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(())
}

View 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(())
}

View 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;

View 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(())
}

View 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
View 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
View file

@ -0,0 +1,4 @@
pub mod command;
pub mod handlers;
pub mod mapper;
pub mod sender;

10
src/binary/sender.rs Normal file
View 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
View 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(())
}

View 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,
}))
}

View 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.");
});
}
}

View file

@ -0,0 +1,3 @@
// pub mod clean_messages;
pub mod clean_personal_access_tokens;
// pub mod save_messages;

View 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
View 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
View file

@ -0,0 +1,3 @@
pub mod commands;
pub mod handler;
pub mod server_command;

View 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>,
);
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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,
}

View 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(&quota).unwrap();
assert_eq!(serialized, json!("4000000000").to_string());
let quota = MemoryResourceQuota::Percentage(25);
let serialized = serde_json::to_string(&quota).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
View 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
View 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
View 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
View 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(())
}
}

View 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
}

View 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,
})
}

View 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);
});
}
});
}

View 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,
}

View 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)
}
}

View 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)
}

View 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;

View 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));
}
}

View 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}")
}
}

View 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)
// }

View 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;

View 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 {
// }
// }

View 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
View 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
View 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
View 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
View 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
View 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,
}

View 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
// })
}

View 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
})
}

View 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)))
}

View 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
})
}

View file

@ -0,0 +1 @@
pub mod middlewarex;

View 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
})
}

View 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;

View 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))
// }

View 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)
}

View 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
View file

113
src/infrastructure/cache/buffer.rs vendored Normal file
View 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()
}
}

View 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
View file

@ -0,0 +1,2 @@
pub mod buffer;
pub mod memory_tracker;

View 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)
}
}

View file

@ -0,0 +1 @@
pub mod client_manager;

View 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);
}
}

View file

@ -0,0 +1 @@
pub mod metrics;

332
src/infrastructure/error.rs Normal file
View 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
View 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;

View file

@ -0,0 +1 @@
pub mod persister;

View 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(())
}
}

View file

@ -0,0 +1,2 @@
pub mod personal_access_token;
pub mod storage;

View file

@ -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));
}
}

View 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)
}

View 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
)
}
}

View 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 {}),
}
}
}

View 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