64 Commits

Author SHA1 Message Date
Ara Sadoyan
37c2693e22 update cargo 2026-05-13 18:17:43 +02:00
Ara Sadoyan
554fa6648a Added special DEFAULT upstreams for catch up all. 2026-05-13 17:27:13 +02:00
Ara Sadoyan
20329518c1 Fix, add metrics and cleanup. 2026-05-13 16:17:11 +02:00
Ara Sadoyan
136ccc8e44 Persist config from API 2026-05-11 18:34:41 +02:00
Ara Sadoyan
1cbb19ea90 README 2026-05-09 14:53:10 +02:00
Ara Sadoyan
c0e9fbc069 README site change 2026-05-09 13:54:26 +02:00
Ara Sadoyan
98318c5fb6 README 2026-05-08 19:36:38 +02:00
Ara Sadoyan
fec9d5f1d6 Cleanup. Making clippy happy. 2026-05-08 16:35:20 +02:00
Ara Sadoyan
783ffb27e1 Merge pull request #20 from HrachMD/token-in-logs
Token Logging
2026-05-08 13:53:08 +02:00
Ara Sadoyan
3f6ee1799c Merge branch 'main' into token-in-logs 2026-05-08 13:52:14 +02:00
Ara Sadoyan
c549750e9d README update 2026-05-08 13:07:28 +02:00
Ara Sadoyan
788f7fd4ea README update 2026-05-08 13:03:17 +02:00
hrachdev
5e29c077f3 chore: readme tweaks & fmt 2026-05-07 23:10:13 -04:00
hrachdev
22609df4ba Merge branch 'main' of github.com:HrachMD/aralez into token-in-logs 2026-05-07 23:02:58 -04:00
hrachdev
c0594aed48 chore: cleanup 2026-05-07 20:02:18 -04:00
hrachdev
4f7f2d21ca fix: typo 2026-05-07 19:56:08 -04:00
hrachdev
4d04b8d7f1 fix: token logging, unneeded copy 2026-05-07 19:51:55 -04:00
Ara Sadoyan
c381faabc6 README update 2026-05-07 19:57:14 +02:00
Ara Sadoyan
982feb632e minor fix update 2026-05-06 18:14:08 +02:00
Ara Sadoyan
aee71c74f5 README update 2026-05-01 12:32:58 +02:00
Ara Sadoyan
a0b6de9759 updated . config example 2026-04-30 18:13:37 +02:00
Ara Sadoyan
a70eb53bc1 Let's Encrypt auto certificate HTTP-01 challenge #16 2026-04-30 18:04:25 +02:00
Ara Sadoyan
bee307793c restructurisation grades 2026-04-27 15:28:54 +02:00
Ara Sadoyan
6e83775127 restructurisation 2026-04-27 15:22:31 +02:00
Ara Sadoyan
baded40e6e Cache for JWT tokens, to minimize crypto. BRAKING: Claims key "valid" renamed to "exp" 2026-04-17 17:53:31 +02:00
Ara Sadoyan
c0a419f6f7 completed implementation of #17 2026-04-15 18:23:57 +02:00
Ara Sadoyan
8aff2fa875 Standardizing implementation of #17 2026-04-14 16:11:24 +02:00
Ara Sadoyan
9b4ee26a2b Working on #17 2026-04-13 20:06:57 +02:00
Ara Sadoyan
f135106a44 Changes in authentication 2026-04-08 19:05:19 +02:00
Ara Sadoyan
389c12119a code cleanup and improvements. 2026-04-08 17:00:06 +02:00
Ara Sadoyan
93a8661281 Cargo cleanup, dependency merge 2026-04-08 15:14:46 +02:00
Ara Sadoyan
0505ce2849 split upstreams.yaml file 2026-03-30 19:04:32 +02:00
Ara Sadoyan
72ed870538 split upstreams.yaml file 2026-03-27 19:24:30 +01:00
Ara Sadoyan
68140d0cf0 tye changes, optimization 2026-03-26 17:40:22 +01:00
Ara Sadoyan
7b9b206c13 optimization & cleanup 2026-03-26 16:58:53 +01:00
Ara Sadoyan
4706b281bc cleanup 2026-03-26 14:17:59 +01:00
Ara Sadoyan
1f8efc6af7 FUNDING.yml 2026-03-25 15:16:47 +01:00
Ara Sadoyan
9f595b2709 example config file update 2026-03-25 11:15:55 +01:00
Ara Sadoyan
ed44516015 added redirect_to directive for upstreams 2026-03-24 16:08:14 +01:00
Ara Sadoyan
17da7862e3 upstreams ID hashing update 2026-03-18 20:06:50 +01:00
Ara Sadoyan
24d00da855 performance improvement, sticky session minor bug fix 2026-03-17 19:21:05 +01:00
Ara Sadoyan
c9422759aa Minor performance improvement 2026-03-17 13:54:42 +01:00
Ara Sadoyan
94b1f77734 Type changes, auth override policy 2026-03-04 12:35:45 +01:00
Ara Sadoyan
9d986f9a28 Path level authentication 2026-03-03 19:35:16 +01:00
Ara Sadoyan
3afa2f209f pingora 0.8.0 upgrade 2026-03-03 13:54:53 +01:00
Ara Sadoyan
c151fdf58b moving to boringssl 2026-02-19 18:11:54 +01:00
Ara Sadoyan
438426153f removed unwrap 2026-02-18 12:00:33 +01:00
Ara Sadoyan
9bb01fd1b0 minor improvements 2026-02-17 18:22:46 +01:00
Ara Sadoyan
abb5fef1d6 minor improvements 2026-02-17 17:03:52 +01:00
Ara Sadoyan
3618687ad5 Memory allocation improvements for proxyhttp, fix issue with sticky session . 2026-02-10 19:07:43 +01:00
Ara Sadoyan
a893b3c301 Memory allocation improvements for metrics collector . 2026-02-05 13:57:39 +01:00
Ara Sadoyan
3ff262c7f4 Merge pull request #13 from yerke/patch-1
Fix grammar and formatting in README.md
2026-02-04 14:41:50 +01:00
Yerkebulan Tulibergenov
062f02259f Fix grammar and formatting in README.md 2026-01-30 23:59:10 -08:00
Ara Sadoyan
1a4c9b7d55 Performance optimization in headers 2026-01-28 16:07:45 +01:00
Ara Sadoyan
6ef7f23823 Performance optimization v2 2026-01-28 13:20:31 +01:00
Ara Sadoyan
2b437c65fb Performance improvement. String removal from hot paths. 2026-01-27 16:19:51 +01:00
Ara Sadoyan
38055ae94e added new metric aralez_requests_by_upstream 2026-01-25 18:08:15 +01:00
Ara Sadoyan
703de9e909 updates on API server https://sadoyan.github.io/aralez-docs/assets/api/ 2026-01-22 16:50:51 +01:00
Ara Sadoyan
2c8b01295c Minor subfunction removal 2026-01-21 20:01:16 +01:00
Ara Sadoyan
baebe1c00f Async apply of config via API 2026-01-20 19:16:27 +01:00
Ara Sadoyan
6c1d3c5ef8 Error handling on API server 2026-01-09 18:44:36 +01:00
Ara Sadoyan
2d1a827007 Removed unneeded loop 2025-12-14 12:09:11 +01:00
Ara Sadoyan
a2a5250711 Performance improvements on data types . 2025-12-11 15:21:34 +01:00
Ara Sadoyan
985e923342 to https redirect bug fix 2025-12-11 13:37:40 +01:00
36 changed files with 3712 additions and 2130 deletions

13
.cargo/config.toml Normal file
View File

@@ -0,0 +1,13 @@
[target.aarch64-unknown-linux-musl]
rustflags = [
"-C", "link-arg=-Wl,--defsym=fopen64=fopen",
"-C", "link-arg=-Wl,--defsym=fseeko64=fseeko",
"-C", "link-arg=-Wl,--defsym=ftello64=ftello"
]
[target.x86_64-unknown-linux-musl]
rustflags = [
"-C", "link-arg=-Wl,--defsym=fopen64=fopen",
"-C", "link-arg=-Wl,--defsym=fseeko64=fseeko",
"-C", "link-arg=-Wl,--defsym=ftello64=ftello"
]

4
.github/FUNDING.yml vendored
View File

@@ -1,6 +1,6 @@
# These are supported funding model platforms # These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] github: sadoyan
patreon: # Replace with a single Patreon username patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username
@@ -10,6 +10,6 @@ liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username polar: # Replace with a single Polar username
buy_me_a_coffee: sadoyan buy_me_a_coffee: sadoyan
thanks_dev: # Replace with a single thanks.dev username thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

2967
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,45 +11,40 @@ panic = "abort"
strip = true strip = true
[dependencies] [dependencies]
tokio = { version = "1.45.1", features = ["full"] } tokio = { version = "1.52.3", features = ["full"] }
pingora = { version = "0.6.0", features = ["lb", "openssl"] } # openssl, rustls, boringssl pingora = { version = "0.8.0", features = ["lb", "openssl"] } # openssl, rustls, boringssl
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
dashmap = "7.0.0-rc2" dashmap = "7.0.0-rc2"
pingora-core = "0.6.0" pingora-core = "0.8.0"
pingora-proxy = "0.6.0" pingora-proxy = "0.8.0"
pingora-http = "0.6.0" pingora-http = "0.8.0"
pingora-limits = "0.6.0" pingora-limits = "0.8.0"
async-trait = "0.1.89" async-trait = "0.1.89"
env_logger = "0.11.8" env_logger = "0.11.10"
log = "0.4.28" log = "0.4.29"
futures = "0.3.31" futures = "0.3.32"
notify = "8.2.0" notify = "9.0.0-rc.4"
axum = { version = "0.8.4" } axum = { version = "0.8.9" }
axum-server = { version = "0.7.2", features = ["tls-openssl"] } reqwest = { version = "0.13.3", features = ["json", "stream", "blocking"] }
reqwest = { version = "0.12.23", features = ["json", "native-tls-alpn", "stream"] } serde_yml = "0.0.12"
#reqwest = { version = "0.12.15", features = ["json", "rustls-tls"] } rand = "0.10.1"
#reqwest = { version = "0.12.15", default-features = false, features = ["rustls-tls", "json"] }
serde_yaml = "0.9.34-deprecated"
rand = "0.9.2"
base64 = "0.22.1" base64 = "0.22.1"
jsonwebtoken = "9.3.1" jsonwebtoken = { version = "10.4.0", default-features = false, features = ["use_pem", "rust_crypto"] }
tonic = "0.14.2" tonic = "0.14.6"
sha2 = { version = "0.11.0-rc.2", default-features = false } sha2 = { version = "0.11.0-rc.5", default-features = false }
base16ct = { version = "0.3.0", features = ["alloc"] } base16ct = { version = "1.0.0", features = ["alloc"] }
urlencoding = "2.1.3" urlencoding = "2.1.3"
arc-swap = "1.7.1" arc-swap = "1.9.1"
mimalloc = { version = "0.1.48", default-features = false } mimalloc = { version = "0.1.50", default-features = false }
prometheus = "0.14.0" prometheus = "0.14.0"
lazy_static = "1.5.0" x509-parser = "0.18.1"
x509-parser = "0.18.0"
rustls-pemfile = "2.2.0" rustls-pemfile = "2.2.0"
tower-http = { version = "0.6.6", features = ["fs"] } tower-http = { version = "0.6.10", features = ["fs"] }
once_cell = "1.21.3"
privdrop = "0.5.6" privdrop = "0.5.6"
ctrlc = "3.5.0" ctrlc = "3.5.2"
port_check = "0.3.0" serde_json = "1.0.149"
#moka = { version = "0.12.10", features = ["sync"] } subtle = "2.6.1"
#rustls = { version = "0.23.27", features = ["ring"] } moka = { version = "0.12.15", features = ["sync"] }
#hickory-client = { version = "0.25.2" } ahash = "0.8.12"
#openssl = "0.10.73" instant-acme = "0.8.5"
rcgen = "0.14.7"

23
Makefile Normal file
View File

@@ -0,0 +1,23 @@
update:
cargo update --verbose
features:
cargo features
checkup:
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo check --workspace --all-targets --all-features
fix:
cargo fix
fix-all:
cargo fix --all
cargo clippy --workspace --all-targets --all-features --fix
test:
cargo test --workspace --all-targets --all-features
.PHONY: update features checkup fix fix-all test
# -- ⚝ by Dave -- in NeoVim ⚝ --

355
README.md
View File

@@ -2,7 +2,7 @@
--- ---
# Aralez (Արալեզ), # Aralez (Արալեզ)
### **Reverse proxy built on top of Cloudflare's Pingora** ### **Reverse proxy built on top of Cloudflare's Pingora**
@@ -11,7 +11,7 @@ featuring Consul and Kubernetes integration for dynamic pod discovery and health
--- ---
What Aralez means ? What Aralez means ?
**Aralez = Արալեզ** <ins>.Named after the legendary Armenian guardian spirit, winged dog-like creature, that descend upon fallen heroes to lick their wounds and resurrect them.</ins>. **Aralez = Արալեզ** <ins>Named after the legendary Armenian guardian spirit, winged dog-like creature, that descend upon fallen heroes to lick their wounds and resurrect them</ins>.
Built on Rust, on top of **Cloudflares Pingora engine**, **Aralez** delivers world-class performance, security and scalability — right out of the box. Built on Rust, on top of **Cloudflares Pingora engine**, **Aralez** delivers world-class performance, security and scalability — right out of the box.
@@ -19,116 +19,92 @@ Built on Rust, on top of **Cloudflares Pingora engine**, **Aralez** delivers
--- ---
## 🔧 Key Features ## Key Features
- **Dynamic Config Reloads** — Upstreams can be updated live via API, no restart required. - **Dynamic Config Reloads** — Upstreams can be updated live via API, no restart required.
- **TLS Termination** — Built-in OpenSSL support. - **Yes loading of certificates** — Auto load certificates from a folder, without a restart.
- **Automatic load of certificates** — Automatically reads and loads certificates from a folder, without a restart. - **Lets Encrypt Certificates** — Yes ordering and renewal of SSL/TLS certificates via the HTTP-01 challenge
- **Upstreams TLS detection** — Aralez will automatically detect if upstreams uses secure connection. - **Upstreams TLS detection** — Aralez will automatically detect if upstreams uses secure connection.
- **Built in rate limiter** — Limit requests to server, by setting up upper limit for requests per seconds, per virtualhost. - **Built in rate limiter** — Globar or route limit requests to upstreams.
- **Global rate limiter** — Set rate limit for all virtualhosts.
- **Per path rate limiter** — Set rate limit for specific paths. Path limits will override global limits.
- **Authentication** — Supports Basic Auth, API tokens, and JWT verification. - **Authentication** — Supports Basic Auth, API tokens, and JWT verification.
- **Basic Auth** - **Basic Auth**
- **API Key** via `x-api-key` header - **API Key** via `x-api-key` header
- **JWT Auth**, with tokens issued by Aralez itself via `/jwt` API - **JWT Auth**, with tokens issued by Aralez itself via `/jwt` API
- ⬇️ See below for examples and implementation details. - **Forward Auth**, Sends requests to an authentication server.
- **Load Balancing Strategies** - **Load Balancing** Round-robin, health checks, optional sticky sessions.
- Round-robin
- Failover with health checks
- Sticky sessions via cookies
- **Unified Port** — Serve HTTP and WebSocket traffic over the same connection.
- **Built in file server** — Build in minimalistic file server for serving static files, should be added as upstreams for public access. - **Built in file server** — Build in minimalistic file server for serving static files, should be added as upstreams for public access.
- **Memory Safe** — Created purely on Rust. - **Upstream Providers:**
- **High Performance** — Built with [Pingora](https://github.com/cloudflare/pingora) and tokio for async I/O.
## 🌍 Highlights
- ⚙️ **Upstream Providers:**
- `file` Upstreams are declared in config file. - `file` Upstreams are declared in config file.
- `consul` Upstreams are dynamically updated from Hashicorp Consul. - `consul` Upstreams are dynamically updated from Hashicorp Consul.
- 🔁 **Hot Reloading:** Modify upstreams on the fly via `upstreams.yaml` — no restart needed. - `kubernetes` Upstreams are dynamically updated from kubernetes api server.
- 🔮 **Automatic WebSocket Support:** Zero config — connection upgrades are handled seamlessly. - **Yes WebSocket Support:** WS connection upgrades are handled automatically.
- 🔮 **Automatic GRPC Support:** Zero config, Requires `ssl` to proxy, gRPC handled seamlessly. - **Yes gRPC Support:** gRPC detected and handled automatically.
- 🔮 **Upstreams Session Stickiness:** Enable/Disable Sticky sessions globally. - **Header Injection:** Global and per-route server/client headers injection.
- 🔐 **TLS Termination:** Fully supports TLS for upstreams and downstreams. - **Remote Config Push:** Lightweight HTTP API to update configs from CI/CD or other systems.
- 🛡️ **Built-in Authentication** Basic Auth, JWT, API key. - **Memory Safe** — 100% Rust.
- 🧠 **Header Injection:** Global and per-route header configuration. - **High Performance** — Built with [Pingora](https://github.com/cloudflare/pingora) and tokio for async I/O.
- 🧪 **Health Checks:** Pluggable health check methods for upstreams.
- 🛰️ **Remote Config Push:** Lightweight HTTP API to update configs from CI/CD or other systems.
--- ---
## 📁 File Structure ## Configuration Overview
``` ### `main.yaml`
.
├── main.yaml # Main configuration loaded at startup | Key | Example Value | Description |
├── upstreams.yaml # Watched config with upstream mappings |----------------------------------|------------------------|----------------------------------------------------------------------------------------------------|
├── etc/ | **threads** | 12 | Number of running daemon threads. Optional, defaults to 1 |
│ ├── server.crt # TLS certificate (required if using TLS) | **runuser** | aralez | Optional, Username for running aralez after dropping root privileges, requires to launch as root |
│ └── key.pem # TLS private key | **rungroup** | aralez | Optional,Group for running aralez after dropping root privileges, requires to launch as root |
``` | **daemon** | false | Run in background (boolean) |
| **upstream_keepalive_pool_size** | 500 | Pool size for upstream keepalive connections |
| **pid_file** | /tmp/aralez.pid | Path to PID file |
| **error_log** | /tmp/aralez_err.log | Path to error log file |
| **upgrade_sock** | /tmp/aralez.sock | Path to live upgrade socket file |
| **config_address** | 0.0.0.0:3000 | HTTP API address for pushing upstreams.yaml from remote location |
| **proxy_tls_grade** | (high, medium, unsafe) | Grade of TLS ciphers, for easy configuration. High matches Qualys SSL Labs A+ (defaults to medium) |
| **config_tls_key_file** | etc/key.pem | Private Key file path. Mandatory if proxy_address_tls is set, else optional |
| **proxy_address_http** | 0.0.0.0:6193 | Aralez HTTP bind address |
| **proxy_address_tls** | 0.0.0.0:6194 | Aralez HTTPS bind address (Optional) |
| **proxy_configs** | etc/ | The top directory of config files |
| **upstreams_conf** | etc/upstreams.yaml | The location of upstreams file |
| **log_level** | info | Log level , possible values : info, warn, error, debug, trace, off |
| **hc_method** | HEAD | Healthcheck method (HEAD, GET, POST are supported) UPPERCASE |
| **hc_interval** | 2 | Interval for health checks in seconds |
| **master_key** | Random long string | Master key for working with API server and JWT Secret generation |
| **file_server_folder** | /some/local/folder | Optional, local folder to serve |
| **file_server_address** | 127.0.0.1:3002 | Optional, Local address for file server. Can set as upstream for public access |
| **config_api_enabled** | true | Boolean to enable/disable remote config push capability |
--- ---
## 🛠 Configuration Overview ## Installation
### 🔧 `main.yaml`
| Key | Example Value | Description |
|----------------------------------|--------------------------------------|----------------------------------------------------------------------------------------------------|
| **threads** | 12 | Number of running daemon threads. Optional, defaults to 1 |
| **runuser** | aralez | Optional, Username for running aralez after dropping root privileges, requires to launch as root |
| **rungroup** | aralez | Optional,Group for running aralez after dropping root privileges, requires to launch as root |
| **daemon** | false | Run in background (boolean) |
| **upstream_keepalive_pool_size** | 500 | Pool size for upstream keepalive connections |
| **pid_file** | /tmp/aralez.pid | Path to PID file |
| **error_log** | /tmp/aralez_err.log | Path to error log file |
| **upgrade_sock** | /tmp/aralez.sock | Path to live upgrade socket file |
| **config_address** | 0.0.0.0:3000 | HTTP API address for pushing upstreams.yaml from remote location |
| **config_tls_address** | 0.0.0.0:3001 | HTTPS API address for pushing upstreams.yaml from remote location |
| **config_tls_certificate** | etc/server.crt | Certificate file path for API. Mandatory if proxy_address_tls is set, else optional |
| **proxy_tls_grade** | (high, medium, unsafe) | Grade of TLS ciphers, for easy configuration. High matches Qualys SSL Labs A+ (defaults to medium) |
| **config_tls_key_file** | etc/key.pem | Private Key file path. Mandatory if proxy_address_tls is set, else optional |
| **proxy_address_http** | 0.0.0.0:6193 | Aralez HTTP bind address |
| **proxy_address_tls** | 0.0.0.0:6194 | Aralez HTTPS bind address (Optional) |
| **proxy_certificates** | etc/certs/ | The directory containing certificate and key files. In a format {NAME}.crt, {NAME}.key. |
| **upstreams_conf** | etc/upstreams.yaml | The location of upstreams file |
| **log_level** | info | Log level , possible values : info, warn, error, debug, trace, off |
| **hc_method** | HEAD | Healthcheck method (HEAD, GET, POST are supported) UPPERCASE |
| **hc_interval** | 2 | Interval for health checks in seconds |
| **master_key** | 5aeff7f9-7b94-447c-af60-e8c488544a3e | Master key for working with API server and JWT Secret generation |
| **file_server_folder** | /some/local/folder | Optional, local folder to serve |
| **file_server_address** | 127.0.0.1:3002 | Optional, Local address for file server. Can set as upstream for public access |
| **config_api_enabled** | true | Boolean to enable/disable remote config push capability |
### 🌐 `upstreams.yaml`
- `provider`: `file` or `consul`
- File-based upstreams define:
- Hostnames and routing paths
- Backend servers (load-balanced)
- Optional request headers, specific to this upstream
- Global headers (e.g., CORS) apply to all proxied responses
- Optional authentication (Basic, API Key, JWT)
---
## 🛠 Installation
Download the prebuilt binary for your architecture from releases section of [GitHub](https://github.com/sadoyan/aralez/releases) repo Download the prebuilt binary for your architecture from releases section of [GitHub](https://github.com/sadoyan/aralez/releases) repo
Make the binary executable `chmod 755 ./aralez-VERSION` and run. Make the binary executable `chmod 755 ./aralez-VERSION` and run.
File names: File names:
| File Name | Description | | File Name | Description |
|---------------------------|--------------------------------------------------------------------------| |---------------------------------|----------------------------------------------------------------------------|
| `aralez-x86_64-musl.gz` | Static Linux x86_64 binary, without any system dependency | | `aralez-x86_64-musl.gz` | Static Linux x86_64 binary, without any system dependency |
| `aralez-x86_64-glibc.gz` | Dynamic Linux x86_64 binary, with minimal system dependencies | | `aralez-x86_64-glibc.gz` | Dynamic Linux x86_64 binary, with minimal system dependencies |
| `aralez-aarch64-musl.gz` | Static Linux ARM64 binary, without any system dependency | | `aralez-x86_64-compat-musl.gz` | Static Linux x86_64 binary, compatible with old pre Haswell CPUs |
| `aralez-aarch64-glibc.gz` | Dynamic Linux ARM64 binary, with minimal system dependencies | | `aralez-x86_64-compat-glibc.gz` | Dynamic Linux x86_64 binary, compatible with old pre Haswell CPUs |
| `sadoyan/aralez` | Docker image on Debian 13 slim (https://hub.docker.com/r/sadoyan/aralez) | | `aralez-aarch64-musl.gz` | Static Linux ARM64 binary, without any system dependency |
| `aralez-aarch64-glibc.gz` | Dynamic Linux ARM64 binary, with minimal system dependencies |
| `sadoyan/aralez` | Docker image on Debian 13 slim (<https://hub.docker.com/r/sadoyan/aralez>) |
## About binaries
**glibc** builds are in general faster, but have few, basic, Glibc dependencies:
**musl** builds are 100% portable, static compiled binaries and have zero system dependencies.
In general musl builds have a little less performance.
The most intensive tests shows 107k-110k requests per second on **Glibc** binaries against 97k-100k **Musl** ones.
For running **Aralez** on very old hardware, CPUs prior Haswell, (launched before 2013) use `aralez-x86_64-compat-*.gz`
For getting the best performance on newer hardware use `aralez-x86_64-*.gz`.
**Via docker** **Via docker**
@@ -140,51 +116,50 @@ docker run -d \
sadoyan/aralez sadoyan/aralez
``` ```
## 💡 Note ## Running the Proxy
In general **glibc** builds are working faster, but have few, basic, system dependencies for example :
```
linux-vdso.so.1 (0x00007ffeea33b000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f09e7377000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f09e6320000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f09e613f000)
/lib64/ld-linux-x86-64.so.2 (0x00007f09e73b1000)
```
These are common to any Linux systems, so the binary should work on almost any Linux system.
**musl** builds are 100% portable, static compiled binaries and have zero system depencecies.
In general musl builds have a little less performance.
The most intensive tests shows 107k-110k requests per second on **Glibc** binaries against 97k-100k **Musl** ones.
## 🔌 Running the Proxy
```bash ```bash
./aralez -c path/to/main.yaml ./aralez -c path/to/main.yaml
``` ```
## 🔌 Systemd integration ## Systemd integration
Assuming Arales in installed in `/opt/aralez` folder
```bash ```bash
cat > /etc/systemd/system/aralez.service <<EOF cat > /etc/systemd/system/aralez.service <<EOF
[Unit]
Description=meilisearch
Documentation=https://github.com/sadoyan/aralez
Wants=network-online.target
After=network-online.target
[Service] [Service]
Type=forking WorkingDirectory = /opt/aralez/
PIDFile=/run/aralez.pid ExecReload=/bin/kill -HUP
ExecStart=/bin/aralez -d -c /etc/aralez.conf ExecStart=/opt/aralez/aralez -c /opt/aralez/proxyconfigs/main.yaml
ExecReload=kill -QUIT $MAINPID KillMode=process
ExecReload=/bin/aralez -u -d -c /etc/aralez.conf KillSignal=SIGINT
LimitNOFILE=infinity
LimitNPROC=infinity
Restart=on-failure
RestartSec=2
StartLimitBurst=3
StartLimitIntervalSec=10
TasksMax=infinity
[Install]
WantedBy=multi-user.target
EOF EOF
``` ```
```bash ```bash
systemctl daemon-reload
systemctl enable aralez.service. systemctl enable aralez.service.
systemctl restart aralez.service. systemctl restart aralez.service.
``` ```
## 💡 Example ## Example upstreams config
A sample `upstreams.yaml` entry:
```yaml ```yaml
provider: "file" provider: "file"
@@ -198,9 +173,6 @@ client_headers:
- "Access-Control-Allow-Origin:*" - "Access-Control-Allow-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS" - "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400" - "Access-Control-Max-Age:86400"
authorization:
type: "jwt"
creds: "910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774"
myhost.mydomain.com: myhost.mydomain.com:
paths: paths:
"/": "/":
@@ -217,6 +189,9 @@ myhost.mydomain.com:
- "127.0.0.2:8000" - "127.0.0.2:8000"
"/foo": "/foo":
to_https: true to_https: true
authorization:
type: "jwt"
data: "266463d1-210a-4787-9a81-4aacb37a8723"
client_headers: client_headers:
- "X-Another-Header:Hohohohoho" - "X-Another-Header:Hohohohoho"
servers: servers:
@@ -226,6 +201,11 @@ myhost.mydomain.com:
healthcheck: false healthcheck: false
servers: servers:
- "127.0.0.1:8001" - "127.0.0.1:8001"
DEFAULT:
paths:
"/":
servers:
- "127.0.0.1:3000"
``` ```
**This means:** **This means:**
@@ -241,6 +221,7 @@ myhost.mydomain.com:
- Requests to `myhost.mydomain.com/` will be limited to 20 requests per second. - Requests to `myhost.mydomain.com/` will be limited to 20 requests per second.
- Requests to `myhost.mydomain.com/` will be proxied to `127.0.0.1` and `127.0.0.2`. - Requests to `myhost.mydomain.com/` will be proxied to `127.0.0.1` and `127.0.0.2`.
- Plain HTTP to `myhost.mydomain.com/foo` will get 301 redirect to configured TLS port of Aralez. - Plain HTTP to `myhost.mydomain.com/foo` will get 301 redirect to configured TLS port of Aralez.
- `myhost.mydomain.com/foo` will require authentication with JWT token, signed by `266463d1-210a-4787-9a81-4aacb37a8723`.
- Requests to `myhost.mydomain.com/foo` will be proxied to `127.0.0.4` and `127.0.0.5`. - Requests to `myhost.mydomain.com/foo` will be proxied to `127.0.0.4` and `127.0.0.5`.
- Requests to `myhost.mydomain.com/.well-known/acme-challenge` will be proxied to `127.0.0.1:8001`, but healthcheks are disabled. - Requests to `myhost.mydomain.com/.well-known/acme-challenge` will be proxied to `127.0.0.1:8001`, but healthcheks are disabled.
- SSL/TLS for upstreams is detected automatically, no need to set any config parameter. - SSL/TLS for upstreams is detected automatically, no need to set any config parameter.
@@ -252,27 +233,30 @@ myhost.mydomain.com:
- All requests to servers will require JWT token authentication (You can comment out the authorization to disable it), - All requests to servers will require JWT token authentication (You can comment out the authorization to disable it),
- Firs parameter specifies the mechanism of authorisation `jwt` - Firs parameter specifies the mechanism of authorisation `jwt`
- Second is the secret key for validating `jwt` tokens - Second is the secret key for validating `jwt` tokens
- `DEFAULT` catch up everything else and proxy to `127.0.0.1:3000`
--- ---
## 🔄 Hot Reload ## Hot Reload
- Changes to `upstreams.yaml` are applied immediately. - Changes to `upstreams.yaml` are applied immediately on save without restart .
- No need to restart the proxy — just save the file. - If `consul` or `kubernetes` provider is chosen, upstreams will be periodically update from API.
- If `consul` provider is chosen, upstreams will be periodically update from Consul's API.
--- ---
## 🔐 TLS Support ## TLS Support
To enable TLS for A proxy server: Currently only OpenSSL is supported, working on Boringssl and Rustls To enable TLS for the proxy server.
1. Set `proxy_address_tls` in `main.yaml` - Set `proxy_address_tls` in `main.yaml`
2. Provide `tls_certificate` and `tls_key_file` - Provide at least on `tls_certificate/tls_key_file` pair.
- First pair is required to create the TLS listener.
- This pair can be anything, even self-signed with dummy domain.
- After getting normal certificate it can be deleted
--- ---
## 📡 Remote Config API ## Remote Config API
Push new `upstreams.yaml` over HTTP to `config_address` (`:3000` by default). Useful for CI/CD automation or remote config updates. Push new `upstreams.yaml` over HTTP to `config_address` (`:3000` by default). Useful for CI/CD automation or remote config updates.
URL parameter. `key=MASTERKEY` is required. `MASTERKEY` is the value of `master_key` in the `main.yaml` URL parameter. `key=MASTERKEY` is required. `MASTERKEY` is the value of `master_key` in the `main.yaml`
@@ -283,7 +267,7 @@ curl -XPOST --data-binary @./etc/upstreams.txt 127.0.0.1:3000/conf?key=${MASTERK
--- ---
## 🔐 Authentication (Optional) ## Authentication (Optional)
- Adds authentication to all requests. - Adds authentication to all requests.
- Only one method can be active at a time. - Only one method can be active at a time.
@@ -337,13 +321,13 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/
``` ```
## 📃 License ## License
[Apache License Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) [Apache License Version 2.0](https://www.apache.org/licenses/LICENSE-2.0)
--- ---
## 🧠 Notes ## Notes
- Uses Pingora under the hood for efficiency and flexibility. - Uses Pingora under the hood for efficiency and flexibility.
- Designed for edge proxying, internal routing, or hybrid cloud scenarios. - Designed for edge proxying, internal routing, or hybrid cloud scenarios.
@@ -352,37 +336,42 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/
- Sticky session support. - Sticky session support.
- HTTP2 ready. - HTTP2 ready.
### 🧩 Summary Table: Feature Comparison ### Summary Table: Feature Comparison
| Feature / Proxy | **Aralez** | **Nginx** | **HAProxy** | **Traefik** | **Caddy** | **Envoy** | | Feature / Proxy | **Aralez** | **Nginx** | **HAProxy** | **Traefik** | **Caddy** | **Envoy** |
|----------------------------------|:-----------------:|:---------------------------:|:-----------------:|:--------------------------------:|:---------------:|:---------------:| |--------------------|:----------:|:-----------:|:-----------:|:-----------:|:----------:|:---------:|
| **Hot Reload (Zero Downtime)** | ✅ **Automatic** | ⚙️ Manual (graceful reload) | ⚙️ Manual | ✅ Automatic | ✅ Automatic | ✅ Automatic | | **Reload** | Hot | ⚙️ Manual | ⚙️ Manual | ✅ Hot | ✅ Hot | ✅ Hot |
| **Auto Cert Reload (from disk)** | **Automatic** | No | ❌ No |Automatic (Let's Encrypt only) | ✅ Automatic | ⚙️ Manual | | **Cert load** | ✅ Hot |Reload | ❌ Reload |Yes | ✅ Yes | ⚙️ No ? |
| **Auth: Basic / API Key / JWT** | ✅ **Built-in** | ⚙️ Basic only | ⚙️ Basic only | ✅ Config-based | ✅ Config-based |Config-based | | **Authentication** | ✅ Yes | ⚙️ Limited | ⚙️ Limited | ✅ Yes | ✅ Yes |Yes |
| **TLS / HTTP2 Termination** | ✅ **Automatic** | ⚙️ Manual config | ⚙️ Manual config | ✅ Automatic | ✅ Automatic | ✅ Automatic | | **HTTP2** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **Built-in A+ TLS Grades** | **Automatic** | ⚙️ Manual tuning | ⚙️ Manual | ⚙️ Manual | ✅ Automatic | ⚙️ Manual | | **TLS Grades** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ⚙️ Manual |
| **gRPC Proxy** | ✅ **Zero-Config** | ⚙️ Manual setup | ⚙️ Manual | ⚙️ Needs config | ⚙️ Needs config | ⚙️ Needs config | | **gRPC** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual |
| **SSL Proxy** | ✅ **Zero-Config** | ⚙️ Manual | ⚙️ Manual | ✅ Automatic | ✅ Automatic | ✅ Automatic | | **SSL Proxy** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **HTTP/2 Proxy** | ✅ **Zero-Config** | ⚙️ Manual enable | ⚙️ Manual enable | ✅ Automatic | ✅ Automatic | ✅ Automatic | | **HTTP/2** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **WebSocket Proxy** | ✅ **Zero-Config** | ⚙️ Manual upgrade | ⚙️ Manual upgrade | ✅ Automatic | ✅ Automatic | ✅ Automatic | | **WebSocket** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **Sticky Sessions** | ✅ **Built-in** | ⚙️ Config-based | ⚙️ Config-based | ✅ Automatic | ⚙️ Limited | ✅ Config-based | | **Sticky Session** | ✅ Yes | ❌ No | ⚙️ Yes | ✅ Yes | ⚙️ Limited | ✅ Manual |
| **Prometheus Metrics** | ✅ **Built-in** | ⚙️ External exporter | ✅ Built-in | ✅ Built-in | ✅ Built-in | ✅ Built-in | | **Prometheus** | ✅ Yes | ⚙️ External | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| **Consul Integration** | ✅ **Yes** | ❌ No | ⚙️ Via DNS only | ✅ Yes | ❌ No | ✅ Yes | | **Consul** | ✅ Yes | ❌ No | ⚙DNS API | ✅ Yes | ❌ No | ✅ Yes |
| **Kubernetes Integration** | **Yes** | ⚙️ Needs ingress setup | ⚙️ External | ✅ Yes | ⚙️ Limited | ✅ Yes | | **Kubernetes** |Yes | ⚙️ Ingress | ⚙️ External | ✅ Yes | ⚙️ Limited | ✅ Yes |
| **Request Limiter** | **Yes** | ✅ Config-based |Config-based | ✅ Config-based | ✅ Config-based |Config-based | | **Limiter** | ✅ Yes | ✅ Yes |Yes | ✅ Yes | ✅ Yes |Yes |
| **Serve Static Files** | ✅ **Yes** | ✅ Yes | ⚙️ Basic | ✅ Automatic | ✅ Automatic | ❌ No | | **Static Files** | ✅ Yes | ✅ Yes | ⚙️ Lua ? | ✅ Yes | ✅ Yes | ❌ No |
| **Upstream Health Checks** | ✅ **Automatic** | ⚙️ Manual config | ⚙️ Manual config | ✅ Automatic | ✅ Automatic | ✅ Automatic | | **Health Checks** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **Built With** | 🦀 **Rust** | C | C | Go | Go | C++ | | **Built With** | Rust | C | C | Go | Go | C++ |
--- ---
**Automatic / Zero-Config** Works immediately, no setup required **Auto** Automatically detected and loaded
⚙️ **Manual / Config-based** Requires explicit configuration or modules **Hot** Works immediately, no reload/restart is required
**Yes** Works immediately, no setup required
⚙️ **Manual** Requires explicit configuration or modules
⚙️ **Reload** Reload or restart is required
⚙️ **Limited** Support is limited to certain features
⚙️ **External** Requires an external module
**No** Not supported **No** Not supported
## 💡 Simple benchmark by [Oha](https://github.com/hatoo/oha) ## Simple benchmark by [Oha](https://github.com/hatoo/oha)
⚠️ These benchmarks use : **These benchmarks use :**
- 3 async Rust echo servers on a local network with 1Gbit as upstreams. - 3 async Rust echo servers on a local network with 1Gbit as upstreams.
- A dedicated server for running **Aralez** - A dedicated server for running **Aralez**
@@ -411,7 +400,7 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/
- "192.168.211.212:8000" - "192.168.211.212:8000"
``` ```
## 💡 Results reflect synthetic performance under optimal conditions. ## Results reflect synthetic performance under optimal conditions.
- CPU : Intel(R) Xeon(R) CPU E3-1270 v6 @ 3.80GHz - CPU : Intel(R) Xeon(R) CPU E3-1270 v6 @ 3.80GHz
- 300 : simultaneous connections - 300 : simultaneous connections
@@ -420,16 +409,16 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/
``` ```
Summary: Summary:
Success rate: 100.00% Success rate: 100.00%
Total: 600.0027 secs Total: 600.0027 secs
Slowest: 0.2138 secs Slowest: 0.2138 secs
Fastest: 0.0002 secs Fastest: 0.0002 secs
Average: 0.0023 secs Average: 0.0023 secs
Requests/sec: 129777.3838 Requests/sec: 129777.3838
Total data: 0 B Total data: 0 B
Size/request: 0 B Size/request: 0 B
Size/sec: 0 B Size/sec: 0 B
Response time histogram: Response time histogram:
0.000 [1] | 0.000 [1] |
@@ -457,8 +446,8 @@ Response time distribution:
Details (average, fastest, slowest): Details (average, fastest, slowest):
DNS+dialup: 0.0161 secs, 0.0002 secs, 0.0316 secs DNS+dialup: 0.0161 secs, 0.0002 secs, 0.0316 secs
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs
Status code distribution: Status code distribution:
[200] 77866624 responses [200] 77866624 responses
@@ -476,16 +465,16 @@ Error distribution:
``` ```
Summary: Summary:
Success rate: 100.00% Success rate: 100.00%
Total: 600.0021 secs Total: 600.0021 secs
Slowest: 0.2182 secs Slowest: 0.2182 secs
Fastest: 0.0002 secs Fastest: 0.0002 secs
Average: 0.0024 secs Average: 0.0024 secs
Requests/sec: 123870.5820 Requests/sec: 123870.5820
Total data: 0 B Total data: 0 B
Size/request: 0 B Size/request: 0 B
Size/sec: 0 B Size/sec: 0 B
Response time histogram: Response time histogram:
0.000 [1] | 0.000 [1] |
@@ -513,8 +502,8 @@ Response time distribution:
Details (average, fastest, slowest): Details (average, fastest, slowest):
DNS+dialup: 0.0066 secs, 0.0002 secs, 0.0210 secs DNS+dialup: 0.0066 secs, 0.0002 secs, 0.0210 secs
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs
Status code distribution: Status code distribution:
[200] 74322377 responses [200] 74322377 responses
@@ -525,7 +514,7 @@ Error distribution:
![Aralez](https://netangels.net/utils/musl10.png) ![Aralez](https://netangels.net/utils/musl10.png)
## 🚀 Aralez, Nginx, Traefik performance benchmark ## Aralez, Nginx, Traefik performance benchmark
This benchmark is done on 4 servers. With CPU Intel(R) Xeon(R) E-2174G CPU @ 3.80GHz, 64 GB RAM. This benchmark is done on 4 servers. With CPU Intel(R) Xeon(R) E-2174G CPU @ 3.80GHz, 64 GB RAM.
@@ -541,3 +530,9 @@ The results show requests per second performed by Load balancer. You can see 3 b
2. Requests to via http2 to SSL endpoint. 2. Requests to via http2 to SSL endpoint.
3. Mixed workload with plain http1.1 and htt2 SSL. 3. Mixed workload with plain http1.1 and htt2 SSL.
## Links
- [**Documentation**](https://aralez.rs) : The manual you should read
- [**Downloads**](https://github.com/sadoyan/aralez/releases) : Binary downloads
- [**Issues**](https://github.com/sadoyan/aralez/issues) : Issues and requests

BIN
assets/bench2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -1,7 +1,7 @@
# Main configuration file, applied on startup # Main configuration file, applied on startup
threads: 12 # Number of daemon threads default setting threads: 12 # Number of daemon threads default setting
#runuser: pastor # Username for running aralez after dropping root privileges, requires program to start as root runuser: aralez # Username for running aralez after dropping root privileges, requires program to start as root
#rungroup: pastor # Group for running aralez after dropping root privileges, requires program to start as root rungroup: aralez # Group for running aralez after dropping root privileges, requires program to start as root
daemon: false # Run in background daemon: false # Run in background
upstream_keepalive_pool_size: 500 # Pool size for upstream keepalive connections upstream_keepalive_pool_size: 500 # Pool size for upstream keepalive connections
pid_file: /tmp/aralez.pid # Path to PID file pid_file: /tmp/aralez.pid # Path to PID file
@@ -9,17 +9,14 @@ error_log: /tmp/aralez_err.log # Path to error log
upgrade_sock: /tmp/aralez.sock # Path to socket file upgrade_sock: /tmp/aralez.sock # Path to socket file
config_api_enabled: true # Boolean to enable/disable remote config push capability. config_api_enabled: true # Boolean to enable/disable remote config push capability.
config_address: 0.0.0.0:3000 # HTTP API address for pushing upstreams.yaml from remote location config_address: 0.0.0.0:3000 # HTTP API address for pushing upstreams.yaml from remote location
config_tls_address: 0.0.0.0:3001 # HTTP TLS API address for pushing upstreams.yaml from remote location
config_tls_certificate: /etc/server.crt # Mandatory if config_tls_address is set
config_tls_key_file: /etc/key.pem # Mandatory if config_tls_address is set
proxy_address_http: 0.0.0.0:6193 # Proxy HTTP bind address proxy_address_http: 0.0.0.0:6193 # Proxy HTTP bind address
proxy_address_tls: 0.0.0.0:6194 # Optional, Proxy TLS bind address proxy_address_tls: 0.0.0.0:6194 # Optional, Proxy TLS bind address
proxy_certificates: /etc/certs # Mandatory if proxy_address_tls set, should contain a certificate and key files strictly in a format {NAME}.crt, {NAME}.key. proxy_configs: /opt/aralez/etc # Mandatory if proxy_address_tls set, should contain a certificate and key files strictly in a format {NAME}.crt, {NAME}.key.
proxy_tls_grade: a+ # Grade of TLS suite for proxy (a+, a, b, c, unsafe), matching grades of Qualys SSL Labs proxy_tls_grade: a+ # Grade of TLS suite for proxy (a+, a, b, c, unsafe), matching grades of Qualys SSL Labs
upstreams_conf: /etc/upstreams.yaml # the location of upstreams file upstreams_conf: /opt/aralez/etc/upstreams.yaml # the location of upstreams file
file_server_folder: /opt/storage # Optional, local folder to serve file_server_folder: /opt/storage # Optional, local folder to serve
file_server_address: 127.0.0.1:3002 # Optional, Local address for file server. Can set as upstream for public access. file_server_address: 127.0.0.1:3002 # Optional, Local address for file server. Can set as upstream for public access.
log_level: info # info, warn, error, debug, trace, off log_level: info # info, warn, error, debug, trace, off
hc_method: HEAD # Healthcheck method (HEAD, GET, POST are supported) UPPERCASE hc_method: HEAD # Healthcheck method (HEAD, GET, POST are supported) UPPERCASE
hc_interval: 2 #Interval for health checks in seconds hc_interval: 2 #Interval for health checks in seconds
master_key: 910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774 # Mater key for working with API server and JWT Secret master_key: 910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774 # Mater key for working with API server and JWT Secret

View File

@@ -43,7 +43,7 @@ kubernetes:
path: "/" path: "/"
upstream: "webapi-service" upstream: "webapi-service"
- hostname: "webapi-service" - hostname: "webapi-service"
upstream: "vt-console-service" upstream: "console-service"
path: "/one" path: "/one"
client_headers: client_headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa" - "X-Some-Thing:Yaaaaaaaaaaaaaaa"
@@ -51,7 +51,7 @@ kubernetes:
rate_limit: 100 rate_limit: 100
to_https: false to_https: false
- hostname: "webapi-service" - hostname: "webapi-service"
upstream: "vt-rambulik-service" upstream: "rambul-service"
path: "/two" path: "/two"
- hostname: "websocket-service" - hostname: "websocket-service"
upstream: "websocket-service" upstream: "websocket-service"
@@ -72,6 +72,9 @@ upstreams:
- "127.0.0.4:8000" - "127.0.0.4:8000"
- "127.0.0.5:8000" - "127.0.0.5:8000"
"/ping": "/ping":
authorization: # Will be ignored if global authentication is enabled.
type: "basic"
creds: "admin:admin"
to_https: false to_https: false
server_headers: server_headers:
- "X-Forwarded-Proto:https" - "X-Forwarded-Proto:https"
@@ -107,9 +110,10 @@ upstreams:
healthcheck: false healthcheck: false
servers: servers:
- "127.0.0.1:8001" - "127.0.0.1:8001"
localpost: rdr.mydomain.com:
paths: paths:
"/": "/":
to_https: false redirect_to: "https://som.other.domain:6194"
healthcheck: false
servers: servers:
- "127.0.0.1:9000" - "127.0.0.1:8080"

View File

@@ -1,8 +1,10 @@
mod tls;
mod utils; mod utils;
mod web; mod web;
#[global_allocator] #[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
// pub static A: CountingAllocator = CountingAllocator;
fn main() { fn main() {
web::start::run(); web::start::run();

3
src/tls.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod acme;
pub mod grades;
pub mod load;

2
src/tls/acme.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod account;
pub mod order;

58
src/tls/acme/account.rs Normal file
View File

@@ -0,0 +1,58 @@
use instant_acme::{Account, AccountCredentials, LetsEncrypt, NewAccount};
use log::info;
use std::fs;
use std::path::Path;
use std::sync::OnceLock;
static ACCOUNT: OnceLock<Account> = OnceLock::new();
pub async fn get_account(file: &str) -> Result<&'static Account, Box<dyn std::error::Error>> {
if let Some(account) = ACCOUNT.get() {
return Ok(account);
}
if let Some(credentials) = load_credentials(file) {
let acc_builder = Account::builder()?;
let account = acc_builder.from_credentials(credentials).await?;
let _ = ACCOUNT.set(account);
info!("Loaded existing ACME account");
} else {
info!("No existing credentials found, creating new account");
create_account(file).await?;
}
ACCOUNT.get().ok_or("Failed to initialize account".into())
}
async fn create_account(file: &str) -> Result<(), Box<dyn std::error::Error>> {
let new_account = NewAccount {
contact: &[],
terms_of_service_agreed: true,
only_return_existing: false,
};
let acc_builder = Account::builder()?;
let (account, credentials) = acc_builder.create(&new_account, LetsEncrypt::Production.url().to_string(), None).await?;
// let (account, credentials) = acc_builder.create(&new_account, LetsEncrypt::Staging.url().to_string(), None).await?;
info!("Account created: {:?}", account.id());
save_credentials(&credentials, file)?;
let _ = ACCOUNT.set(account);
Ok(())
}
pub async fn load_or_create(file: &str) -> Result<String, Box<dyn std::error::Error>> {
let account = get_account(file).await?;
Ok(account.id().to_string() + "\n")
}
fn save_credentials(credentials: &AccountCredentials, file: &str) -> Result<(), Box<dyn std::error::Error>> {
let json = serde_json::to_string_pretty(credentials)?;
fs::write(file, json)?;
info!("ACME credentials saved to {}", file);
Ok(())
}
fn load_credentials(file: &str) -> Option<AccountCredentials> {
if !Path::new(file).exists() {
return None;
}
let json = fs::read_to_string(file).ok()?;
serde_json::from_str(&json).ok()
}

91
src/tls/acme/order.rs Normal file
View File

@@ -0,0 +1,91 @@
use crate::tls::acme::account::get_account;
use crate::utils::parceyaml::DOMAINS;
use instant_acme::{ChallengeType, Identifier, NewOrder, RetryPolicy};
use log::{error, info};
use pingora::prelude::sleep;
use rcgen::{CertificateParams, DistinguishedName, KeyPair};
use std::collections::HashMap;
use std::fs;
use std::sync::{LazyLock, RwLock};
use std::time::Duration;
use x509_parser::prelude::*;
pub static CHALLENGES: LazyLock<RwLock<HashMap<String, String>>> = LazyLock::new(|| RwLock::new(HashMap::new()));
pub async fn refresh_order(certs_dir: String, autoconf_dir: String) {
let credsfile = autoconf_dir + "/acme_credentials.json";
loop {
for item in DOMAINS.iter() {
let _what = order(item.key(), credsfile.as_str(), certs_dir.clone()).await;
}
sleep(Duration::from_secs(12 * 3600)).await;
}
}
pub async fn order(domain: &str, credsfile: &str, certs_dir: String) -> Result<String, Box<dyn std::error::Error>> {
let crt = certs_dir.clone() + "/" + domain + ".crt";
let key = certs_dir.clone() + "/" + domain + ".key";
if DOMAINS.get(domain).is_none() {
DOMAINS.insert(domain.to_string(), true);
let mut newlist: Vec<String> = Vec::new();
for item in DOMAINS.iter() {
newlist.push(item.key().to_string());
}
if let Ok(json_content) = serde_json::to_string_pretty(&newlist) {
let autocfg_file = credsfile.replace("/acme_credentials.json", "/domains.json");
if let Err(err) = std::fs::write(&autocfg_file, json_content) {
error!("Error Updating domains for certificates: {} : {}", domain, err);
return Err(Box::from(err));
}
}
}
if let Ok(expiry) = cert_expiry(crt.as_str()) {
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs();
if expiry > now + 30 * 24 * 3600 {
// println!("Fresh certificate exists. Not renewing !");
return Ok("Fresh certificate exists. Not renewing ! \n".to_string());
}
};
let account = get_account(credsfile).await?;
let mut order = account.new_order(&NewOrder::new(&[Identifier::Dns(domain.to_string())])).await?;
let mut authorizations = order.authorizations();
while let Some(auth) = authorizations.next().await {
let mut auth = auth?;
let mut challenge_handle = auth.challenge(ChallengeType::Http01).ok_or("no http01 challenge found")?;
let key_auth = challenge_handle.key_authorization();
let key_auth_str = key_auth.as_str().to_string();
let token = key_auth_str.split('.').next().ok_or("invalid key authorization")?.to_string();
CHALLENGES.write().unwrap().insert(token, key_auth_str);
challenge_handle.set_ready().await?;
}
let status = order.poll_ready(&RetryPolicy::default()).await?;
info!("ACME poll_ready status: {:?}", status);
let mut params = CertificateParams::new(vec![domain.to_owned()])?;
params.distinguished_name = DistinguishedName::new();
let private_key = KeyPair::generate()?;
let signing_request = params.serialize_request(&private_key)?;
let csr_der = signing_request.der();
order.finalize_csr(csr_der).await?;
// poll for certificate
let cert_chain_pem = order.poll_certificate(&RetryPolicy::default()).await?;
CHALLENGES.write().unwrap().clear();
let private_key_pem = private_key.serialize_pem();
fs::write(crt, cert_chain_pem)?;
fs::write(key, private_key_pem)?;
Ok("Certificate is successfully generated \n".to_string())
}
fn cert_expiry(path: &str) -> Result<u64, Box<dyn std::error::Error>> {
let pem = fs::read(path)?;
let (_, pem) = parse_x509_pem(&pem)?;
let (_, cert) = parse_x509_certificate(&pem.contents)?;
let expiry = cert.validity().not_after.timestamp() as u64;
Ok(expiry)
}

75
src/tls/grades.rs Normal file
View File

@@ -0,0 +1,75 @@
use log::{info, warn};
use pingora::tls::ssl::{select_next_proto, AlpnError, SslRef, SslVersion};
use pingora_core::listeners::tls::TlsSettings;
#[derive(Debug)]
pub struct CipherSuite {
pub high: &'static str,
pub medium: &'static str,
pub legacy: &'static str,
}
const CIPHERS: CipherSuite = CipherSuite {
high: "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305",
medium: "ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:AES128-GCM-SHA256",
legacy: "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH",
};
#[derive(Debug)]
pub enum TlsGrade {
High,
Medium,
Legacy,
}
impl TlsGrade {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"high" => Some(TlsGrade::High),
"medium" => Some(TlsGrade::Medium),
"unsafe" => Some(TlsGrade::Legacy),
_ => None,
}
}
}
pub fn prefer_h2<'a>(_ssl: &mut SslRef, alpn_in: &'a [u8]) -> Result<&'a [u8], AlpnError> {
match select_next_proto("\x02h2\x08http/1.1".as_bytes(), alpn_in) {
Some(p) => Ok(p),
_ => Err(AlpnError::NOACK),
}
}
pub fn set_tsl_grade(tls_settings: &mut TlsSettings, grade: &str) {
let config_grade = TlsGrade::from_str(grade);
match config_grade {
Some(TlsGrade::High) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1_2));
// let _ = tls_settings.set_max_proto_version(Some(SslVersion::TLS1_3));
let _ = tls_settings.set_cipher_list(CIPHERS.high);
// let _ = tls_settings.set_ciphersuites(CIPHERS.high);
let _ = tls_settings.set_cipher_list(CIPHERS.high);
info!("TLS grade: {:?}, => High", tls_settings.options());
}
Some(TlsGrade::Medium) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1));
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
// let _ = tls_settings.set_ciphersuites(CIPHERS.medium);
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
info!("TLS grade: {:?}, => Medium", tls_settings.options());
}
Some(TlsGrade::Legacy) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::SSL3));
let _ = tls_settings.set_cipher_list(CIPHERS.legacy);
// let _ = tls_settings.set_ciphersuites(CIPHERS.legacy);
let _ = tls_settings.set_cipher_list(CIPHERS.legacy);
warn!("TLS grade: {:?}, => UNSAFE", tls_settings.options());
}
None => {
// Defaults to Medium
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1));
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
// let _ = tls_settings.set_ciphersuites(CIPHERS.medium);
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
warn!("TLS grade is not detected defaulting top Medium");
}
}
}

View File

@@ -1,7 +1,7 @@
use crate::tls::grades;
use dashmap::DashMap; use dashmap::DashMap;
use log::{error, info, warn}; use log::error;
use pingora::tls::ssl::{select_next_proto, AlpnError, NameType, SniError, SslAlert, SslContext, SslFiletype, SslMethod, SslRef, SslVersion}; use pingora::tls::ssl::{NameType, SniError, SslAlert, SslContext, SslFiletype, SslMethod, SslRef};
use pingora_core::listeners::tls::TlsSettings;
use rustls_pemfile::{read_one, Item}; use rustls_pemfile::{read_one, Item};
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashSet; use std::collections::HashSet;
@@ -10,7 +10,6 @@ use std::io::BufReader;
use x509_parser::extensions::GeneralName; use x509_parser::extensions::GeneralName;
use x509_parser::nom::Err as NomErr; use x509_parser::nom::Err as NomErr;
use x509_parser::prelude::*; use x509_parser::prelude::*;
#[derive(Clone, Deserialize, Debug)] #[derive(Clone, Deserialize, Debug)]
pub struct CertificateConfig { pub struct CertificateConfig {
pub cert_path: String, pub cert_path: String,
@@ -18,14 +17,14 @@ pub struct CertificateConfig {
} }
#[derive(Debug)] #[derive(Debug)]
struct CertificateInfo { pub struct CertificateInfo {
common_names: Vec<String>, pub common_names: Vec<String>,
alt_names: Vec<String>, pub alt_names: Vec<String>,
ssl_context: SslContext, pub ssl_context: SslContext,
#[allow(dead_code)] #[allow(dead_code)]
cert_path: String, // Only used for logging pub cert_path: String, // Only used for logging
#[allow(dead_code)] #[allow(dead_code)]
key_path: String, // Only used for logging pub key_path: String, // Only used for logging
} }
#[derive(Debug)] #[derive(Debug)]
@@ -61,7 +60,7 @@ impl Certificates {
} }
} }
Some(Self { Some(Self {
name_map: name_map, name_map,
configs: cert_infos, configs: cert_infos,
default_cert_path: default_cert.cert_path.clone(), default_cert_path: default_cert.cert_path.clone(),
default_key_path: default_cert.key_path.clone(), default_key_path: default_cert.key_path.clone(),
@@ -94,7 +93,7 @@ impl Certificates {
if let Some(name) = server_name { if let Some(name) = server_name {
match self.find_ssl_context(name) { match self.find_ssl_context(name) {
Some(ctx) => { Some(ctx) => {
ssl_ref.set_ssl_context(&*ctx).map_err(|_| SniError::ALERT_FATAL)?; ssl_ref.set_ssl_context(&ctx).map_err(|_| SniError::ALERT_FATAL)?;
} }
None => { None => {
log::debug!("No matching server name found"); log::debug!("No matching server name found");
@@ -106,7 +105,7 @@ impl Certificates {
} }
} }
fn load_cert_info(cert_path: &str, key_path: &str, _grade: &str) -> Option<CertificateInfo> { pub fn load_cert_info(cert_path: &str, key_path: &str, _grade: &str) -> Option<CertificateInfo> {
let mut common_names = HashSet::new(); let mut common_names = HashSet::new();
let mut alt_names = HashSet::new(); let mut alt_names = HashSet::new();
@@ -180,75 +179,7 @@ fn create_ssl_context(cert_path: &str, key_path: &str) -> Result<SslContext, Box
let mut ctx = SslContext::builder(SslMethod::tls())?; let mut ctx = SslContext::builder(SslMethod::tls())?;
ctx.set_certificate_chain_file(cert_path)?; ctx.set_certificate_chain_file(cert_path)?;
ctx.set_private_key_file(key_path, SslFiletype::PEM)?; ctx.set_private_key_file(key_path, SslFiletype::PEM)?;
ctx.set_alpn_select_callback(prefer_h2); ctx.set_alpn_select_callback(grades::prefer_h2);
let built = ctx.build(); let built = ctx.build();
Ok(built) Ok(built)
} }
#[derive(Debug)]
pub struct CipherSuite {
pub high: &'static str,
pub medium: &'static str,
pub legacy: &'static str,
}
const CIPHERS: CipherSuite = CipherSuite {
high: "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305",
medium: "ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:AES128-GCM-SHA256",
legacy: "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH",
};
#[derive(Debug)]
pub enum TlsGrade {
HIGH,
MEDIUM,
LEGACY,
}
impl TlsGrade {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"high" => Some(TlsGrade::HIGH),
"medium" => Some(TlsGrade::MEDIUM),
"unsafe" => Some(TlsGrade::LEGACY),
_ => None,
}
}
}
pub fn prefer_h2<'a>(_ssl: &mut SslRef, alpn_in: &'a [u8]) -> Result<&'a [u8], AlpnError> {
match select_next_proto("\x02h2\x08http/1.1".as_bytes(), alpn_in) {
Some(p) => Ok(p),
_ => Err(AlpnError::NOACK),
}
}
pub fn set_tsl_grade(tls_settings: &mut TlsSettings, grade: &str) {
let config_grade = TlsGrade::from_str(grade);
match config_grade {
Some(TlsGrade::HIGH) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1_2));
// let _ = tls_settings.set_max_proto_version(Some(SslVersion::TLS1_3));
let _ = tls_settings.set_cipher_list(CIPHERS.high);
let _ = tls_settings.set_ciphersuites(CIPHERS.high);
info!("TLS grade: {:?}, => HIGH", tls_settings.options());
}
Some(TlsGrade::MEDIUM) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1));
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
let _ = tls_settings.set_ciphersuites(CIPHERS.medium);
info!("TLS grade: {:?}, => MEDIUM", tls_settings.options());
}
Some(TlsGrade::LEGACY) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::SSL3));
let _ = tls_settings.set_cipher_list(CIPHERS.legacy);
let _ = tls_settings.set_ciphersuites(CIPHERS.legacy);
warn!("TLS grade: {:?}, => UNSAFE", tls_settings.options());
}
None => {
// Defaults to MEDIUM
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1));
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
let _ = tls_settings.set_ciphersuites(CIPHERS.medium);
warn!("TLS grade is not detected defaulting top MEDIUM");
}
}
}

View File

@@ -1,7 +1,7 @@
pub mod auth; pub mod auth;
pub mod discovery; pub mod discovery;
pub mod dnsclient;
mod filewatch; mod filewatch;
pub mod fordebug;
pub mod healthcheck; pub mod healthcheck;
pub mod httpclient; pub mod httpclient;
pub mod jwt; pub mod jwt;
@@ -10,6 +10,5 @@ pub mod metrics;
pub mod parceyaml; pub mod parceyaml;
pub mod state; pub mod state;
pub mod structs; pub mod structs;
pub mod tls;
pub mod tools; pub mod tools;
// pub mod watchksecret; // pub mod watchksecret;

View File

@@ -1,41 +1,187 @@
use crate::utils::jwt::check_jwt; use crate::utils::jwt::check_jwt;
// use reqwest::Client;
use axum::http::StatusCode;
use base64::engine::general_purpose::STANDARD; use base64::engine::general_purpose::STANDARD;
use base64::Engine; use base64::Engine;
use pingora_proxy::Session; use pingora_proxy::Session;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, LazyLock};
use subtle::ConstantTimeEq;
use urlencoding::decode; use urlencoding::decode;
// use pingora::http::{RequestHeader, ResponseHeader, StatusCode};
use pingora::http::RequestHeader;
// --------------------------------- //
use pingora_core::connectors::http::Connector;
use pingora_core::upstreams::peer::HttpPeer;
use pingora_http::ResponseHeader;
// --------------------------------- //
#[async_trait::async_trait]
trait AuthValidator { trait AuthValidator {
fn validate(&self, session: &Session) -> bool; async fn validate(&self, session: &mut Session) -> bool;
} }
struct BasicAuth<'a>(&'a str); struct BasicAuth<'a>(&'a str);
struct ApiKeyAuth<'a>(&'a str); struct ApiKeyAuth<'a>(&'a str);
struct JwtAuth<'a>(&'a str); struct JwtAuth<'a>(&'a str);
struct ForwardAuth<'a>(&'a str);
pub static AUTH_CONNECTOR: LazyLock<Connector> = LazyLock::new(|| Connector::new(None));
#[async_trait::async_trait]
impl AuthValidator for ForwardAuth<'_> {
async fn validate(&self, session: &mut Session) -> bool {
let method = match session.req_header().method.as_str() {
"HEAD" => "HEAD",
_ => "GET",
};
let auth_url = self.0;
let (plain, tls) = if let Some(p) = auth_url.strip_prefix("http://") {
(p, false)
} else if let Some(p) = auth_url.strip_prefix("https://") {
(p, true)
} else {
return false;
};
let (addr, uri) = if let Some(pos) = plain.find('/') {
(&plain[..pos], &plain[pos..])
} else {
(plain, "/")
};
let hp = match split_host_port(addr, tls) {
Some(hp) => hp,
None => return false,
};
let peer = HttpPeer::new((hp.0, hp.1), tls, hp.0.to_string());
let (mut http_session, _) = match AUTH_CONNECTOR.get_http_session(&peer).await {
Ok(s) => s,
Err(e) => {
log::warn!("ForwardAuth: connect failed: {}", e);
return false;
}
};
let mut auth_req = match RequestHeader::build(method, uri.as_bytes(), None) {
Ok(r) => r,
Err(e) => {
log::warn!("ForwardAuth: failed to build request: {}", e);
return false;
}
};
// auth_req.headers = session.req_header().headers.clone();
auth_req.insert_header("Host", addr).ok();
auth_req.insert_header("X-Forwarded-Uri", uri).ok();
auth_req.insert_header("X-Forwarded-Method", session.req_header().method.as_str()).ok();
if let Some(auth) = session.req_header().headers.get("authorization") {
auth_req.insert_header("Authorization", auth.clone()).ok();
}
if let Some(cookie) = session.req_header().headers.get("cookie") {
auth_req.insert_header("Cookie", cookie.clone()).ok();
}
if tls {
auth_req.insert_header("X-Forwarded-Proto", "https").ok();
} else {
auth_req.insert_header("X-Forwarded-Proto", "http").ok();
}
if let Err(e) = http_session.write_request_header(Box::new(auth_req)).await {
log::warn!("ForwardAuth: write failed: {}", e);
return false;
}
let status = match http_session.read_response_header().await {
Ok(_) => http_session.response_header().map(|r| r.status.as_u16()).unwrap_or(500),
Err(e) => {
log::warn!("ForwardAuth: read failed: {}", e);
return false;
}
};
let auth_headers_to_forward: Vec<(String, String)> = if let Some(resp_header) = http_session.response_header() {
resp_header
.headers
.iter()
.filter_map(|(name, value)| {
let name_str = name.as_str();
if name_str.starts_with("x-") || name_str.starts_with("remote-") || name_str.starts_with("locat") {
value.to_str().ok().map(|v| (name_str.to_string(), v.to_string()))
} else {
None
}
})
.collect()
} else {
Vec::new()
};
AUTH_CONNECTOR.release_http_session(http_session, &peer, None).await;
if (200..300).contains(&status) {
for (name, value) in auth_headers_to_forward {
session.req_header_mut().insert_header(name, value).ok();
}
true
} else if status == 302 || status == 301 {
let resp = ResponseHeader::build(StatusCode::MOVED_PERMANENTLY, None);
match resp {
Ok(mut r) => {
for (name, value) in auth_headers_to_forward {
r.insert_header(name, value).ok();
}
let _ = r.insert_header("Content-Length", "0");
let _ = session.write_response_header(Box::new(r), true).await;
true
}
Err(_) => return false,
}
} else {
false
}
}
}
#[async_trait::async_trait]
impl AuthValidator for BasicAuth<'_> { impl AuthValidator for BasicAuth<'_> {
fn validate(&self, session: &Session) -> bool { async fn validate(&self, session: &mut Session) -> bool {
if let Some(header) = session.get_header("authorization") { if let Some(header) = session.get_header("authorization") {
if let Some((_, val)) = header.to_str().ok().unwrap().split_once(' ') { if let Ok(h) = header.to_str() {
let decoded = STANDARD.decode(val).ok().unwrap(); if let Some((_, val)) = h.split_once(' ') {
let decoded_str = String::from_utf8(decoded).ok().unwrap(); if let Ok(decoded) = STANDARD.decode(val) {
return decoded_str == self.0; if decoded.as_slice().ct_eq(self.0.as_bytes()).into() {
return true;
}
}
}
} }
} }
false false
} }
} }
#[async_trait::async_trait]
impl AuthValidator for ApiKeyAuth<'_> { impl AuthValidator for ApiKeyAuth<'_> {
fn validate(&self, session: &Session) -> bool { async fn validate(&self, session: &mut Session) -> bool {
if let Some(header) = session.get_header("x-api-key") { if let Some(header) = session.get_header("x-api-key") {
return header.to_str().ok().unwrap() == self.0; if let Ok(h) = header.to_str() {
return h.as_bytes().ct_eq(self.0.as_bytes()).into();
}
} }
false false
} }
} }
#[async_trait::async_trait]
impl AuthValidator for JwtAuth<'_> { impl AuthValidator for JwtAuth<'_> {
fn validate(&self, session: &Session) -> bool { async fn validate(&self, session: &mut Session) -> bool {
let jwtsecret = self.0; let jwtsecret = self.0;
if let Some(tok) = get_query_param(session, "araleztoken") { if let Some(tok) = get_query_param(session, "araleztoken") {
return check_jwt(tok.as_str(), jwtsecret); return check_jwt(tok.as_str(), jwtsecret);
@@ -52,32 +198,21 @@ impl AuthValidator for JwtAuth<'_> {
false false
} }
} }
fn validate(auth: &dyn AuthValidator, session: &Session) -> bool {
auth.validate(session)
}
pub fn authenticate(c: &[String], session: &Session) -> bool { pub async fn authenticate(auth_type: &Arc<str>, credentials: &Arc<str>, session: &mut Session) -> bool {
match c[0].as_str() { match &**auth_type {
"basic" => { "basic" => BasicAuth(credentials).validate(session).await,
let auth = BasicAuth(c[1].as_str().into()); "apikey" => ApiKeyAuth(credentials).validate(session).await,
validate(&auth, session) "jwt" => JwtAuth(credentials).validate(session).await,
} "forward" => ForwardAuth(credentials).validate(session).await,
"apikey" => {
let auth = ApiKeyAuth(c[1].as_str().into());
validate(&auth, session)
}
"jwt" => {
let auth = JwtAuth(c[1].as_str().into());
validate(&auth, session)
}
_ => { _ => {
println!("Unsupported authentication mechanism : {}", c[0]); log::warn!("Unsupported authentication mechanism : {}", auth_type);
false false
} }
} }
} }
pub fn get_query_param(session: &Session, key: &str) -> Option<String> { pub fn get_query_param(session: &mut Session, key: &str) -> Option<String> {
let query = session.req_header().uri.query()?; let query = session.req_header().uri.query()?;
let params: HashMap<_, _> = query let params: HashMap<_, _> = query
@@ -89,6 +224,25 @@ pub fn get_query_param(session: &Session, key: &str) -> Option<String> {
Some((k, v)) Some((k, v))
}) })
.collect(); .collect();
params.get(key).and_then(|v| decode(v).ok()).map(|s| s.to_string())
params.get(key).map(|v| decode(v).ok()).flatten().map(|s| s.to_string()) }
#[allow(clippy::needless_return)]
fn split_host_port(addr: &str, tls: bool) -> Option<(&str, u16, bool, &str)> {
match addr.split_once(':') {
Some((h, p)) => match p.parse::<u16>() {
Ok(port) => return Some((h, port, tls, h)),
Err(_) => {
log::warn!("ForwardAuth: invalid port in {}", addr);
return None;
}
},
None => {
if tls {
return Some((addr, 443u16, tls, addr));
} else {
return Some((addr, 80u16, tls, addr));
}
}
};
} }

View File

@@ -1,6 +1,6 @@
use crate::utils::filewatch; use crate::utils::filewatch;
use crate::utils::kuberconsul::{ConsulDiscovery, KubernetesDiscovery, ServiceDiscovery}; use crate::utils::kuberconsul::{ConsulDiscovery, KubernetesDiscovery, ServiceDiscovery};
use crate::utils::structs::Configuration; use crate::utils::structs::{Configuration, UpstreamsDashMap};
use crate::web::webserver; use crate::web::webserver;
use async_trait::async_trait; use async_trait::async_trait;
use futures::channel::mpsc::Sender; use futures::channel::mpsc::Sender;
@@ -10,18 +10,16 @@ pub struct APIUpstreamProvider {
pub config_api_enabled: bool, pub config_api_enabled: bool,
pub address: String, pub address: String,
pub masterkey: String, pub masterkey: String,
pub tls_address: Option<String>, pub certs_dir: String,
pub tls_certificate: Option<String>, pub config_dir: String,
pub tls_key_file: Option<String>, pub upstreams_file: String,
// pub tls_address: Option<String>,
// pub tls_certificate: Option<String>,
// pub tls_key_file: Option<String>,
pub file_server_address: Option<String>, pub file_server_address: Option<String>,
pub file_server_folder: Option<String>, pub file_server_folder: Option<String>,
} pub current_upstreams: Arc<UpstreamsDashMap>,
pub full_upstreams: Arc<UpstreamsDashMap>,
#[async_trait]
impl Discovery for APIUpstreamProvider {
async fn start(&self, toreturn: Sender<Configuration>) {
webserver::run_server(self, toreturn).await;
}
} }
pub struct FromFileProvider { pub struct FromFileProvider {
@@ -41,6 +39,13 @@ pub trait Discovery {
async fn start(&self, tx: Sender<Configuration>); async fn start(&self, tx: Sender<Configuration>);
} }
#[async_trait]
impl Discovery for APIUpstreamProvider {
async fn start(&self, toreturn: Sender<Configuration>) {
webserver::run_server(self, toreturn, self.current_upstreams.clone(), self.full_upstreams.clone()).await;
}
}
#[async_trait] #[async_trait]
impl Discovery for FromFileProvider { impl Discovery for FromFileProvider {
async fn start(&self, tx: Sender<Configuration>) { async fn start(&self, tx: Sender<Configuration>) {

View File

@@ -1,158 +0,0 @@
/*
use crate::utils::structs::InnerMap;
use dashmap::DashMap;
use hickory_client::client::{Client, ClientHandle};
use hickory_client::proto::rr::{DNSClass, Name, RecordType};
use hickory_client::proto::runtime::TokioRuntimeProvider;
use hickory_client::proto::udp::UdpClientStream;
use std::net::SocketAddr;
use std::str::FromStr;
use std::sync::atomic::AtomicUsize;
use std::time::Duration;
use tokio::sync::Mutex;
type DnsError = Box<dyn std::error::Error + Send + Sync + 'static>;
pub struct DnsClientPool {
clients: Vec<Mutex<DnsClient>>,
}
struct DnsClient {
client: Client,
}
pub async fn start2(mut toreturn: Sender<Configuration>, config: Arc<Configuration>) {
let k8s = config.kubernetes.clone();
match k8s {
Some(k8s) => {
let dnserver = k8s.servers.unwrap_or(vec!["127.0.0.1:53".to_string()]);
let headers = DashMap::new();
let end = dnserver.len() - 1;
let mut num = 0;
if end > 0 {
num = rand::rng().random_range(0..end);
}
let srv = dnserver.get(num).unwrap().to_string();
let pool = DnsClientPool::new(5, srv.clone()).await;
let u = UpstreamsDashMap::new();
if let Some(whitelist) = k8s.services {
loop {
let upstreams = UpstreamsDashMap::new();
for service in whitelist.iter() {
let ret = pool.query_srv(service.real.as_str(), srv.clone()).await;
match ret {
Ok(r) => {
upstreams.insert(service.proxy.clone(), r);
}
Err(e) => eprintln!("DNS query failed for {:?}: {:?}", service, e),
}
}
if !compare_dashmaps(&u, &upstreams) {
headers.clear();
for (k, v) in config.headers.clone() {
headers.insert(k.to_string(), v);
}
let mut tosend: Configuration = Configuration {
upstreams: Default::default(),
headers: Default::default(),
consul: None,
kubernetes: None,
typecfg: "".to_string(),
extraparams: config.extraparams.clone(),
};
clone_dashmap_into(&upstreams, &u);
clone_dashmap_into(&upstreams, &tosend.upstreams);
tosend.headers = headers.clone();
tosend.extraparams.authentication = config.extraparams.authentication.clone();
tosend.typecfg = config.typecfg.clone();
tosend.consul = config.consul.clone();
print_upstreams(&tosend.upstreams);
toreturn.send(tosend).await.unwrap();
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
}
None => {}
}
}
impl DnsClient {
pub async fn new(server: String) -> Result<Self, DnsError> {
let server_details = server;
let server: SocketAddr = server_details.parse().expect("Unable to parse socket address");
let conn = UdpClientStream::builder(server, TokioRuntimeProvider::default()).build();
let (client, bg) = Client::connect(conn).await.unwrap();
tokio::spawn(bg);
Ok(Self { client })
}
pub async fn query_srv(&mut self, name: &str) -> Result<DashMap<String, (Vec<InnerMap>, AtomicUsize)>, DnsError> {
let upstreams: DashMap<String, (Vec<InnerMap>, AtomicUsize)> = DashMap::new();
let mut values = Vec::new();
match tokio::time::timeout(Duration::from_secs(5), self.client.query(Name::from_str(name)?, DNSClass::IN, RecordType::SRV)).await {
Ok(Ok(response)) => {
for answer in response.answers() {
if let hickory_client::proto::rr::RData::SRV(srv) = answer.data() {
let to_add = InnerMap {
address: srv.target().to_string(),
port: srv.port(),
is_ssl: false,
is_http2: false,
to_https: false,
rate_limit: None,
};
values.push(to_add);
}
}
upstreams.insert("/".to_string(), (values, AtomicUsize::new(0)));
Ok(upstreams)
}
Ok(Err(e)) => Err(Box::new(e)),
Err(_) => Err("DNS query timed out".into()),
}
}
}
impl DnsClientPool {
pub async fn new(pool_size: usize, server: String) -> Self {
let mut clients = Vec::with_capacity(pool_size);
for _ in 0..pool_size {
if let Ok(client) = DnsClient::new(server.clone()).await {
clients.push(Mutex::new(client));
}
}
Self { clients }
}
pub async fn query_srv(&self, name: &str, server: String) -> Result<DashMap<String, (Vec<InnerMap>, AtomicUsize)>, DnsError> {
// Try to get an available client
for client_mutex in &self.clients {
if let Ok(mut client) = client_mutex.try_lock() {
let vay = client.query_srv(name).await;
match vay {
Ok(_) => return vay,
Err(_) => {
// If query fails, drop this client and create a new one
*client = match DnsClient::new(server).await {
Ok(c) => c,
Err(e) => return Err(e),
};
// Retry with the new client
return client.query_srv(name).await;
}
}
}
}
// If all clients are busy, wait for the first one with a timeout
match tokio::time::timeout(Duration::from_secs(2), self.clients[0].lock()).await {
Ok(mut client) => client.query_srv(name).await,
Err(_) => Err("All DNS clients are busy and timeout reached".into()),
}
}
}
*/

View File

@@ -37,17 +37,12 @@ pub async fn start(fp: String, mut toreturn: Sender<Configuration>) {
match event { match event {
Ok(e) => match e.kind { Ok(e) => match e.kind {
EventKind::Modify(ModifyKind::Data(_)) | EventKind::Create(..) | EventKind::Remove(..) => { EventKind::Modify(ModifyKind::Data(_)) | EventKind::Create(..) | EventKind::Remove(..) => {
if e.paths[0].to_str().unwrap().ends_with("yaml") { if e.paths[0].to_str().unwrap().ends_with("yaml") && start.elapsed() > Duration::from_secs(2) {
if start.elapsed() > Duration::from_secs(2) { start = Instant::now();
start = Instant::now(); // info!("Config File changed :=> {:?}", e);
// info!("Config File changed :=> {:?}", e); let snd = load_configuration(file_path, "filepath").await.0;
let snd = load_configuration(file_path, "filepath").await; if let Some(snd) = snd {
match snd { toreturn.send(snd).await.unwrap();
Some(snd) => {
toreturn.send(snd).await.unwrap();
}
None => {}
}
} }
} }
} }

31
src/utils/fordebug.rs Normal file
View File

@@ -0,0 +1,31 @@
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
pub struct CountingAllocator;
pub static ALLOC_COUNT: AtomicUsize = AtomicUsize::new(0);
pub static DEALLOC_COUNT: AtomicUsize = AtomicUsize::new(0);
pub static ALLOC_BYTES: AtomicUsize = AtomicUsize::new(0);
#[allow(dead_code)]
unsafe impl GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
ALLOC_BYTES.fetch_add(layout.size(), Ordering::Relaxed);
System.alloc(layout)
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
DEALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
System.dealloc(ptr, layout)
}
}
// Uncomment following lines and comment allocator in main.rs
// #[global_allocator]
// pub static A: CountingAllocator = CountingAllocator;
#[allow(dead_code)]
fn for_example() {
let before = crate::utils::fordebug::ALLOC_COUNT.load(Ordering::Relaxed);
let after = crate::utils::fordebug::ALLOC_COUNT.load(Ordering::Relaxed);
println!("Allocations : {}", after - before);
}

View File

@@ -15,12 +15,18 @@ pub async fn hc2(upslist: Arc<UpstreamsDashMap>, fullist: Arc<UpstreamsDashMap>,
loop { loop {
tokio::select! { tokio::select! {
_ = period.tick() => { _ = period.tick() => {
populate_upstreams(&upslist, &fullist, &idlist, params, &client).await; // populate_upstreams(&upslist, &fullist, &idlist, params, &client).await;
let totest = build_upstreams(&fullist, params.0, &client).await;
if !compare_dashmaps(&totest, &upslist) {
clone_dashmap_into(&totest, &upslist);
clone_idmap_into(&totest, &idlist);
}
} }
} }
} }
} }
/*
pub async fn populate_upstreams(upslist: &Arc<UpstreamsDashMap>, fullist: &Arc<UpstreamsDashMap>, idlist: &Arc<UpstreamsIdMap>, params: (&str, u64), client: &Client) { pub async fn populate_upstreams(upslist: &Arc<UpstreamsDashMap>, fullist: &Arc<UpstreamsDashMap>, idlist: &Arc<UpstreamsIdMap>, params: (&str, u64), client: &Client) {
let totest = build_upstreams(fullist, params.0, client).await; let totest = build_upstreams(fullist, params.0, client).await;
if !compare_dashmaps(&totest, upslist) { if !compare_dashmaps(&totest, upslist) {
@@ -28,6 +34,7 @@ pub async fn populate_upstreams(upslist: &Arc<UpstreamsDashMap>, fullist: &Arc<U
clone_idmap_into(&totest, idlist); clone_idmap_into(&totest, idlist);
} }
} }
*/
pub async fn initiate_upstreams(fullist: UpstreamsDashMap) -> UpstreamsDashMap { pub async fn initiate_upstreams(fullist: UpstreamsDashMap) -> UpstreamsDashMap {
let client = Client::builder().timeout(Duration::from_secs(2)).danger_accept_invalid_certs(true).build().unwrap(); let client = Client::builder().timeout(Duration::from_secs(2)).danger_accept_invalid_certs(true).build().unwrap();
@@ -45,8 +52,8 @@ async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str, client: &Clie
let path = path_entry.key(); let path = path_entry.key();
let mut innervec = Vec::new(); let mut innervec = Vec::new();
for (_, upstream) in path_entry.value().0.iter().enumerate() { for upstream in path_entry.value().0.iter() {
let tls = detect_tls(&upstream.address.to_string(), &upstream.port, &client).await; let tls = detect_tls(upstream.address.as_ref(), &upstream.port, client).await;
let is_h2 = matches!(tls.1, Some(Version::HTTP_2)); let is_h2 = matches!(tls.1, Some(Version::HTTP_2));
let link = if tls.0 { let link = if tls.0 {
@@ -63,31 +70,23 @@ async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str, client: &Clie
to_https: upstream.to_https, to_https: upstream.to_https,
rate_limit: upstream.rate_limit, rate_limit: upstream.rate_limit,
healthcheck: upstream.healthcheck, healthcheck: upstream.healthcheck,
redirect_to: upstream.redirect_to.clone(),
authorization: upstream.authorization.clone(),
}; };
if scheme.healthcheck.unwrap_or(true) { if scheme.healthcheck.unwrap_or(true) {
let resp = http_request(&link, method, "", &client).await; let resp = http_request(&link, method, "", client).await;
if resp.0 { if resp.0 {
if resp.1 { if resp.1 {
scheme.is_http2 = is_h2; // could be adjusted further scheme.is_http2 = is_h2; // could be adjusted further
} }
innervec.push(scheme); innervec.push(Arc::from(scheme));
} else { } else {
warn!("Dead Upstream : {}", link); warn!("Dead Upstream : {}", link);
} }
} else { } else {
innervec.push(scheme); innervec.push(Arc::from(scheme));
} }
// let resp = http_request(&link, method, "", &client).await;
// if resp.0 {
// if resp.1 {
// scheme.is_http2 = is_h2; // could be adjusted further
// }
// innervec.push(scheme);
// } else {
// warn!("Dead Upstream : {}", link);
// }
} }
inner.insert(path.clone(), (innervec, AtomicUsize::new(0))); inner.insert(path.clone(), (innervec, AtomicUsize::new(0)));
} }
@@ -110,38 +109,27 @@ async fn http_request(url: &str, method: &str, payload: &str, client: &Client) -
} }
} }
match send_request(&client, method, url, payload).await { match send_request(client, method, url, payload).await {
Some(response) => { Some(response) => {
let status = response.status().as_u16(); let status = response.status().as_u16();
((99..499).contains(&status), false) ((99..499).contains(&status), false)
} }
None => (ping_grpc(&url).await, true), None => (ping_grpc(url).await, true),
} }
} }
pub async fn ping_grpc(addr: &str) -> bool { pub async fn ping_grpc(addr: &str) -> bool {
let endpoint_result = Endpoint::from_shared(addr.to_owned()); let endpoint = match Endpoint::from_shared(addr.to_owned()) {
Ok(e) => e.timeout(Duration::from_secs(2)),
if let Ok(endpoint) = endpoint_result { Err(_) => return false,
let endpoint = endpoint.timeout(Duration::from_secs(2)); };
tokio::time::timeout(Duration::from_secs(3), endpoint.connect()).await.ok().and_then(Result::ok).is_some()
match tokio::time::timeout(Duration::from_secs(3), endpoint.connect()).await {
Ok(Ok(_channel)) => true,
_ => false,
}
} else {
false
}
} }
async fn detect_tls(ip: &str, port: &u16, client: &Client) -> (bool, Option<Version>) { async fn detect_tls(ip: &str, port: &u16, client: &Client) -> (bool, Option<Version>) {
let https_url = format!("https://{}:{}", ip, port); let https_url = format!("https://{}:{}", ip, port);
match client.get(&https_url).send().await { if let Ok(response) = client.get(&https_url).send().await {
Ok(response) => { return (true, Some(response.version()));
// println!("{} => {:?} (HTTPS)", https_url, response.version());
return (true, Some(response.version()));
}
_ => {}
} }
let http_url = format!("http://{}:{}", ip, port); let http_url = format!("http://{}:{}", ip, port);
match client.get(&http_url).send().await { match client.get(&http_url).send().await {

View File

@@ -1,12 +1,13 @@
use crate::utils::kuberconsul::{match_path, ConsulService, KubeEndpoints}; use crate::utils::kuberconsul::{match_path, ConsulService, KubeEndpoints};
use crate::utils::structs::{InnerMap, ServiceMapping}; use crate::utils::structs::{GlobalServiceMapping, InnerMap};
use axum::http::{HeaderMap, HeaderValue}; use axum::http::{HeaderMap, HeaderValue};
use dashmap::DashMap; use dashmap::DashMap;
use reqwest::Client; use reqwest::Client;
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
pub async fn for_consul(url: String, token: Option<String>, conf: &ServiceMapping) -> Option<DashMap<String, (Vec<InnerMap>, AtomicUsize)>> { pub async fn for_consul(url: String, token: Option<String>, conf: &GlobalServiceMapping) -> Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>> {
let client = Client::builder().timeout(Duration::from_secs(2)).danger_accept_invalid_certs(true).build().ok()?; let client = Client::builder().timeout(Duration::from_secs(2)).danger_accept_invalid_certs(true).build().ok()?;
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
if let Some(token) = token { if let Some(token) = token {
@@ -19,29 +20,29 @@ pub async fn for_consul(url: String, token: Option<String>, conf: &ServiceMappin
return None; return None;
} }
let mut inner_vec = Vec::new(); let mut inner_vec = Vec::new();
let upstreams: DashMap<String, (Vec<InnerMap>, AtomicUsize)> = DashMap::new(); let upstreams: DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)> = DashMap::new();
let endpoints: Vec<ConsulService> = resp.json().await.ok()?; let endpoints: Vec<ConsulService> = resp.json().await.ok()?;
for subsets in endpoints { for subsets in endpoints {
// let addr = subsets.tagged_addresses.get("lan_ipv4").unwrap().address.clone(); let addr = subsets.tagged_addresses.get("lan_ipv4").unwrap().address.clone();
// let prt = subsets.tagged_addresses.get("lan_ipv4").unwrap().port.clone(); let prt = subsets.tagged_addresses.get("lan_ipv4").unwrap().port;
let addr = subsets.tagged_addresses.get("lan_ipv4").unwrap().address.clone().parse().unwrap(); let to_add = Arc::from(InnerMap {
let prt = subsets.tagged_addresses.get("lan_ipv4").unwrap().port.clone(); address: Arc::from(&*addr),
let to_add = InnerMap {
address: addr,
port: prt, port: prt,
is_ssl: false, is_ssl: false,
is_http2: false, is_http2: false,
to_https: conf.to_https.unwrap_or(false), to_https: conf.to_https.unwrap_or(false),
rate_limit: conf.rate_limit, rate_limit: conf.rate_limit,
redirect_to: None,
healthcheck: None, healthcheck: None,
}; authorization: None,
});
inner_vec.push(to_add); inner_vec.push(to_add);
} }
match_path(&conf, &upstreams, inner_vec.clone()); match_path(conf, &upstreams, inner_vec);
Some(upstreams) Some(upstreams)
} }
pub async fn for_kuber(url: &str, token: &str, conf: &ServiceMapping) -> Option<DashMap<String, (Vec<InnerMap>, AtomicUsize)>> { pub async fn for_kuber(url: &str, token: &str, conf: &GlobalServiceMapping) -> Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>> {
let to = Duration::from_secs(10); let to = Duration::from_secs(10);
let client = Client::builder().timeout(Duration::from_secs(10)).danger_accept_invalid_certs(true).build().ok()?; let client = Client::builder().timeout(Duration::from_secs(10)).danger_accept_invalid_certs(true).build().ok()?;
let resp = client.get(url).timeout(to).bearer_auth(token).send().await.ok()?; let resp = client.get(url).timeout(to).bearer_auth(token).send().await.ok()?;
@@ -50,26 +51,31 @@ pub async fn for_kuber(url: &str, token: &str, conf: &ServiceMapping) -> Option<
return None; return None;
} }
let endpoints: KubeEndpoints = resp.json().await.ok()?; let endpoints: KubeEndpoints = resp.json().await.ok()?;
let upstreams: DashMap<String, (Vec<InnerMap>, AtomicUsize)> = DashMap::new();
let upstreams: DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)> = DashMap::new();
if let Some(subsets) = endpoints.subsets { if let Some(subsets) = endpoints.subsets {
for subset in subsets { for subset in subsets {
if let (Some(addresses), Some(ports)) = (subset.addresses, subset.ports) { if let (Some(addresses), Some(ports)) = (subset.addresses, subset.ports) {
let mut inner_vec = Vec::new(); let mut inner_vec = Vec::new();
for addr in addresses { for addr in addresses {
for port in &ports { for port in &ports {
let to_add = InnerMap { // let redirect_link = conf.redirect_to.as_ref().map(|www| Arc::from(www.as_str()));
address: addr.ip.parse().unwrap(), let to_add = Arc::from(InnerMap {
port: port.port.clone(), address: Arc::from(addr.ip.clone()),
port: port.port,
is_ssl: false, is_ssl: false,
is_http2: false, is_http2: false,
to_https: conf.to_https.unwrap_or(false), to_https: conf.to_https.unwrap_or(false),
rate_limit: conf.rate_limit, rate_limit: conf.rate_limit,
healthcheck: None, healthcheck: None,
}; redirect_to: None,
authorization: None,
});
inner_vec.push(to_add); inner_vec.push(to_add);
} }
} }
match_path(&conf, &upstreams, inner_vec.clone()); match_path(conf, &upstreams, inner_vec.clone());
} }
} }
} }

View File

@@ -1,16 +1,87 @@
use ahash::AHasher;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use moka::sync::Cache;
use moka::Expiry;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::hash::{Hash, Hasher};
use std::sync::LazyLock;
use std::time::{Duration, Instant, SystemTime};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub(crate) struct Claims { pub struct Claims {
pub(crate) user: String, pub master_key: String,
pub(crate) exp: u64, pub owner: String,
pub exp: u64,
pub random: Option<String>,
} }
pub fn check_jwt(input: &str, secret: &str) -> bool {
let validation = Validation::new(Algorithm::HS256); #[derive(Debug, Deserialize)]
let token_data = decode::<Claims>(&input, &DecodingKey::from_secret(secret.as_ref()), &validation); struct Expired {
match token_data { exp: Option<u64>,
Ok(_) => true, }
static JWT_VALIDATION: LazyLock<Validation> = LazyLock::new(|| Validation::new(Algorithm::HS256));
static JWT_CACHE: LazyLock<Cache<u64, u64>> = LazyLock::new(|| Cache::builder().max_capacity(100_000).expire_after(JwtExpiry).build());
struct JwtExpiry;
impl Expiry<u64, u64> for JwtExpiry {
fn expire_after_create(&self, _key: &u64, value: &u64, _current_time: Instant) -> Option<Duration> {
let now = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
if *value > now {
Some(Duration::from_secs(value - now))
} else {
Some(Duration::ZERO)
}
}
}
pub fn check_jwt(token: &str, secret: &str) -> bool {
let key = hash_token(token, secret);
let now = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
if let Some(exp) = JWT_CACHE.get(&key) {
if exp < now {
return false;
}
return true;
}
match is_expired(token, now) {
Ok(true) => return false,
Ok(false) => {}
Err(_) => return false,
}
match decode::<Claims>(token, &DecodingKey::from_secret(secret.as_ref()), &JWT_VALIDATION) {
Ok(data) => {
let now = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
if data.claims.exp > now {
JWT_CACHE.insert(key, data.claims.exp);
true
} else {
false
}
}
Err(_) => false, Err(_) => false,
} }
} }
fn is_expired(token: &str, now: u64) -> Result<bool, Box<dyn std::error::Error>> {
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 3 {
return Err("Invalid JWT format".into());
}
let decoded = URL_SAFE_NO_PAD.decode(parts[1])?;
let claims: Expired = serde_json::from_slice(&decoded)?;
if let Some(exp) = claims.exp {
Ok(exp < now)
} else {
Ok(true)
}
}
fn hash_token(token: &str, secret: &str) -> u64 {
let mut hasher = AHasher::default();
token.hash(&mut hasher);
secret.hash(&mut hasher);
hasher.finish()
}

View File

@@ -1,13 +1,13 @@
use crate::utils::httpclient; use crate::utils::httpclient;
use crate::utils::parceyaml::build_headers; use crate::utils::parceyaml::build_headers;
use crate::utils::structs::{Configuration, InnerMap, ServiceMapping, UpstreamsDashMap}; use crate::utils::structs::{Configuration, GlobalServiceMapping, InnerMap, UpstreamsDashMap};
use crate::utils::tools::{clone_dashmap_into, compare_dashmaps, print_upstreams}; use crate::utils::tools::{clone_dashmap_into, compare_dashmaps, print_upstreams};
use async_trait::async_trait; use async_trait::async_trait;
use dashmap::DashMap; use dashmap::DashMap;
use futures::channel::mpsc::Sender; use futures::channel::mpsc::Sender;
use futures::SinkExt; use futures::SinkExt;
use pingora::prelude::sleep; use pingora::prelude::sleep;
use rand::Rng; use rand::RngExt;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::env; use std::env;
@@ -52,28 +52,29 @@ pub struct ConsulTaggedAddress {
#[serde(rename = "Port")] #[serde(rename = "Port")]
pub port: u16, pub port: u16,
} }
pub fn list_to_upstreams(lt: Option<DashMap<String, (Vec<InnerMap>, AtomicUsize)>>, upstreams: &UpstreamsDashMap, i: &ServiceMapping) { #[allow(clippy::type_complexity)]
pub fn list_to_upstreams(lt: Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>>, upstreams: &UpstreamsDashMap, i: &GlobalServiceMapping) {
if let Some(list) = lt { if let Some(list) = lt {
match upstreams.get(&i.hostname.clone()) { match upstreams.get(&*i.hostname.clone()) {
Some(upstr) => { Some(upstr) => {
for (k, v) in list { for (k, v) in list {
upstr.value().insert(k, v); upstr.value().insert(k.to_owned(), v);
} }
} }
None => { None => {
upstreams.insert(i.hostname.clone(), list); upstreams.insert(Arc::from(i.hostname.clone()), list);
} }
}; };
} }
} }
pub fn match_path(conf: &ServiceMapping, upstreams: &DashMap<String, (Vec<InnerMap>, AtomicUsize)>, values: Vec<InnerMap>) { pub fn match_path(conf: &GlobalServiceMapping, upstreams: &DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>, values: Vec<Arc<InnerMap>>) {
match conf.path { match conf.path {
Some(ref p) => { Some(ref p) => {
upstreams.insert(p.to_string(), (values, AtomicUsize::new(0))); upstreams.insert(Arc::from(p.clone()), (values, AtomicUsize::new(0)));
} }
None => { None => {
upstreams.insert("/".to_string(), (values, AtomicUsize::new(0))); upstreams.insert(Arc::from("/"), (values, AtomicUsize::new(0)));
} }
} }
} }
@@ -114,12 +115,12 @@ impl ServiceDiscovery for KubernetesDiscovery {
let upstreams = UpstreamsDashMap::new(); let upstreams = UpstreamsDashMap::new();
if let Some(kuber) = config.kubernetes.clone() { if let Some(kuber) = config.kubernetes.clone() {
if let Some(svc) = kuber.services { if let Some(svc) = kuber.services {
for i in svc { for service in svc {
let header_list: DashMap<Arc<str>, Vec<(Arc<str>, Arc<str>)>> = DashMap::new(); let header_list: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
let mut hl = Vec::new(); let mut hl = Vec::new();
build_headers(&i.client_headers, config.as_ref(), &mut hl); build_headers(&service.client_headers, config.as_ref(), &mut hl);
if !hl.is_empty() { if !hl.is_empty() {
match i.path.clone() { match service.path.clone() {
Some(path) => { Some(path) => {
header_list.insert(Arc::from(path.as_str()), hl); header_list.insert(Arc::from(path.as_str()), hl);
} }
@@ -130,11 +131,13 @@ impl ServiceDiscovery for KubernetesDiscovery {
// header_list.insert(Arc::from(path.as_str()), hl); // header_list.insert(Arc::from(path.as_str()), hl);
// header_list.insert(Arc::from(i.path).unwrap_or(Arc::from("/")).as_str(), hl); // header_list.insert(Arc::from(i.path).unwrap_or(Arc::from("/")).as_str(), hl);
config.client_headers.insert(i.hostname.clone(), header_list); config.client_headers.insert(Arc::from(service.hostname.clone()), header_list);
} }
let url = format!("https://{}/api/v1/namespaces/{}/endpoints/{}", server, namespace, i.hostname); let url = format!("https://{}/api/v1/namespaces/{}/endpoints/{}", server, namespace, service.hostname);
let list = httpclient::for_kuber(&*url, &*token, &i).await; // let url = format!("https://{}/api/v1/namespaces/{}/endpoints?labelSelector=app", server, namespace);
list_to_upstreams(list, &upstreams, &i); let list = httpclient::for_kuber(&url, &token, &service).await;
// println!("{:?}", list);
list_to_upstreams(list, &upstreams, &service);
} }
} }
if let Some(lt) = clone_compare(&upstreams, &prev_upstreams, &config).await { if let Some(lt) = clone_compare(&upstreams, &prev_upstreams, &config).await {
@@ -190,7 +193,7 @@ impl ServiceDiscovery for ConsulDiscovery {
} }
} }
// header_list.insert(i.path.clone().unwrap_or("/".to_string()), hl); // header_list.insert(i.path.clone().unwrap_or("/".to_string()), hl);
config.client_headers.insert(i.hostname.clone(), header_list); config.client_headers.insert(Arc::from(i.hostname.clone()), header_list);
} }
let pref = ss.clone() + &i.upstream; let pref = ss.clone() + &i.upstream;
@@ -207,7 +210,7 @@ impl ServiceDiscovery for ConsulDiscovery {
} }
} }
async fn clone_compare(upstreams: &UpstreamsDashMap, prev_upstreams: &UpstreamsDashMap, config: &Arc<Configuration>) -> Option<Configuration> { async fn clone_compare(upstreams: &UpstreamsDashMap, prev_upstreams: &UpstreamsDashMap, config: &Arc<Configuration>) -> Option<Configuration> {
if !compare_dashmaps(&upstreams, &prev_upstreams) { if !compare_dashmaps(upstreams, prev_upstreams) {
let tosend: Configuration = Configuration { let tosend: Configuration = Configuration {
upstreams: Default::default(), upstreams: Default::default(),
client_headers: config.client_headers.clone(), client_headers: config.client_headers.clone(),
@@ -217,8 +220,8 @@ async fn clone_compare(upstreams: &UpstreamsDashMap, prev_upstreams: &UpstreamsD
typecfg: config.typecfg.clone(), typecfg: config.typecfg.clone(),
extraparams: config.extraparams.clone(), extraparams: config.extraparams.clone(),
}; };
clone_dashmap_into(&upstreams, &prev_upstreams); clone_dashmap_into(upstreams, prev_upstreams);
clone_dashmap_into(&upstreams, &tosend.upstreams); clone_dashmap_into(upstreams, &tosend.upstreams);
print_upstreams(&tosend.upstreams); print_upstreams(&tosend.upstreams);
return Some(tosend); return Some(tosend);
}; };

View File

@@ -1,63 +1,56 @@
use pingora_http::Method;
use pingora_http::StatusCode;
use pingora_http::Version; use pingora_http::Version;
use prometheus::{register_histogram, register_int_counter, register_int_counter_vec, Histogram, IntCounter, IntCounterVec}; use prometheus::{register_histogram, register_int_counter, register_int_counter_vec, register_int_gauge, Histogram, IntCounter, IntCounterVec, IntGauge};
use std::sync::Arc;
use std::sync::LazyLock;
use std::time::Duration; use std::time::Duration;
pub struct MetricTypes { pub struct MetricTypes {
pub method: String, pub method: Method,
pub code: String, pub upstream: Arc<str>,
pub code: Option<StatusCode>,
pub latency: Duration, pub latency: Duration,
pub version: Version, pub version: Version,
} }
lazy_static::lazy_static! {
pub static ref REQUEST_COUNT: IntCounter = register_int_counter!( pub static ACTIVE_SESSIONS: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_active_sessions", "Current number of active sessions").unwrap());
"aralez_requests_total",
"Total number of requests handled by Aralez" pub static REQUEST_COUNT: LazyLock<IntCounter> = LazyLock::new(|| register_int_counter!("aralez_requests_total", "Total number of requests handled by Aralez").unwrap());
).unwrap();
pub static ref RESPONSE_CODES: IntCounterVec = register_int_counter_vec!( pub static RESPONSE_CODES: LazyLock<IntCounterVec> =
"aralez_responses_total", LazyLock::new(|| register_int_counter_vec!("aralez_responses_total", "Responses grouped by status code", &["status"]).unwrap());
"Responses grouped by status code",
&["status"] pub static RESPONSE_LATENCY: LazyLock<Histogram> = LazyLock::new(|| {
).unwrap(); register_histogram!(
pub static ref REQUEST_LATENCY: Histogram = register_histogram!(
"aralez_request_latency_seconds",
"Request latency in seconds",
vec![0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0]
).unwrap();
pub static ref RESPONSE_LATENCY: Histogram = register_histogram!(
"aralez_response_latency_seconds", "aralez_response_latency_seconds",
"Response latency in seconds", "Response latency in seconds",
vec![0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 5.0] vec![0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 5.0]
).unwrap(); )
pub static ref REQUESTS_BY_METHOD: IntCounterVec = register_int_counter_vec!( .unwrap()
"aralez_requests_by_method_total", });
"Number of requests by HTTP method",
&["method"] pub static REQUESTS_BY_METHOD: LazyLock<IntCounterVec> =
).unwrap(); LazyLock::new(|| register_int_counter_vec!("aralez_requests_by_method_total", "Number of requests by HTTP method", &["method"]).unwrap());
pub static ref REQUESTS_BY_VERSION: IntCounterVec = register_int_counter_vec!(
"aralez_requests_by_version_total", pub static REQUESTS_BY_UPSTREAM: LazyLock<IntCounterVec> =
"Number of requests by HTTP versions", LazyLock::new(|| register_int_counter_vec!("aralez_requests_by_upstream", "Number of requests by UPSTREAM server", &["upstream"]).unwrap());
&["version"]
).unwrap(); pub static REQUESTS_BY_VERSION: LazyLock<IntCounterVec> =
pub static ref ERROR_COUNT: IntCounter = register_int_counter!( LazyLock::new(|| register_int_counter_vec!("aralez_requests_by_version_total", "Number of requests by HTTP versions", &["version"]).unwrap());
"aralez_errors_total",
"Total number of errors"
).unwrap();
}
pub fn calc_metrics(metric_types: &MetricTypes) { pub fn calc_metrics(metric_types: &MetricTypes) {
REQUEST_COUNT.inc(); REQUEST_COUNT.inc();
let timer = REQUEST_LATENCY.start_timer(); let version_str = match metric_types.version {
timer.observe_duration(); Version::HTTP_11 => "HTTP/1.1",
Version::HTTP_2 => "HTTP/2.0",
let version_str = match &metric_types.version { Version::HTTP_3 => "HTTP/3.0",
&Version::HTTP_11 => "HTTP/1.1", Version::HTTP_10 => "HTTP/1.0",
&Version::HTTP_2 => "HTTP/2.0",
&Version::HTTP_3 => "HTTP/3.0",
&Version::HTTP_10 => "HTTP/1.0",
_ => "Unknown", _ => "Unknown",
}; };
REQUESTS_BY_VERSION.with_label_values(&[&version_str]).inc(); REQUESTS_BY_VERSION.with_label_values(&[version_str]).inc();
RESPONSE_CODES.with_label_values(&[&metric_types.code.to_string()]).inc(); RESPONSE_CODES.with_label_values(&[metric_types.code.unwrap_or(StatusCode::GONE).as_str()]).inc();
REQUESTS_BY_METHOD.with_label_values(&[&metric_types.method]).inc(); REQUESTS_BY_METHOD.with_label_values(&[metric_types.method.as_str()]).inc();
REQUESTS_BY_UPSTREAM.with_label_values(&[metric_types.upstream.as_ref()]).inc();
RESPONSE_LATENCY.observe(metric_types.latency.as_secs_f64()); RESPONSE_LATENCY.observe(metric_types.latency.as_secs_f64());
} }

View File

@@ -5,22 +5,65 @@ use crate::utils::tools::{clone_dashmap, clone_dashmap_into, print_upstreams};
use dashmap::DashMap; use dashmap::DashMap;
use log::{error, info, warn}; use log::{error, info, warn};
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path;
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::sync::Arc; use std::sync::{Arc, LazyLock};
use std::{env, fs}; use std::{env, fs};
// use tokio::sync::oneshot::{Receiver, Sender};
pub async fn load_configuration(d: &str, kind: &str) -> Option<Configuration> { pub static DOMAINS: LazyLock<DashMap<String, bool>> = LazyLock::new(DashMap::new);
pub async fn load_configuration(d: &str, kind: &str) -> (Option<Configuration>, String) {
let mut conf_files = Vec::new();
let yaml_data = match kind { let yaml_data = match kind {
"filepath" => match fs::read_to_string(d) { "filepath" => match fs::read_to_string(d) {
Ok(data) => { Ok(data) => {
let mut confdir = Path::new(d).parent().unwrap().to_path_buf();
let mut autocfg = Path::new(d).parent().unwrap().to_path_buf();
autocfg.push("autoconfigs");
if fs::metadata(autocfg.clone()).is_err() {
fs::create_dir_all(autocfg.clone()).ok();
}
autocfg.push("domains.json");
if autocfg.exists() {
let json: Option<Vec<String>> = fs::read_to_string(autocfg).ok().and_then(|s| serde_json::from_str(&s).ok());
if let Some(domains) = json {
for domain in domains {
DOMAINS.insert(domain, true);
}
}
}
confdir.push("conf.d");
if let Ok(entries) = fs::read_dir(&confdir) {
let mut paths: Vec<_> = entries
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("yaml"))
.collect();
paths.sort();
for path in paths {
let content = fs::read_to_string(&path);
match content {
Ok(content) => {
conf_files.push(content);
}
Err(e) => {
error!("Reading: {}: {:?}", path.display(), e)
}
};
}
}
info!("Reading upstreams from {}", d); info!("Reading upstreams from {}", d);
data data
} }
Err(e) => { Err(e) => {
error!("Reading: {}: {:?}", d, e); error!("Reading: {}: {:?}", d, e);
warn!("Running with empty upstreams list, update it via API"); warn!("Running with empty upstreams list, update it via API");
return None; return (None, e.to_string());
} }
}, },
"content" => { "content" => {
@@ -29,85 +72,93 @@ pub async fn load_configuration(d: &str, kind: &str) -> Option<Configuration> {
} }
_ => { _ => {
error!("Mismatched parameter, only filepath|content is allowed"); error!("Mismatched parameter, only filepath|content is allowed");
return None; return (None, "Mismatched parameter, only filepath|content is allowed".to_string());
} }
}; };
let parsed: Config = match serde_yaml::from_str(&yaml_data) { let mut parsed: Config = match serde_yml::from_str(&yaml_data) {
Ok(cfg) => cfg, Ok(cfg) => cfg,
Err(e) => { Err(e) => {
error!("Failed to parse upstreams file: {}", e); error!("Failed to parse upstreams file: {}", e);
return None; return (None, e.to_string());
} }
}; };
let mut toreturn = Configuration::default(); if let Some(ref mut upstreams) = parsed.upstreams {
for uconf in conf_files {
let p: HashMap<String, HostConfig> = match serde_yml::from_str(&uconf) {
Ok(ucfg) => ucfg,
Err(e) => {
error!("Failed to parse upstreams file: {}", e);
return (None, e.to_string());
}
};
upstreams.extend(p);
}
}
let mut toreturn = Configuration::default();
populate_headers_and_auth(&mut toreturn, &parsed).await; populate_headers_and_auth(&mut toreturn, &parsed).await;
toreturn.typecfg = parsed.provider.clone(); toreturn.typecfg = parsed.provider.clone();
match parsed.provider.as_str() { match parsed.provider.as_str() {
"file" => { "file" => {
populate_file_upstreams(&mut toreturn, &parsed).await; populate_file_upstreams(&mut toreturn, &parsed).await;
Some(toreturn) (Some(toreturn), "Ok".to_string())
} }
"consul" => { "consul" => {
toreturn.consul = parsed.consul; toreturn.consul = parsed.consul;
toreturn.consul.is_some().then_some(toreturn) (toreturn.consul.is_some().then_some(toreturn), "Ok".to_string())
} }
"kubernetes" => { "kubernetes" => {
toreturn.kubernetes = parsed.kubernetes; toreturn.kubernetes = parsed.kubernetes;
toreturn.kubernetes.is_some().then_some(toreturn) (toreturn.kubernetes.is_some().then_some(toreturn), "Ok".to_string())
} }
_ => { _ => {
warn!("Unknown provider {}", parsed.provider); warn!("Unknown provider {}", parsed.provider);
None (None, "Unknown provider".to_string())
} }
} }
} }
async fn populate_headers_and_auth(config: &mut Configuration, parsed: &Config) { async fn populate_headers_and_auth(config: &mut Configuration, parsed: &Config) {
let mut ch: Vec<(Arc<str>, Arc<str>)> = Vec::new(); let mut ch: Vec<(String, Arc<str>)> = Vec::new();
ch.push((Arc::from("Server"), Arc::from("Aralez")));
// println!("{:?}", &parsed.client_headers);
if let Some(headers) = &parsed.client_headers { if let Some(headers) = &parsed.client_headers {
for header in headers { for header in headers {
if let Some((key, val)) = header.split_once(':') { if let Some((key, val)) = header.split_once(':') {
ch.push((Arc::from(key), Arc::from(val))); ch.push((key.to_string(), Arc::from(val)));
} }
} }
} }
let global_headers: DashMap<Arc<str>, Vec<(Arc<str>, Arc<str>)>> = DashMap::new(); let global_headers: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
global_headers.insert(Arc::from("/"), ch); global_headers.insert(Arc::from("/"), ch);
config.client_headers.insert("GLOBAL_CLIENT_HEADERS".to_string(), global_headers); config.client_headers.insert(Arc::from("GLOBAL_CLIENT_HEADERS"), global_headers);
let mut sh: Vec<(Arc<str>, Arc<str>)> = Vec::new(); let mut sh: Vec<(String, Arc<str>)> = Vec::new();
sh.push((Arc::from("X-Proxy-Server"), Arc::from("Aralez")));
if let Some(headers) = &parsed.server_headers { if let Some(headers) = &parsed.server_headers {
for header in headers { for header in headers {
if let Some((key, val)) = header.split_once(':') { if let Some((key, val)) = header.split_once(':') {
sh.push((Arc::from(key.trim()), Arc::from(val.trim()))); sh.push((key.to_string(), Arc::from(val.trim())));
} }
} }
} }
let server_global_headers: DashMap<Arc<str>, Vec<(Arc<str>, Arc<str>)>> = DashMap::new(); let server_global_headers: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
server_global_headers.insert(Arc::from("/"), sh); server_global_headers.insert(Arc::from("/"), sh);
config.server_headers.insert("GLOBAL_SERVER_HEADERS".to_string(), server_global_headers); config.server_headers.insert(Arc::from("GLOBAL_SERVER_HEADERS"), server_global_headers);
config.extraparams.sticky_sessions = parsed.sticky_sessions;
config.extraparams.to_https = parsed.to_https; config.extraparams.to_https = parsed.to_https;
config.extraparams.sticky_sessions = parsed.sticky_sessions;
config.extraparams.rate_limit = parsed.rate_limit; config.extraparams.rate_limit = parsed.rate_limit;
if let Some(rate) = &parsed.rate_limit { if let Some(rate) = &parsed.rate_limit {
info!("Applied Global Rate Limit : {} request per second", rate); info!("Applied Global Rate Limit : {} request per second", rate);
} }
if let Some(auth) = &parsed.authorization { if let Some(pa) = &parsed.authorization {
let name = auth.get("type").unwrap_or(&"".to_string()).to_string(); let y: InnerAuth = InnerAuth {
let creds = auth.get("creds").unwrap_or(&"".to_string()).to_string(); auth_type: Arc::from(pa.auth_type.clone()),
config.extraparams.authentication.insert("authorization".to_string(), vec![name, creds]); auth_cred: Arc::from(pa.auth_cred.clone()),
} else { };
config.extraparams.authentication = DashMap::new(); config.extraparams.authentication = Some(Arc::from(y));
} }
} }
@@ -122,35 +173,44 @@ async fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) {
if let Some(rate) = &path_config.rate_limit { if let Some(rate) = &path_config.rate_limit {
info!("Applied Rate Limit for {} : {} request per second", hostname, rate); info!("Applied Rate Limit for {} : {} request per second", hostname, rate);
} }
let mut hl: Vec<(String, Arc<str>)> = Vec::new();
let mut hl: Vec<(Arc<str>, Arc<str>)> = Vec::new(); let mut sl: Vec<(String, Arc<str>)> = Vec::new();
let mut sl: Vec<(Arc<str>, Arc<str>)> = Vec::new();
build_headers(&path_config.client_headers, config, &mut hl); build_headers(&path_config.client_headers, config, &mut hl);
build_headers(&path_config.server_headers, config, &mut sl); build_headers(&path_config.server_headers, config, &mut sl);
client_header_list.insert(Arc::from(path.as_str()), hl); client_header_list.insert(Arc::from(path.as_str()), hl);
server_header_list.insert(Arc::from(path.as_str()), sl); server_header_list.insert(Arc::from(path.as_str()), sl);
let mut server_list = Vec::new(); let mut server_list = Vec::new();
for server in &path_config.servers { for server in &path_config.servers {
let mut path_auth: Option<Arc<InnerAuth>> = None;
if let Some(pa) = &path_config.authorization {
let y: InnerAuth = InnerAuth {
auth_type: Arc::from(pa.auth_type.clone()),
auth_cred: Arc::from(pa.auth_cred.clone()),
};
path_auth = Some(Arc::from(y));
}
let redirect_link = path_config.redirect_to.as_ref().map(|www| Arc::from(www.as_str()));
if let Some((ip, port_str)) = server.split_once(':') { if let Some((ip, port_str)) = server.split_once(':') {
if let Ok(port) = port_str.parse::<u16>() { if let Ok(port) = port_str.parse::<u16>() {
server_list.push(InnerMap { server_list.push(Arc::from(InnerMap {
address: ip.trim().parse().unwrap(), address: Arc::from(ip),
port, port,
is_ssl: true, is_ssl: false,
is_http2: false, is_http2: false,
to_https: path_config.to_https.unwrap_or(false), to_https: path_config.to_https.unwrap_or(false),
rate_limit: path_config.rate_limit, rate_limit: path_config.rate_limit,
healthcheck: path_config.healthcheck, healthcheck: path_config.healthcheck,
}); redirect_to: redirect_link,
authorization: path_auth,
}));
} }
} }
} }
path_map.insert(path.clone(), (server_list, AtomicUsize::new(0))); path_map.insert(Arc::from(path.clone()), (server_list, AtomicUsize::new(0)));
} }
config.client_headers.insert(hostname.clone(), client_header_list); config.client_headers.insert(Arc::from(hostname.clone()), client_header_list);
config.server_headers.insert(hostname.clone(), server_header_list); config.server_headers.insert(Arc::from(hostname.clone()), server_header_list);
imtdashmap.insert(hostname.clone(), path_map); imtdashmap.insert(Arc::from(hostname.clone()), path_map);
} }
if is_first_run() { if is_first_run() {
@@ -168,8 +228,8 @@ async fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) {
pub fn parce_main_config(path: &str) -> AppConfig { pub fn parce_main_config(path: &str) -> AppConfig {
let data = fs::read_to_string(path).unwrap(); let data = fs::read_to_string(path).unwrap();
let reply = DashMap::new(); let reply = DashMap::new();
let cfg: HashMap<String, String> = serde_yaml::from_str(&*data).expect("Failed to parse main config file"); let cfg: HashMap<String, String> = serde_yml::from_str(&data).expect("Failed to parse main config file");
let mut cfo: AppConfig = serde_yaml::from_str(&*data).expect("Failed to parse main config file"); let mut cfo: AppConfig = serde_yml::from_str(&data).expect("Failed to parse main config file");
log_builder(&cfo); log_builder(&cfo);
cfo.hc_method = cfo.hc_method.to_uppercase(); cfo.hc_method = cfo.hc_method.to_uppercase();
for (k, v) in cfg { for (k, v) in cfg {
@@ -180,13 +240,24 @@ pub fn parce_main_config(path: &str) -> AppConfig {
cfo.local_server = Option::from((ip.to_string(), port)); cfo.local_server = Option::from((ip.to_string(), port));
} }
} }
// if let Some(tlsport_cfg) = cfo.proxy_address_tls.clone() {
// if let Some((_, port_str)) = tlsport_cfg.split_once(':') {
// if let Ok(port) = port_str.parse::<u16>() {
// cfo.proxy_port_tls = Some(port);
// }
// }
// };
if let Some(tlsport_cfg) = cfo.proxy_address_tls.clone() { if let Some(tlsport_cfg) = cfo.proxy_address_tls.clone() {
if let Some((_, port_str)) = tlsport_cfg.split_once(':') { if let Some((_, port_str)) = tlsport_cfg.split_once(':') {
if let Ok(port) = port_str.parse::<u16>() { cfo.proxy_port_tls = Some(port_str.to_string());
cfo.proxy_port_tls = Some(port);
}
} }
}; };
if let Some((_, port_str)) = cfo.proxy_address_http.split_once(':') {
cfo.proxy_port = Some(port_str.to_string());
}
cfo.proxy_tls_grade = parce_tls_grades(cfo.proxy_tls_grade.clone()); cfo.proxy_tls_grade = parce_tls_grades(cfo.proxy_tls_grade.clone());
cfo cfo
} }
@@ -213,7 +284,7 @@ fn parce_tls_grades(what: Option<String>) -> Option<String> {
}, },
None => { None => {
warn!("TLS grade not set, defaulting to: medium"); warn!("TLS grade not set, defaulting to: medium");
Some("b".to_string()) Some("medium".to_string())
} }
} }
} }
@@ -237,19 +308,12 @@ fn log_builder(conf: &AppConfig) {
env_logger::builder().init(); env_logger::builder().init();
} }
pub fn build_headers(path_config: &Option<Vec<String>>, _config: &Configuration, hl: &mut Vec<(Arc<str>, Arc<str>)>) { pub fn build_headers(path_config: &Option<Vec<String>>, _config: &Configuration, hl: &mut Vec<(String, Arc<str>)>) {
if let Some(headers) = &path_config { if let Some(headers) = &path_config {
for header in headers { for header in headers {
if let Some((key, val)) = header.split_once(':') { if let Some((key, val)) = header.split_once(':') {
hl.push((Arc::from(key.trim()), Arc::from(val.trim()))); hl.push((key.trim().to_string(), Arc::from(val.trim())));
} }
} }
// if let Some(push) = config.client_headers.get("GLOBAL_HEADERS") {
// for k in push.iter() {
// for x in k.value() {
// hl.push(x.to_owned());
// }
// }
// }
} }
} }

View File

@@ -1,12 +1,11 @@
use once_cell::sync::Lazy; use std::sync::{LazyLock, RwLock};
use std::sync::RwLock;
#[derive(Debug)] #[derive(Debug)]
pub struct SharedState { pub struct SharedState {
pub first_run: bool, pub first_run: bool,
} }
pub static GLOBAL_STATE: Lazy<RwLock<SharedState>> = Lazy::new(|| RwLock::new(SharedState { first_run: true })); pub static GLOBAL_STATE: LazyLock<RwLock<SharedState>> = LazyLock::new(|| RwLock::new(SharedState { first_run: true }));
pub fn mark_not_first_run() { pub fn mark_not_first_run() {
let mut state = GLOBAL_STATE.write().unwrap(); let mut state = GLOBAL_STATE.write().unwrap();

View File

@@ -2,51 +2,53 @@ use dashmap::DashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
pub type UpstreamsDashMap = DashMap<String, DashMap<String, (Vec<InnerMap>, AtomicUsize)>>;
use std::net::IpAddr;
use std::sync::Arc; use std::sync::Arc;
pub type UpstreamsIdMap = DashMap<String, InnerMap>; pub type UpstreamsDashMap = DashMap<Arc<str>, DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>>;
pub type Headers = DashMap<String, DashMap<Arc<str>, Vec<(Arc<str>, Arc<str>)>>>;
pub type UpstreamsIdMap = DashMap<String, Arc<InnerMap>>;
pub type Headers = DashMap<Arc<str>, DashMap<Arc<str>, Vec<(String, Arc<str>)>>>;
// pub type UpstreamsSerDde = Option<HashMap<String, HostConfig>>;
// pub type UpstreamsSerDe = HashMap<String, HostConfig>;
#[derive(Clone, Debug, Default)]
pub struct Extraparams {
pub to_https: Option<bool>,
pub sticky_sessions: bool,
pub authentication: Option<Arc<InnerAuth>>,
pub rate_limit: Option<isize>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct ServiceMapping { pub struct GlobalServiceMapping {
pub upstream: String, pub upstream: String,
pub hostname: String, pub hostname: String,
pub path: Option<String>, pub path: Option<String>,
pub to_https: Option<bool>, pub to_https: Option<bool>,
pub sticky_sessions: Option<bool>,
pub rate_limit: Option<isize>, pub rate_limit: Option<isize>,
pub client_headers: Option<Vec<String>>, pub client_headers: Option<Vec<String>>,
pub server_headers: Option<Vec<String>>, pub server_headers: Option<Vec<String>>,
} }
// pub type Services = DashMap<String, Vec<(String, Option<String>)>>;
#[derive(Clone, Debug, Default)]
pub struct Extraparams {
pub sticky_sessions: bool,
pub to_https: Option<bool>,
pub authentication: DashMap<String, Vec<String>>,
pub rate_limit: Option<isize>,
}
#[derive(Clone, Default, Debug, Serialize, Deserialize)] #[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct Kubernetes { pub struct Kubernetes {
pub servers: Option<Vec<String>>, pub servers: Option<Vec<String>>,
pub services: Option<Vec<ServiceMapping>>, pub services: Option<Vec<GlobalServiceMapping>>,
pub tokenpath: Option<String>, pub tokenpath: Option<String>,
} }
#[derive(Clone, Default, Debug, Serialize, Deserialize)] #[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct Consul { pub struct Consul {
pub servers: Option<Vec<String>>, pub servers: Option<Vec<String>>,
pub services: Option<Vec<ServiceMapping>>, pub services: Option<Vec<GlobalServiceMapping>>,
pub token: Option<String>, pub token: Option<String>,
} }
#[derive(Debug, Default, Serialize, Deserialize)] #[derive(Debug, Default, Serialize, Deserialize)]
pub struct Config { pub struct Config {
pub provider: String, pub provider: String,
pub sticky_sessions: bool,
pub to_https: Option<bool>, pub to_https: Option<bool>,
pub sticky_sessions: bool,
#[serde(default)] #[serde(default)]
pub upstreams: Option<HashMap<String, HostConfig>>, pub upstreams: Option<HashMap<String, HostConfig>>,
#[serde(default)] #[serde(default)]
@@ -56,7 +58,7 @@ pub struct Config {
#[serde(default)] #[serde(default)]
pub server_headers: Option<Vec<String>>, pub server_headers: Option<Vec<String>>,
#[serde(default)] #[serde(default)]
pub authorization: Option<HashMap<String, String>>, pub authorization: Option<Auth>,
#[serde(default)] #[serde(default)]
pub consul: Option<Consul>, pub consul: Option<Consul>,
#[serde(default)] #[serde(default)]
@@ -70,15 +72,24 @@ pub struct HostConfig {
pub paths: HashMap<String, PathConfig>, pub paths: HashMap<String, PathConfig>,
pub rate_limit: Option<isize>, pub rate_limit: Option<isize>,
} }
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct Auth {
#[serde(rename = "type")]
pub auth_type: String,
#[serde(rename = "data")]
pub auth_cred: String,
}
#[derive(Debug, Default, Serialize, Deserialize)] #[derive(Debug, Default, Serialize, Deserialize)]
pub struct PathConfig { pub struct PathConfig {
pub servers: Vec<String>, pub servers: Vec<String>,
pub to_https: Option<bool>, pub to_https: Option<bool>,
pub sticky_sessions: Option<bool>,
pub client_headers: Option<Vec<String>>, pub client_headers: Option<Vec<String>>,
pub server_headers: Option<Vec<String>>, pub server_headers: Option<Vec<String>>,
pub rate_limit: Option<isize>, pub rate_limit: Option<isize>,
pub healthcheck: Option<bool>, pub healthcheck: Option<bool>,
pub redirect_to: Option<String>,
pub authorization: Option<Auth>,
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Configuration { pub struct Configuration {
@@ -105,9 +116,10 @@ pub struct AppConfig {
pub config_tls_certificate: Option<String>, pub config_tls_certificate: Option<String>,
pub config_tls_key_file: Option<String>, pub config_tls_key_file: Option<String>,
pub proxy_address_tls: Option<String>, pub proxy_address_tls: Option<String>,
pub proxy_port_tls: Option<u16>, pub proxy_port_tls: Option<String>,
pub proxy_port: Option<String>,
pub local_server: Option<(String, u16)>, pub local_server: Option<(String, u16)>,
pub proxy_certificates: Option<String>, pub proxy_configs: Option<String>,
pub proxy_tls_grade: Option<String>, pub proxy_tls_grade: Option<String>,
pub file_server_address: Option<String>, pub file_server_address: Option<String>,
pub file_server_folder: Option<String>, pub file_server_folder: Option<String>,
@@ -115,9 +127,45 @@ pub struct AppConfig {
pub rungroup: Option<String>, pub rungroup: Option<String>,
} }
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct InnerAuth {
pub auth_type: Arc<str>,
pub auth_cred: Arc<str>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct InnerMap { pub struct InnerMap {
pub address: IpAddr, pub address: Arc<str>,
pub port: u16,
pub is_ssl: bool,
pub is_http2: bool,
pub to_https: bool,
pub rate_limit: Option<isize>,
pub healthcheck: Option<bool>,
pub redirect_to: Option<Arc<str>>,
pub authorization: Option<Arc<InnerAuth>>,
}
#[allow(dead_code)]
impl InnerMap {
pub fn new() -> Self {
Self {
address: Arc::from("127.0.0.1"),
port: Default::default(),
is_ssl: Default::default(),
is_http2: Default::default(),
to_https: Default::default(),
rate_limit: Default::default(),
healthcheck: Default::default(),
redirect_to: Default::default(),
authorization: Default::default(),
}
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct InnerMapForJson {
pub address: String,
pub port: u16, pub port: u16,
pub is_ssl: bool, pub is_ssl: bool,
pub is_http2: bool, pub is_http2: bool,
@@ -125,18 +173,8 @@ pub struct InnerMap {
pub rate_limit: Option<isize>, pub rate_limit: Option<isize>,
pub healthcheck: Option<bool>, pub healthcheck: Option<bool>,
} }
#[derive(Debug, Default, Serialize, Deserialize)]
#[allow(dead_code)] pub struct UpstreamSnapshotForJson {
impl InnerMap { pub backends: Vec<InnerMapForJson>,
pub fn new() -> Self { pub requests: usize,
Self {
address: "127.0.0.1".parse().unwrap(),
port: Default::default(),
is_ssl: Default::default(),
is_http2: Default::default(),
to_https: Default::default(),
rate_limit: Default::default(),
healthcheck: Default::default(),
}
}
} }

View File

@@ -1,19 +1,20 @@
use crate::utils::structs::{InnerMap, UpstreamsDashMap, UpstreamsIdMap}; use crate::tls::load;
use crate::utils::tls; use crate::tls::load::CertificateConfig;
use crate::utils::tls::CertificateConfig; use crate::utils::structs::{InnerMap, InnerMapForJson, UpstreamSnapshotForJson, UpstreamsDashMap, UpstreamsIdMap};
use dashmap::DashMap; use dashmap::DashMap;
use log::{error, info}; use log::{error, info};
use notify::{event::ModifyKind, Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; use notify::{event::ModifyKind, Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use port_check::is_port_reachable;
use privdrop::PrivDrop; use privdrop::PrivDrop;
use serde_json::{json, Value};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::any::type_name; use std::any::type_name;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::fmt::Write; use std::fmt::Write;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::net::TcpListener;
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use std::str::FromStr; use std::str::FromStr;
use std::sync::atomic::AtomicUsize; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc::{channel, Sender}; use std::sync::mpsc::{channel, Sender};
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@@ -100,29 +101,26 @@ pub fn clone_dashmap_into(original: &UpstreamsDashMap, cloned: &UpstreamsDashMap
} }
pub fn compare_dashmaps(map1: &UpstreamsDashMap, map2: &UpstreamsDashMap) -> bool { pub fn compare_dashmaps(map1: &UpstreamsDashMap, map2: &UpstreamsDashMap) -> bool {
let keys1: HashSet<_> = map1.iter().map(|entry| entry.key().clone()).collect(); if map1.len() != map2.len() {
let keys2: HashSet<_> = map2.iter().map(|entry| entry.key().clone()).collect();
if keys1 != keys2 {
return false; return false;
} }
for entry1 in map1.iter() { for entry1 in map1.iter() {
let hostname = entry1.key(); let Some(inner_map2) = map2.get(entry1.key()) else {
let inner_map1 = entry1.value();
let Some(inner_map2) = map2.get(hostname) else {
return false; return false;
}; };
let inner_keys1: HashSet<_> = inner_map1.iter().map(|e| e.key().clone()).collect(); let inner_map1 = entry1.value();
let inner_keys2: HashSet<_> = inner_map2.iter().map(|e| e.key().clone()).collect(); if inner_map1.len() != inner_map2.len() {
if inner_keys1 != inner_keys2 {
return false; return false;
} }
for path_entry in inner_map1.iter() { for path_entry in inner_map1.iter() {
let path = path_entry.key(); let Some(entry2) = inner_map2.get(path_entry.key()) else {
let (vec1, _counter1) = path_entry.value(); return false;
let Some(entry2) = inner_map2.get(path) else {
return false; // Path exists in map1 but not in map2
}; };
let (vec2, _counter2) = entry2.value(); let (vec1, _) = path_entry.value();
let (vec2, _) = entry2.value();
if vec1.len() != vec2.len() {
return false;
}
let set1: HashSet<_> = vec1.iter().collect(); let set1: HashSet<_> = vec1.iter().collect();
let set2: HashSet<_> = vec2.iter().collect(); let set2: HashSet<_> = vec2.iter().collect();
if set1 != set2 { if set1 != set2 {
@@ -133,11 +131,11 @@ pub fn compare_dashmaps(map1: &UpstreamsDashMap, map2: &UpstreamsDashMap) -> boo
true true
} }
pub fn merge_headers(target: &DashMap<Arc<str>, Vec<(Arc<str>, Arc<str>)>>, source: &DashMap<Arc<str>, Vec<(Arc<str>, Arc<str>)>>) { pub fn merge_headers(target: &DashMap<Arc<str>, Vec<(String, Arc<str>)>>, source: &DashMap<Arc<str>, Vec<(String, Arc<str>)>>) {
for entry in source.iter() { for entry in source.iter() {
let global_key = entry.key().clone(); let global_key = entry.key().clone();
let global_values = entry.value().clone(); let global_values = entry.value().clone();
let mut target_entry = target.entry(global_key).or_insert_with(Vec::new); let mut target_entry = target.entry(global_key).or_default();
target_entry.extend(global_values); target_entry.extend(global_values);
} }
} }
@@ -153,32 +151,50 @@ pub fn clone_idmap_into(original: &UpstreamsDashMap, cloned: &UpstreamsIdMap) {
let new_vec = vec.clone(); let new_vec = vec.clone();
for x in vec.iter() { for x in vec.iter() {
let mut id = String::new(); let mut id = String::new();
write!(&mut id, "{}:{}:{}", x.address, x.port, x.is_ssl).unwrap(); write!(
&mut id,
"{}:{}:{}:{}:{}:{}:{}:{:?}",
outer_entry.key(),
x.address,
x.port,
x.is_http2,
x.to_https,
x.rate_limit.unwrap_or_default(),
x.healthcheck.unwrap_or_default(),
x.authorization
)
.unwrap_or(());
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
// address: "127.0.0.3", port: 8000, is_ssl: false, is_http2: false, to_https: false, rate_limit: Some(200), healthcheck: None, authorization: None } }
hasher.update(id.clone().into_bytes()); hasher.update(id.clone().into_bytes());
let hash = hasher.finalize(); let hash = hasher.finalize();
let hex_hash = base16ct::lower::encode_string(&hash); let hex_hash = base16ct::lower::encode_string(&hash);
let hh = hex_hash[0..50].to_string(); let hh = hex_hash[0..50].to_string();
let to_add = InnerMap { let to_add = InnerMap {
address: "127.0.0.1".parse().unwrap(), address: Arc::from("127.0.0.1"),
port: 0, port: 0,
is_ssl: false, is_ssl: false,
is_http2: false, is_http2: false,
to_https: false, to_https: false,
rate_limit: None, rate_limit: None,
healthcheck: None, healthcheck: None,
redirect_to: None,
authorization: None,
}; };
cloned.insert(id, to_add); cloned.insert(id, Arc::from(to_add));
cloned.insert(hh, x.to_owned()); cloned.insert(hh, x.to_owned());
// println!("CLONNED :===========> {:?}", cloned);
} }
new_inner_map.insert(path.clone(), new_vec); new_inner_map.insert(path.clone(), new_vec);
} }
} }
info!("Upstreams are fully populated. Ready to server requests");
} }
pub fn listdir(dir: String) -> Vec<tls::CertificateConfig> { pub fn listdir(dir: String) -> Vec<load::CertificateConfig> {
let mut f = HashMap::new(); let mut f = HashMap::new();
let mut certificate_configs: Vec<tls::CertificateConfig> = vec![]; let mut certificate_configs: Vec<load::CertificateConfig> = vec![];
let paths = fs::read_dir(dir).unwrap(); let paths = fs::read_dir(dir).unwrap();
for path in paths { for path in paths {
let path_str = path.unwrap().path().to_str().unwrap().to_owned(); let path_str = path.unwrap().path().to_str().unwrap().to_owned();
@@ -196,13 +212,13 @@ pub fn listdir(dir: String) -> Vec<tls::CertificateConfig> {
certificate_configs.push(y); certificate_configs.push(y);
} }
} }
for (_, v) in f.iter() { // for (_, v) in f.iter() {
let y = CertificateConfig { // let y = CertificateConfig {
cert_path: v[0].clone(), // cert_path: v[0].clone(),
key_path: v[1].clone(), // key_path: v[1].clone(),
}; // };
certificate_configs.push(y); // certificate_configs.push(y);
} // }
certificate_configs certificate_configs
} }
@@ -237,14 +253,14 @@ pub fn drop_priv(user: String, group: String, http_addr: String, tls_addr: Optio
thread::sleep(time::Duration::from_millis(10)); thread::sleep(time::Duration::from_millis(10));
loop { loop {
thread::sleep(time::Duration::from_millis(10)); thread::sleep(time::Duration::from_millis(10));
if is_port_reachable(http_addr.clone()) { if TcpListener::bind(&http_addr).is_err() {
break; break;
} }
} }
if let Some(tls_addr) = tls_addr { if let Some(tls_addr) = tls_addr {
loop { loop {
thread::sleep(time::Duration::from_millis(10)); thread::sleep(time::Duration::from_millis(10));
if is_port_reachable(tls_addr.clone()) { if TcpListener::bind(&tls_addr).is_err() {
break; break;
} }
} }
@@ -258,14 +274,107 @@ pub fn drop_priv(user: String, group: String, http_addr: String, tls_addr: Optio
pub fn check_priv(addr: &str) { pub fn check_priv(addr: &str) {
let port = SocketAddr::from_str(addr).map(|sa| sa.port()).unwrap(); let port = SocketAddr::from_str(addr).map(|sa| sa.port()).unwrap();
match port < 1024 { if port < 1024 {
true => { let meta = std::fs::metadata("/proc/self").map(|m| m.uid()).unwrap();
let meta = std::fs::metadata("/proc/self").map(|m| m.uid()).unwrap(); if meta != 0 {
if meta != 0 { error!("Running on privileged port requires to start as ROOT");
error!("Running on privileged port requires to start as ROOT"); process::exit(1)
process::exit(1)
}
} }
false => {}
} }
} }
#[allow(dead_code)]
pub fn upstreams_to_json(upstreams: &UpstreamsDashMap) -> serde_json::Result<String> {
let mut outer = HashMap::new();
for outer_entry in upstreams.iter() {
let mut inner_map = HashMap::new();
for inner_entry in outer_entry.value().iter() {
let (backends, counter) = inner_entry.value();
inner_map.insert(
inner_entry.key().to_string(),
UpstreamSnapshotForJson {
backends: backends
.iter()
.map(|a| InnerMapForJson {
address: a.address.to_string(),
port: a.port,
is_ssl: a.is_ssl,
is_http2: a.is_http2,
to_https: a.to_https,
rate_limit: a.rate_limit,
healthcheck: a.healthcheck,
})
.collect(),
requests: counter.load(Ordering::Relaxed),
},
);
}
outer.insert(outer_entry.key().to_string(), inner_map);
}
// serde_json::to_string_pretty(&outer)
serde_json::to_string(&outer)
}
pub fn upstreams_liveness_json(configured: &UpstreamsDashMap, current: &UpstreamsDashMap) -> Value {
let mut result = serde_json::Map::new();
for host_entry in configured.iter() {
let hostname = host_entry.key().to_string();
let configured_paths = host_entry.value();
let mut paths_json = serde_json::Map::new();
for path_entry in configured_paths.iter() {
let path = path_entry.key().clone();
let (configured_backends, _) = path_entry.value();
let backends_json: Vec<Value> = configured_backends
.iter()
.map(|backend| {
let alive = if let Some(host_map) = current.get(&*hostname) {
if let Some(path_entry) = host_map.get(&*path) {
let list = &path_entry.value().0; // Vec<Arc<InnerMap>>
list.iter().any(|b| b.address == backend.address && b.port == backend.port)
} else {
false
}
} else {
false
};
json!({
"address": &*backend.address,
"port": backend.port,
"alive": alive
})
})
.collect();
paths_json.insert(
path.to_string(),
json!({
"backends": backends_json
}),
);
}
result.insert(hostname, Value::Object(paths_json));
}
Value::Object(result)
}
#[allow(dead_code)]
pub fn prepend(prefix: &str, val: &Option<Arc<str>>, uri: &str, port: &str) -> Option<String> {
val.as_ref().map(|s| {
let mut buf = String::with_capacity(32);
buf.push_str(prefix);
buf.push_str(s);
buf.push(':');
buf.push_str(port);
buf.push_str(uri);
buf
})
}

View File

@@ -1,3 +1,4 @@
use crate::tls::acme::order::refresh_order;
use crate::utils::discovery::{APIUpstreamProvider, ConsulProvider, Discovery, FromFileProvider, KubernetesProvider}; use crate::utils::discovery::{APIUpstreamProvider, ConsulProvider, Discovery, FromFileProvider, KubernetesProvider};
use crate::utils::parceyaml::load_configuration; use crate::utils::parceyaml::load_configuration;
use crate::utils::structs::Configuration; use crate::utils::structs::Configuration;
@@ -21,6 +22,7 @@ impl BackgroundService for LB {
let tx_api = tx.clone(); let tx_api = tx.clone();
let config = load_configuration(self.config.upstreams_conf.clone().as_str(), "filepath") let config = load_configuration(self.config.upstreams_conf.clone().as_str(), "filepath")
.await .await
.0
.expect("Failed to load configuration"); .expect("Failed to load configuration");
match config.typecfg.as_str() { match config.typecfg.as_str() {
@@ -30,43 +32,57 @@ impl BackgroundService for LB {
let file_load = FromFileProvider { let file_load = FromFileProvider {
path: self.config.upstreams_conf.clone(), path: self.config.upstreams_conf.clone(),
}; };
let _ = tokio::spawn(async move { file_load.start(tx).await }); // let _ = tokio::spawn(async move { file_load.start(tx).await });
drop(tokio::spawn(async move { file_load.start(tx).await }));
} }
"kubernetes" => { "kubernetes" => {
info!("Running Kubernetes discovery, requested type is: {}", config.typecfg); info!("Running Kubernetes discovery, requested type is: {}", config.typecfg);
let cf = Arc::from(config); let cf = Arc::from(config);
let kuber_load = KubernetesProvider { config: cf.clone() }; let kuber_load = KubernetesProvider { config: cf.clone() };
let _ = tokio::spawn(async move { kuber_load.start(tx).await }); drop(tokio::spawn(async move { kuber_load.start(tx).await }));
} }
"consul" => { "consul" => {
info!("Running Consul discovery, requested type is: {}", config.typecfg); info!("Running Consul discovery, requested type is: {}", config.typecfg);
let cf = Arc::from(config); let cf = Arc::from(config);
let consul_load = ConsulProvider { config: cf.clone() }; let consul_load = ConsulProvider { config: cf.clone() };
let _ = tokio::spawn(async move { consul_load.start(tx).await }); drop(tokio::spawn(async move { consul_load.start(tx).await }));
} }
_ => { _ => {
error!("Unknown discovery type: {}", config.typecfg); error!("Unknown discovery type: {}", config.typecfg);
} }
} }
let confdir = self.config.proxy_configs.clone().unwrap_or_else(|| "/tmp".to_string()) + "/autoconfigs";
let certdir = self.config.proxy_configs.clone().unwrap_or_else(|| "/tmp".to_string()) + "/certificates";
let api_load = APIUpstreamProvider { let api_load = APIUpstreamProvider {
address: self.config.config_address.clone(), address: self.config.config_address.clone(),
masterkey: self.config.master_key.clone(), masterkey: self.config.master_key.clone(),
config_api_enabled: self.config.config_api_enabled.clone(), config_api_enabled: self.config.config_api_enabled,
tls_address: self.config.config_tls_address.clone(), upstreams_file: self.config.upstreams_conf.clone(),
tls_certificate: self.config.config_tls_certificate.clone(), // certs_dir: self.config.proxy_certificates.clone().unwrap_or_else(|| "/tmp".to_string()),
tls_key_file: self.config.config_tls_key_file.clone(), config_dir: confdir.clone(),
certs_dir: certdir.clone(),
// tls_address: self.config.config_tls_address.clone(),
// tls_certificate: self.config.config_tls_certificate.clone(),
// tls_key_file: self.config.config_tls_key_file.clone(),
file_server_address: self.config.file_server_address.clone(), file_server_address: self.config.file_server_address.clone(),
file_server_folder: self.config.file_server_folder.clone(), file_server_folder: self.config.file_server_folder.clone(),
current_upstreams: self.ump_upst.clone(),
full_upstreams: self.ump_full.clone(),
}; };
// let crtdir = api_load.certs_dir.clone();
// let tx_api = tx.clone(); // let tx_api = tx.clone();
let _ = tokio::spawn(async move { api_load.start(tx_api).await }); drop(tokio::spawn(async move { api_load.start(tx_api).await }));
let uu = self.ump_upst.clone(); let uu = self.ump_upst.clone();
let ff = self.ump_full.clone(); let ff = self.ump_full.clone();
let im = self.ump_byid.clone(); let im = self.ump_byid.clone();
let (hc_method, hc_interval) = (self.config.hc_method.clone(), self.config.hc_interval); let (hc_method, hc_interval) = (self.config.hc_method.clone(), self.config.hc_interval);
let _ = tokio::spawn(async move { healthcheck::hc2(uu, ff, im, (&*hc_method.to_string(), hc_interval.to_string().parse().unwrap())).await }); drop(tokio::spawn(async move {
healthcheck::hc2(uu, ff, im, (&*hc_method.to_string(), hc_interval.to_string().parse().unwrap())).await
}));
drop(tokio::spawn(async move { refresh_order(certdir, confdir).await }));
loop { loop {
tokio::select! { tokio::select! {
@@ -74,57 +90,49 @@ impl BackgroundService for LB {
break; break;
} }
val = rx.next() => { val = rx.next() => {
match val { if let Some(ss) = val {
Some(ss) => { clone_dashmap_into(&ss.upstreams, &self.ump_full);
clone_dashmap_into(&ss.upstreams, &self.ump_full); clone_dashmap_into(&ss.upstreams, &self.ump_upst);
clone_dashmap_into(&ss.upstreams, &self.ump_upst); let current = self.extraparams.load_full();
let current = self.extraparams.load_full(); let mut new = (*current).clone();
let mut new = (*current).clone(); new.to_https = ss.extraparams.to_https;
new.sticky_sessions = ss.extraparams.sticky_sessions; new.sticky_sessions = ss.extraparams.sticky_sessions;
new.to_https = ss.extraparams.to_https; new.authentication = ss.extraparams.authentication.clone();
new.authentication = ss.extraparams.authentication.clone(); new.rate_limit = ss.extraparams.rate_limit;
new.rate_limit = ss.extraparams.rate_limit; self.extraparams.store(Arc::new(new));
self.extraparams.store(Arc::new(new)); self.client_headers.clear();
self.client_headers.clear(); self.server_headers.clear();
self.server_headers.clear(); for entry in ss.upstreams.iter() {
let global_key = entry.key().clone();
let client_global_values = DashMap::new();
let server_global_values = DashMap::new();
for entry in ss.upstreams.iter() { let mut client_target_entry = ss.client_headers.entry(global_key.clone()).or_insert_with(DashMap::new);
let global_key = entry.key().clone(); client_target_entry.extend(client_global_values);
let client_global_values = DashMap::new(); let mut server_target_entry = ss.server_headers.entry(global_key).or_insert_with(DashMap::new);
let server_global_values = DashMap::new(); server_target_entry.extend(server_global_values);
self.server_headers.insert(server_target_entry.key().to_owned(), server_target_entry.value().to_owned());
let mut client_target_entry = ss.client_headers.entry(global_key.clone()).or_insert_with(DashMap::new); }
client_target_entry.extend(client_global_values); for path in ss.client_headers.iter() {
let mut server_target_entry = ss.server_headers.entry(global_key).or_insert_with(DashMap::new); let path_key = path.key().clone();
server_target_entry.extend(server_global_values); let path_headers = path.value().clone();
self.server_headers.insert(server_target_entry.key().to_owned(), server_target_entry.value().to_owned()); self.client_headers.insert(path_key.clone(), path_headers);
} if let Some(global_headers) = ss.client_headers.get("GLOBAL_CLIENT_HEADERS") {
if let Some(existing_headers) = self.client_headers.get_mut(&path_key) {
for path in ss.client_headers.iter() { merge_headers(&existing_headers, &global_headers);
let path_key = path.key().clone(); }
let path_headers = path.value().clone(); }
self.client_headers.insert(path_key.clone(), path_headers); }
if let Some(global_headers) = ss.client_headers.get("GLOBAL_CLIENT_HEADERS") { for path in ss.server_headers.iter() {
if let Some(existing_headers) = self.client_headers.get_mut(&path_key) { let path_key = path.key().clone();
merge_headers(&existing_headers, &global_headers); let path_headers = path.value().clone();
} self.server_headers.insert(path_key.clone(), path_headers);
} if let Some(global_headers) = ss.server_headers.get("GLOBAL_SERVER_HEADERS") {
} if let Some(existing_headers) = self.server_headers.get_mut(&path_key) {
merge_headers(&existing_headers, &global_headers);
for path in ss.server_headers.iter() { }
let path_key = path.key().clone(); }
let path_headers = path.value().clone();
self.server_headers.insert(path_key.clone(), path_headers);
if let Some(global_headers) = ss.server_headers.get("GLOBAL_SERVER_HEADERS") {
if let Some(existing_headers) = self.server_headers.get_mut(&path_key) {
merge_headers(&existing_headers, &global_headers);
}
}
}
// info!("Upstreams list is changed, updating to:");
// print_upstreams(&self.ump_full);
} }
None => {}
} }
} }
} }

View File

@@ -6,21 +6,20 @@ use std::sync::Arc;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct GetHostsReturHeaders { pub struct GetHostsReturHeaders {
pub client_headers: Option<Vec<(Arc<str>, Arc<str>)>>, pub client_headers: Option<Vec<(String, Arc<str>)>>,
pub server_headers: Option<Vec<(Arc<str>, Arc<str>)>>, pub server_headers: Option<Vec<(String, Arc<str>)>>,
} }
#[async_trait] #[async_trait]
pub trait GetHost { pub trait GetHost {
// fn get_host<'a>(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<&'a InnerMap>; fn get_host(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<Arc<InnerMap>>;
fn get_host(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<InnerMap>;
fn get_header(&self, peer: &str, path: &str) -> Option<GetHostsReturHeaders>; fn get_header(&self, peer: &str, path: &str) -> Option<GetHostsReturHeaders>;
// fn get_upstreams(&self) -> Arc<UpstreamsDashMap>;
} }
#[async_trait] #[async_trait]
impl GetHost for LB { impl GetHost for LB {
fn get_host(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<InnerMap> { fn get_host(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<Arc<InnerMap>> {
if let Some(b) = backend_id { if let Some(b) = backend_id {
if let Some(bb) = self.ump_byid.get(b) { if let Some(bb) = self.ump_byid.get(b) {
return Some(bb.value().clone()); return Some(bb.value().clone());
@@ -54,49 +53,66 @@ impl GetHost for LB {
} }
fn get_header(&self, peer: &str, path: &str) -> Option<GetHostsReturHeaders> { fn get_header(&self, peer: &str, path: &str) -> Option<GetHostsReturHeaders> {
let client_entry = self.client_headers.get(peer)?; let client_entry = self.client_headers.get(peer);
let server_entry = self.server_headers.get(peer)?; let server_entry = self.server_headers.get(peer);
if client_entry.is_none() && server_entry.is_none() {
return None;
}
let mut current_path = path; let mut current_path = path;
let mut clnt_match = None; let mut clnt_match = None;
loop { if let Some(client_entry) = client_entry {
if let Some(entry) = client_entry.get(current_path) { loop {
if !entry.value().is_empty() { if let Some(entry) = client_entry.get(current_path) {
clnt_match = Some(entry.value().clone()); if !entry.value().is_empty() {
clnt_match = Some(entry.value().clone());
break;
}
}
if current_path == "/" {
break;
}
if let Some(pos) = current_path.rfind('/') {
current_path = if pos == 0 { "/" } else { &current_path[..pos] };
} else {
break; break;
} }
}
if let Some(pos) = current_path.rfind('/') {
current_path = if pos == 0 { "/" } else { &current_path[..pos] };
} else {
break;
} }
} }
current_path = path; current_path = path;
let mut serv_match = None; let mut serv_match = None;
loop { if let Some(server_entry) = server_entry {
if let Some(entry) = server_entry.get(current_path) { loop {
if !entry.value().is_empty() { if let Some(entry) = server_entry.get(current_path) {
serv_match = Some(entry.value().clone());
break;
}
}
if let Some(pos) = current_path.rfind('/') {
current_path = if pos == 0 { "/" } else { &current_path[..pos] };
} else {
break;
}
if serv_match.is_none() {
if let Some(entry) = server_entry.get("/") {
if !entry.value().is_empty() { if !entry.value().is_empty() {
serv_match = Some(entry.value().clone()); serv_match = Some(entry.value().clone());
break; break;
} }
} }
if current_path == "/" {
if let Some(entry) = server_entry.get("/") {
if !entry.value().is_empty() {
serv_match = Some(entry.value().clone());
break;
}
}
break;
}
if let Some(pos) = current_path.rfind('/') {
current_path = if pos == 0 { "/" } else { &current_path[..pos] };
} else {
break;
}
} }
} }
Some(GetHostsReturHeaders { let result = GetHostsReturHeaders {
client_headers: clnt_match, client_headers: clnt_match,
server_headers: serv_match, server_headers: serv_match,
}) };
if result.client_headers.is_some() || result.server_headers.is_some() {
Some(result)
} else {
None
}
} }
} }

View File

@@ -1,12 +1,12 @@
use crate::utils::auth::authenticate; use crate::utils::auth::authenticate;
use crate::utils::metrics::*; use crate::utils::metrics::*;
use crate::utils::structs::{AppConfig, Extraparams, Headers, InnerMap, UpstreamsDashMap, UpstreamsIdMap}; use crate::utils::structs::{AppConfig, Extraparams, Headers, InnerMap, UpstreamsDashMap, UpstreamsIdMap};
use crate::web::gethosts::GetHost; use crate::web::gethosts::{GetHost, GetHostsReturHeaders};
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
use async_trait::async_trait; use async_trait::async_trait;
use axum::body::Bytes; use axum::body::Bytes;
use dashmap::DashMap;
use log::{debug, error, warn}; use log::{debug, error, warn};
use once_cell::sync::Lazy;
use pingora::http::{RequestHeader, ResponseHeader, StatusCode}; use pingora::http::{RequestHeader, ResponseHeader, StatusCode};
use pingora::prelude::*; use pingora::prelude::*;
use pingora::ErrorSource::Upstream; use pingora::ErrorSource::Upstream;
@@ -14,12 +14,17 @@ use pingora_core::listeners::ALPN;
use pingora_core::prelude::HttpPeer; use pingora_core::prelude::HttpPeer;
use pingora_limits::rate::Rate; use pingora_limits::rate::Rate;
use pingora_proxy::{ProxyHttp, Session}; use pingora_proxy::{ProxyHttp, Session};
// use std::net::{IpAddr, Ipv4Addr}; use sha2::{Digest, Sha256};
use std::sync::Arc; use std::cell::RefCell;
use std::fmt::Write;
use std::sync::{Arc, LazyLock};
use std::time::Duration; use std::time::Duration;
use tokio::time::Instant; use tokio::time::Instant;
static RATE_LIMITER: Lazy<Rate> = Lazy::new(|| Rate::new(Duration::from_secs(1))); static REVERSE_STORE: LazyLock<DashMap<String, String>> = LazyLock::new(DashMap::new);
thread_local! {static IP_BUFFER: RefCell<String> = RefCell::new(String::with_capacity(50));}
pub static RATE_LIMITER: LazyLock<Rate> = LazyLock::new(|| Rate::new(Duration::from_secs(1)));
pub static LOCALHOST: LazyLock<Arc<str>> = LazyLock::new(|| Arc::from("localhost"));
#[derive(Clone)] #[derive(Clone)]
pub struct LB { pub struct LB {
@@ -33,15 +38,13 @@ pub struct LB {
} }
pub struct Context { pub struct Context {
backend_id: Arc<str>, backend_id: Option<String>,
// backend_id: Arc<(IpAddr, u16, bool)>, sticky_sessions: bool,
to_https: bool,
redirect_to: Arc<str>,
start_time: Instant, start_time: Instant,
hostname: Option<Arc<str>>, hostname: Option<Arc<str>>,
upstream_peer: Option<InnerMap>, upstream_peer: Option<Arc<InnerMap>>,
extraparams: arc_swap::Guard<Arc<Extraparams>>, extraparams: arc_swap::Guard<Arc<Extraparams>>,
client_headers: Arc<Vec<(Arc<str>, Arc<str>)>>, client_headers: Option<Vec<(String, Arc<str>)>>,
} }
#[async_trait] #[async_trait]
@@ -49,71 +52,95 @@ impl ProxyHttp for LB {
type CTX = Context; type CTX = Context;
fn new_ctx(&self) -> Self::CTX { fn new_ctx(&self) -> Self::CTX {
Context { Context {
backend_id: Arc::from(""), backend_id: None,
// backend_id: Arc::new((IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0, false)), sticky_sessions: false,
to_https: false,
redirect_to: Arc::from(""),
start_time: Instant::now(), start_time: Instant::now(),
hostname: None, hostname: None,
upstream_peer: None, upstream_peer: None,
extraparams: self.extraparams.load(), extraparams: self.extraparams.load(),
client_headers: Arc::new(Vec::new()), client_headers: None,
} }
} }
async fn request_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result<bool> { async fn request_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result<bool> {
let ep = _ctx.extraparams.clone(); ACTIVE_SESSIONS.inc();
let hostname = return_header_host_from_upstream(session, &self.ump_upst);
if let Some(auth) = ep.authentication.get("authorization") {
let authenticated = authenticate(&auth.value(), &session);
if !authenticated {
let _ = session.respond_error(401).await;
warn!("Forbidden: {:?}, {}", session.client_addr(), session.req_header().uri.path());
return Ok(true);
}
};
let hostname = return_header_host(&session);
_ctx.hostname = hostname; _ctx.hostname = hostname;
let mut backend_id = None; let mut backend_id = None;
if _ctx.extraparams.sticky_sessions {
if ep.sticky_sessions {
if let Some(cookies) = session.req_header().headers.get("cookie") { if let Some(cookies) = session.req_header().headers.get("cookie") {
if let Ok(cookie_str) = cookies.to_str() { if let Ok(cookie_str) = cookies.to_str() {
for cookie in cookie_str.split(';') { if let Some(pos) = cookie_str.find("backend_id=") {
let trimmed = cookie.trim(); let value = &cookie_str[pos + "backend_id=".len()..];
if let Some(value) = trimmed.strip_prefix("backend_id=") { let end = value.find(';').unwrap_or(value.len());
backend_id = Some(value); backend_id = Some(&value[..end]);
break;
}
} }
} }
} }
} }
match _ctx.hostname.as_ref() { match _ctx.hostname.as_ref() {
None => return Ok(false), None => return Ok(false),
Some(host) => { Some(host) => {
// let optioninnermap = self.get_host(host.as_str(), host.as_str(), backend_id);
let optioninnermap = self.get_host(host, session.req_header().uri.path(), backend_id); let optioninnermap = self.get_host(host, session.req_header().uri.path(), backend_id);
match optioninnermap { match optioninnermap {
None => return Ok(false), None => return Ok(false),
Some(ref innermap) => { Some(ref innermap) => {
if let Some(rate) = innermap.rate_limit.or(ep.rate_limit) { if let Some(auth) = _ctx.extraparams.authentication.as_ref().or(innermap.authorization.as_ref()) {
// let rate_key = session.client_addr().and_then(|addr| addr.as_inet()).map(|inet| inet.ip().to_string()).unwrap_or_else(|| host.to_string()); if !authenticate(&auth.auth_type, &auth.auth_cred, session).await {
let _ = session.respond_error(401).await;
warn!("Forbidden: {:?}, {}", session.client_addr(), session.req_header().uri.path());
return Ok(true);
}
}
if let Some(rate) = innermap.rate_limit.or(_ctx.extraparams.rate_limit) {
let rate_key = session.client_addr().and_then(|addr| addr.as_inet()).map(|inet| inet.ip()); let rate_key = session.client_addr().and_then(|addr| addr.as_inet()).map(|inet| inet.ip());
let curr_window_requests = RATE_LIMITER.observe(&rate_key, 1); let curr_window_requests = RATE_LIMITER.observe(&rate_key, 1);
if curr_window_requests > rate { if curr_window_requests > rate {
let mut header = ResponseHeader::build(429, None).unwrap(); let header = ResponseHeader::build(429, None)?;
header.insert_header("X-Rate-Limit-Limit", rate.to_string()).unwrap();
header.insert_header("X-Rate-Limit-Remaining", "0").unwrap();
header.insert_header("X-Rate-Limit-Reset", "1").unwrap();
session.set_keepalive(None); session.set_keepalive(None);
session.write_response_header(Box::new(header), true).await?; session.write_response_header(Box::new(header), true).await?;
debug!("Rate limited: {:?}, {}", rate_key, rate); debug!("Rate limited: {:?}, {}", rate_key, rate);
return Ok(true); return Ok(true);
} }
} }
if let Some(redirect_to) = &innermap.redirect_to {
let uri = session.req_header().uri.path();
let capacity = redirect_to.len() + uri.len();
let mut s = String::with_capacity(capacity);
s.push_str(redirect_to);
s.push_str(uri);
let mut resp = ResponseHeader::build(StatusCode::MOVED_PERMANENTLY, None)?;
resp.insert_header("Location", s)?;
resp.insert_header("Content-Length", "0")?;
session.write_response_header(Box::new(resp), true).await?;
return Ok(true);
}
if _ctx.extraparams.to_https.unwrap_or(false) || innermap.to_https {
if let Some(stream) = session.stream() {
if stream.get_ssl().is_none() {
if let Some(host) = _ctx.hostname.as_ref() {
let port = self.config.proxy_port_tls.as_deref().unwrap_or("443");
let uri = session.req_header().uri.path();
let capacity = host.len() + uri.len() + 8;
let mut s = String::with_capacity(capacity);
s.push_str("https://");
s.push_str(host);
if port != "443" {
s.push(':');
s.push_str(port);
}
s.push_str(uri);
let mut resp = ResponseHeader::build(StatusCode::MOVED_PERMANENTLY, None)?;
resp.insert_header("Location", s)?;
resp.insert_header("Content-Length", "0")?;
session.write_response_header(Box::new(resp), true).await?;
return Ok(true);
}
}
}
}
} }
} }
_ctx.upstream_peer = optioninnermap; _ctx.upstream_peer = optioninnermap;
@@ -125,30 +152,33 @@ impl ProxyHttp for LB {
match ctx.hostname.as_ref() { match ctx.hostname.as_ref() {
Some(hostname) => match ctx.upstream_peer.as_ref() { Some(hostname) => match ctx.upstream_peer.as_ref() {
Some(innermap) => { Some(innermap) => {
let mut peer = Box::new(HttpPeer::new((innermap.address.clone(), innermap.port.clone()), innermap.is_ssl, String::new())); let mut peer = Box::new(HttpPeer::new((&*innermap.address, innermap.port), innermap.is_ssl, hostname.to_string()));
if innermap.is_http2 { if innermap.is_http2 {
peer.options.alpn = ALPN::H2; peer.options.alpn = ALPN::H2;
} }
if innermap.is_ssl { if innermap.is_ssl {
peer.sni = hostname.to_string();
peer.options.verify_cert = false; peer.options.verify_cert = false;
peer.options.verify_hostname = false; peer.options.verify_hostname = false;
} }
if ctx.to_https || innermap.to_https { if ctx.extraparams.sticky_sessions {
if let Some(stream) = session.stream() { let mut s = String::with_capacity(64);
if stream.get_ssl().is_none() { write!(
if let Some(addr) = session.server_addr() { &mut s,
if let Some((host, _)) = addr.to_string().split_once(':') { "{}:{}:{}:{}:{}:{}:{}:{:?}",
let uri = session.req_header().uri.path_and_query().map_or("/", |pq| pq.as_str()); hostname,
let port = self.config.proxy_port_tls.unwrap_or(403); innermap.address,
ctx.to_https = true; innermap.port,
ctx.redirect_to = Arc::from(format!("https://{}:{}{}", host, port, uri)); innermap.is_http2,
} innermap.to_https,
} innermap.rate_limit.unwrap_or_default(),
} innermap.healthcheck.unwrap_or_default(),
} innermap.authorization
)
.unwrap_or(());
ctx.backend_id = Some(s);
ctx.sticky_sessions = true;
} }
ctx.backend_id = Arc::from(format!("{}:{}:{}", innermap.address, innermap.port, innermap.is_ssl));
Ok(peer) Ok(peer)
} }
None => { None => {
@@ -165,7 +195,6 @@ impl ProxyHttp for LB {
} }
}, },
None => { None => {
// session.respond_error_with_body(502, Bytes::from("502 Bad Gateway\n")).await.expect("Failed to send error");
if let Err(e) = session.respond_error_with_body(502, Bytes::from("502 Bad Gateway\n")).await { if let Err(e) = session.respond_error_with_body(502, Bytes::from("502 Bad Gateway\n")).await {
error!("Failed to send error response: {:?}", e); error!("Failed to send error response: {:?}", e);
} }
@@ -181,47 +210,61 @@ impl ProxyHttp for LB {
} }
async fn upstream_request_filter(&self, session: &mut Session, upstream_request: &mut RequestHeader, ctx: &mut Self::CTX) -> Result<()> { async fn upstream_request_filter(&self, session: &mut Session, upstream_request: &mut RequestHeader, ctx: &mut Self::CTX) -> Result<()> {
if let Some(hostname) = ctx.hostname.as_ref() { if let Some(client_ip) = session.client_addr() {
upstream_request.insert_header("Host", hostname.as_ref())?; IP_BUFFER.with(|buffer| {
} let mut buf = buffer.borrow_mut();
if let Some(peer) = ctx.upstream_peer.as_ref() { buf.clear();
upstream_request.insert_header("X-Forwarded-For", peer.address.to_string())?; write!(buf, "{}", client_ip).unwrap_or(());
upstream_request.append_header("X-Forwarded-For", buf.as_str()).unwrap_or(false);
});
} }
if let Some(headers) = self.get_header(ctx.hostname.as_ref().unwrap_or(&Arc::from("localhost")), session.req_header().uri.path()) { let hostname = ctx.hostname.as_deref().unwrap_or("localhost");
if let Some(server_headers) = headers.server_headers { let path = session.req_header().uri.path();
for k in server_headers { let GetHostsReturHeaders { server_headers, client_headers } = match self.get_header(hostname, path) {
upstream_request.insert_header(k.0.to_string(), k.1.as_ref())?; Some(h) => h,
} None => return Ok(()),
} };
if let Some(client_headers) = headers.client_headers {
let converted: Vec<(Arc<str>, Arc<str>)> = client_headers.into_iter().map(|(k, v)| (Arc::<str>::from(k), Arc::<str>::from(v))).collect();
ctx.client_headers = Arc::new(converted); if let Some(sh) = server_headers {
for (k, v) in sh {
upstream_request.insert_header(k, v.as_ref())?;
} }
} }
if let Some(ch) = client_headers {
ctx.client_headers = Some(ch);
}
Ok(()) Ok(())
} }
async fn response_filter(&self, session: &mut Session, _upstream_response: &mut ResponseHeader, ctx: &mut Self::CTX) -> Result<()> { async fn response_filter(&self, _session: &mut Session, _upstream_response: &mut ResponseHeader, ctx: &mut Self::CTX) -> Result<()> {
if ctx.extraparams.sticky_sessions { if ctx.sticky_sessions {
let backend_id = ctx.backend_id.clone(); if let Some(bid) = &ctx.backend_id {
if let Some(bid) = self.ump_byid.get(backend_id.as_ref()) { let tt = if let Some(existing) = REVERSE_STORE.get(bid) {
let _ = _upstream_response.insert_header("set-cookie", format!("backend_id={}; Path=/; Max-Age=600; HttpOnly; SameSite=Lax", bid.address)); existing.value().clone()
} else {
let mut hasher = Sha256::new();
hasher.update(bid.as_bytes());
let hash = hasher.finalize();
let hex_hash = base16ct::lower::encode_string(&hash);
let hh = hex_hash[0..50].to_string();
REVERSE_STORE.insert(bid.clone(), hh.clone());
REVERSE_STORE.insert(hh.clone(), bid.clone());
hh
};
// let _ = _upstream_response.insert_header("set-cookie", format!("backend_id={}; Path=/; Max-Age=600; HttpOnly; SameSite=Lax", tt));
let mut buf = String::with_capacity(80);
buf.push_str("backend_id=");
buf.push_str(&tt);
buf.push_str("; Path=/; Max-Age=86400; HttpOnly; SameSite=Lax");
let _ = _upstream_response.insert_header("set-cookie", buf.as_str());
} }
} }
if ctx.to_https {
let mut redirect_response = ResponseHeader::build(StatusCode::MOVED_PERMANENTLY, None)?;
redirect_response.insert_header("Location", ctx.redirect_to.as_ref())?;
redirect_response.insert_header("Content-Length", "0")?;
session.write_response_header(Box::new(redirect_response), false).await?;
}
for (key, value) in ctx.client_headers.iter() { if let Some(client_headers) = &ctx.client_headers {
_upstream_response.insert_header(key.to_string(), value.as_ref()).unwrap(); for (k, v) in client_headers.iter() {
_upstream_response.append_header(k.clone(), v.as_ref())?;
}
} }
session.set_keepalive(Some(300));
Ok(()) Ok(())
} }
@@ -229,28 +272,24 @@ impl ProxyHttp for LB {
let response_code = session.response_written().map_or(0, |resp| resp.status.as_u16()); let response_code = session.response_written().map_or(0, |resp| resp.status.as_u16());
debug!("{}, response code: {response_code}", self.request_summary(session, ctx)); debug!("{}, response code: {response_code}", self.request_summary(session, ctx));
let m = &MetricTypes { let m = &MetricTypes {
method: session.req_header().method.to_string(), method: session.req_header().method.clone(),
code: session.response_written().map(|resp| resp.status.as_str().to_owned()).unwrap_or("0".to_string()), code: session.response_written().map(|resp| resp.status),
latency: ctx.start_time.elapsed(), latency: ctx.start_time.elapsed(),
version: session.req_header().version, version: session.req_header().version,
upstream: ctx.hostname.take().unwrap_or_else(|| LOCALHOST.clone()),
}; };
calc_metrics(m); calc_metrics(m);
ACTIVE_SESSIONS.dec();
} }
} }
fn return_header_host(session: &Session) -> Option<Arc<str>> { fn return_header_host_from_upstream(session: &Session, ump_upst: &UpstreamsDashMap) -> Option<Arc<str>> {
if session.is_http2() { let host_str = if session.is_http2() {
match session.req_header().uri.host() { session.req_header().uri.host()?
Some(host) => Option::from(Arc::from(host)),
None => None,
}
} else { } else {
match session.req_header().headers.get("host") { let h = session.req_header().headers.get("host")?.to_str().ok()?;
Some(host) => { h.split_once(':').map_or(h, |(host, _)| host)
let header_host: &str = host.to_str().unwrap().split_once(':').map_or(host.to_str().unwrap(), |(h, _)| h); };
Option::from(Arc::<str>::from(header_host))
} ump_upst.get(host_str).or_else(|| ump_upst.get("DEFAULT")).map(|entry| entry.key().clone())
None => None,
}
}
} }

View File

@@ -1,7 +1,8 @@
// use rustls::crypto::ring::default_provider; // use rustls::crypto::ring::default_provider;
use crate::tls::grades;
use crate::tls::load;
use crate::tls::load::CertificateConfig;
use crate::utils::structs::Extraparams; use crate::utils::structs::Extraparams;
use crate::utils::tls;
use crate::utils::tls::CertificateConfig;
use crate::utils::tools::*; use crate::utils::tools::*;
use crate::web::proxyhttp::LB; use crate::web::proxyhttp::LB;
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
@@ -14,10 +15,10 @@ use pingora_core::prelude::{background_service, Opt};
use pingora_core::server::Server; use pingora_core::server::Server;
use std::sync::mpsc::{channel, Receiver, Sender}; use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::Arc; use std::sync::Arc;
use std::thread; use std::{fs, thread};
pub fn run() { pub fn run() {
// default_provider().install_default().expect("Failed to install rustls crypto provider"); // default_provider().install_default().expect("Failed to install rustls crypto provider");
let parameters = Some(Opt::parse_args()).unwrap(); let parameters = Opt::parse_args();
let file = parameters.conf.clone().unwrap(); let file = parameters.conf.clone().unwrap();
let maincfg = crate::utils::parceyaml::parce_main_config(file.as_str()); let maincfg = crate::utils::parceyaml::parce_main_config(file.as_str());
@@ -31,9 +32,9 @@ pub fn run() {
let sh_config = Arc::new(DashMap::new()); let sh_config = Arc::new(DashMap::new());
let ec_config = Arc::new(ArcSwap::from_pointee(Extraparams { let ec_config = Arc::new(ArcSwap::from_pointee(Extraparams {
sticky_sessions: false,
to_https: None, to_https: None,
authentication: DashMap::new(), sticky_sessions: false,
authentication: None,
rate_limit: None, rate_limit: None,
})); }));
@@ -59,46 +60,44 @@ pub fn run() {
check_priv(bind_address_http.as_str()); check_priv(bind_address_http.as_str());
match bind_address_tls { if let Some(bind_address_tls) = bind_address_tls {
Some(bind_address_tls) => { check_priv(bind_address_tls.as_str());
check_priv(bind_address_tls.as_str()); let (tx, rx): (Sender<Vec<CertificateConfig>>, Receiver<Vec<CertificateConfig>>) = channel();
let (tx, rx): (Sender<Vec<CertificateConfig>>, Receiver<Vec<CertificateConfig>>) = channel(); let certs_path = cfg.proxy_configs.clone().unwrap() + "/certificates";
let certs_path = cfg.proxy_certificates.clone().unwrap();
thread::spawn(move || {
watch_folder(certs_path, tx).unwrap();
});
let certificate_configs = rx.recv().unwrap();
let first_set = tls::Certificates::new(&certificate_configs, grade.as_str()).unwrap_or_else(|| panic!("Unable to load initial certificate info"));
let certificates = Arc::new(ArcSwap::from_pointee(first_set));
let certs_for_callback = certificates.clone();
let certs_for_watcher = certificates.clone(); if fs::metadata(certs_path.clone()).is_err() {
let new_certs = tls::Certificates::new(&certificate_configs, grade.as_str()); fs::create_dir_all(certs_path.clone()).unwrap();
certs_for_watcher.store(Arc::new(new_certs.unwrap()));
let mut tls_settings =
TlsSettings::intermediate(&certs_for_callback.load().default_cert_path, &certs_for_callback.load().default_key_path).expect("unable to load or parse cert/key");
tls::set_tsl_grade(&mut tls_settings, grade.as_str());
tls_settings.set_servername_callback(move |ssl_ref: &mut SslRef, ssl_alert: &mut SslAlert| certs_for_callback.load().server_name_callback(ssl_ref, ssl_alert));
tls_settings.set_alpn_select_callback(tls::prefer_h2);
proxy.add_tls_with_settings(&bind_address_tls, None, tls_settings);
let certs_for_watcher = certificates.clone();
thread::spawn(move || {
while let Ok(new_configs) = rx.recv() {
let new_certs = tls::Certificates::new(&new_configs, grade.as_str());
match new_certs {
Some(new_certs) => {
certs_for_watcher.store(Arc::new(new_certs));
}
None => {}
};
}
});
} }
None => {} thread::spawn(move || {
watch_folder(certs_path, tx).unwrap();
});
let certificate_configs = rx.recv().unwrap();
let first_set = load::Certificates::new(&certificate_configs, grade.as_str()).unwrap_or_else(|| panic!("Unable to load initial certificate info"));
let certificates = Arc::new(ArcSwap::from_pointee(first_set));
let certs_for_callback = certificates.clone();
let certs_for_watcher = certificates.clone();
let new_certs = load::Certificates::new(&certificate_configs, grade.as_str());
certs_for_watcher.store(Arc::new(new_certs.unwrap()));
let mut tls_settings =
TlsSettings::intermediate(&certs_for_callback.load().default_cert_path, &certs_for_callback.load().default_key_path).expect("unable to load or parse cert/key");
grades::set_tsl_grade(&mut tls_settings, grade.as_str());
tls_settings.set_servername_callback(move |ssl_ref: &mut SslRef, ssl_alert: &mut SslAlert| certs_for_callback.load().server_name_callback(ssl_ref, ssl_alert));
tls_settings.set_alpn_select_callback(grades::prefer_h2);
proxy.add_tls_with_settings(&bind_address_tls, None, tls_settings);
let certs_for_watcher = certificates.clone();
thread::spawn(move || {
while let Ok(new_configs) = rx.recv() {
let new_certs = load::Certificates::new(&new_configs, grade.as_str());
if let Some(new_certs) = new_certs {
certs_for_watcher.store(Arc::new(new_certs));
};
}
});
} }
info!("Running HTTP listener on :{}", bind_address_http.as_str()); info!("Running HTTP listener on :{}", bind_address_http.as_str());
proxy.add_tcp(bind_address_http.as_str()); proxy.add_tcp(bind_address_http.as_str());

View File

@@ -1,31 +1,30 @@
// use std::net::SocketAddr;
use crate::tls::acme::order::CHALLENGES;
// use axum_server::tls_openssl::OpenSSLConfig;
use crate::tls::acme::{account, order};
use crate::utils::discovery::APIUpstreamProvider; use crate::utils::discovery::APIUpstreamProvider;
use crate::utils::structs::Configuration; use crate::utils::jwt::Claims;
use crate::utils::structs::{Config, Configuration, UpstreamsDashMap};
use crate::utils::tools::{upstreams_liveness_json, upstreams_to_json};
use axum::body::Body; use axum::body::Body;
use axum::extract::{Query, State}; use axum::extract::{Query, State};
use axum::http::{Response, StatusCode}; use axum::http::{header::HeaderMap, Response, StatusCode};
use axum::response::IntoResponse; use axum::response::IntoResponse;
use axum::routing::{get, post}; use axum::routing::{any, get, post};
use axum::{Json, Router}; use axum::{Json, Router};
use axum_server::tls_openssl::OpenSSLConfig;
use futures::channel::mpsc::Sender; use futures::channel::mpsc::Sender;
use futures::SinkExt; use futures::SinkExt;
use jsonwebtoken::{encode, EncodingKey, Header}; use jsonwebtoken::{encode, EncodingKey, Header};
use log::{error, info, warn}; use log::{debug, error, info, warn};
use prometheus::{gather, Encoder, TextEncoder}; use prometheus::{gather, Encoder, TextEncoder};
use serde::{Deserialize, Serialize}; use serde::Serialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::net::SocketAddr; use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
use subtle::ConstantTimeEq;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
#[derive(Deserialize)]
struct InputKey {
master_key: String,
owner: String,
valid: u64,
}
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
struct OutToken { struct OutToken {
token: String, token: String,
@@ -34,18 +33,28 @@ struct OutToken {
#[derive(Clone)] #[derive(Clone)]
struct AppState { struct AppState {
master_key: String, master_key: String,
cert_creds: String,
certs_dir: String,
upstreams_file: String,
config_sender: Sender<Configuration>, config_sender: Sender<Configuration>,
config_api_enabled: bool, config_api_enabled: bool,
current_upstreams: Arc<UpstreamsDashMap>,
full_upstreams: Arc<UpstreamsDashMap>,
} }
#[allow(unused_mut)] #[allow(unused_mut)]
pub async fn run_server(config: &APIUpstreamProvider, mut to_return: Sender<Configuration>) { pub async fn run_server(config: &APIUpstreamProvider, mut to_return: Sender<Configuration>, upstreams_curr: Arc<UpstreamsDashMap>, upstreams_full: Arc<UpstreamsDashMap>) {
let credsfile = config.config_dir.clone() + "/acme_credentials.json";
let app_state = AppState { let app_state = AppState {
master_key: config.masterkey.clone(), master_key: config.masterkey.clone(),
cert_creds: credsfile,
certs_dir: config.certs_dir.clone(),
upstreams_file: config.upstreams_file.clone(),
config_sender: to_return.clone(), config_sender: to_return.clone(),
config_api_enabled: config.config_api_enabled.clone(), config_api_enabled: config.config_api_enabled,
current_upstreams: upstreams_curr,
full_upstreams: upstreams_full,
}; };
let app = Router::new() let app = Router::new()
// .route("/{*wildcard}", get(senderror)) // .route("/{*wildcard}", get(senderror))
// .route("/{*wildcard}", post(senderror)) // .route("/{*wildcard}", post(senderror))
@@ -54,27 +63,31 @@ pub async fn run_server(config: &APIUpstreamProvider, mut to_return: Sender<Conf
// .route("/{*wildcard}", delete(senderror)) // .route("/{*wildcard}", delete(senderror))
// .nest_service("/static", static_files) // .nest_service("/static", static_files)
.route("/jwt", post(jwt_gen)) .route("/jwt", post(jwt_gen))
.route("/acme_create", any(acme_create))
.route("/acme_order/{*domain}", any(acme_order))
.route("/.well-known/acme-challenge/{*token}", any(http01_challenge))
.route("/conf", post(conf)) .route("/conf", post(conf))
.route("/metrics", get(metrics)) .route("/metrics", get(metrics))
.route("/status", get(status))
.with_state(app_state); .with_state(app_state);
if let Some(value) = &config.tls_address { // if let Some(value) = &config.tls_address {
let cf = OpenSSLConfig::from_pem_file(config.tls_certificate.clone().unwrap(), config.tls_key_file.clone().unwrap()).unwrap(); // let cf = OpenSSLConfig::from_pem_file(config.tls_certificate.clone().unwrap(), config.tls_key_file.clone().unwrap()).unwrap();
let addr: SocketAddr = value.parse().expect("Unable to parse socket address"); // let addr: SocketAddr = value.parse().expect("Unable to parse socket address");
let tls_app = app.clone(); // let tls_app = app.clone();
tokio::spawn(async move { // tokio::spawn(async move {
if let Err(e) = axum_server::bind_openssl(addr, cf).serve(tls_app.into_make_service()).await { // if let Err(e) = axum_server::bind_openssl(addr, cf).serve(tls_app.into_make_service()).await {
eprintln!("TLS server failed: {}", e); // eprintln!("TLS server failed: {}", e);
} // }
}); // });
info!("Starting the TLS API server on: {}", value); // info!("Starting the TLS API server on: {}", value);
} // }
if let (Some(address), Some(folder)) = (&config.file_server_address, &config.file_server_folder) { if let (Some(address), Some(folder)) = (&config.file_server_address, &config.file_server_folder) {
let static_files = ServeDir::new(folder); let static_files = ServeDir::new(folder);
let static_serve: Router = Router::new().fallback_service(static_files); let static_serve: Router = Router::new().fallback_service(static_files);
let static_listen = TcpListener::bind(address).await.unwrap(); let static_listen = TcpListener::bind(address).await.unwrap();
let _ = tokio::spawn(async move { axum::serve(static_listen, static_serve).await.unwrap() }); drop(tokio::spawn(async move { axum::serve(static_listen, static_serve).await.unwrap() }));
} }
let listener = TcpListener::bind(config.address.clone()).await.unwrap(); let listener = TcpListener::bind(config.address.clone()).await.unwrap();
@@ -82,36 +95,62 @@ pub async fn run_server(config: &APIUpstreamProvider, mut to_return: Sender<Conf
axum::serve(listener, app).await.unwrap(); axum::serve(listener, app).await.unwrap();
} }
async fn conf(State(mut st): State<AppState>, Query(params): Query<HashMap<String, String>>, content: String) -> impl IntoResponse { async fn conf(State(st): State<AppState>, Query(params): Query<HashMap<String, String>>, headers: HeaderMap, content: String) -> impl IntoResponse {
if !st.config_api_enabled { if !st.config_api_enabled {
return Response::builder() return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Config API is disabled !\n")).unwrap();
.status(StatusCode::FORBIDDEN)
.body(Body::from("Config remote API is disabled !\n"))
.unwrap();
} }
// if let Some(s) = headers.get("x-api-key").and_then(|v| v.to_str().ok()).or(params.get("key").map(|s| s.as_str())) {
if let Some(s) = params.get("key") { if key_authorization(&headers, &params, &st.master_key) {
if s.to_owned() == st.master_key { let strcontent = content.as_str();
if let Some(serverlist) = crate::utils::parceyaml::load_configuration(content.as_str(), "content").await { let parsed = serde_yml::from_str::<Config>(strcontent);
st.config_sender.send(serverlist).await.unwrap(); match parsed {
return Response::builder().status(StatusCode::OK).body(Body::from("Config, conf file, updated !\n")).unwrap(); Ok(_) => {
} else { if let Some(_) = params.get("save") {
return Response::builder().status(StatusCode::BAD_GATEWAY).body(Body::from("Failed to parse config!\n")).unwrap(); drop(tokio::spawn(async move { apply_config(content.as_str(), st, true).await }));
}; } else {
drop(tokio::spawn(async move { apply_config(content.as_str(), st, false).await }));
}
// apply_config(content.as_str(), st).await;
return Response::builder().status(StatusCode::OK).body(Body::from("Accepted! Applying in background\n")).unwrap();
}
Err(err) => {
error!("Failed to parse upstreams file: {}", err);
return Response::builder().status(StatusCode::BAD_GATEWAY).body(Body::from(format!("Failed: {}\n", err))).unwrap();
}
} }
} }
Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Access Denied !\n")).unwrap() Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Access Denied !\n")).unwrap()
} }
async fn jwt_gen(State(state): State<AppState>, Json(payload): Json<InputKey>) -> (StatusCode, Json<OutToken>) { async fn apply_config(content: &str, mut st: AppState, save: bool) {
let sl = crate::utils::parceyaml::load_configuration(content, "content").await;
if let Some(serverlist) = sl.0 {
if save {
info!("Saving new upstreams to: {}", st.upstreams_file);
if let Err(err) = std::fs::write(&st.upstreams_file, content) {
error!("Error saving to: {} : {}", st.upstreams_file, err);
}
}
let _ = st.config_sender.send(serverlist).await;
}
}
async fn jwt_gen(State(state): State<AppState>, Json(payload): Json<Claims>) -> (StatusCode, Json<OutToken>) {
if payload.master_key == state.master_key { if payload.master_key == state.master_key {
let now = SystemTime::now() + Duration::from_secs(payload.valid * 60); let now = SystemTime::now() + Duration::from_secs(payload.exp * 60);
let a = now.duration_since(UNIX_EPOCH).unwrap().as_secs(); let expire = now.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
let claim = crate::utils::jwt::Claims { user: payload.owner, exp: a };
let claim = Claims {
master_key: String::new(),
owner: payload.owner,
exp: expire,
random: payload.random,
};
match encode(&Header::default(), &claim, &EncodingKey::from_secret(payload.master_key.as_ref())) { match encode(&Header::default(), &claim, &EncodingKey::from_secret(payload.master_key.as_ref())) {
Ok(t) => { Ok(t) => {
let tok = OutToken { token: t }; let tok = OutToken { token: t };
info!("Generating token: {:?}", tok); debug!("Generating token: {:?}", tok.token);
(StatusCode::CREATED, Json(tok)) (StatusCode::CREATED, Json(tok))
} }
Err(e) => { Err(e) => {
@@ -132,7 +171,6 @@ async fn jwt_gen(State(state): State<AppState>, Json(payload): Json<InputKey>) -
async fn metrics() -> impl IntoResponse { async fn metrics() -> impl IntoResponse {
let metric_families = gather(); let metric_families = gather();
let encoder = TextEncoder::new(); let encoder = TextEncoder::new();
let mut buffer = Vec::new(); let mut buffer = Vec::new();
if let Err(e) = encoder.encode(&metric_families, &mut buffer) { if let Err(e) = encoder.encode(&metric_families, &mut buffer) {
// encoding error fallback // encoding error fallback
@@ -141,7 +179,6 @@ async fn metrics() -> impl IntoResponse {
.body(Body::from(format!("Failed to encode metrics: {}", e))) .body(Body::from(format!("Failed to encode metrics: {}", e)))
.unwrap(); .unwrap();
} }
Response::builder() Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
.header("Content-Type", encoder.format_type()) .header("Content-Type", encoder.format_type())
@@ -149,7 +186,119 @@ async fn metrics() -> impl IntoResponse {
.unwrap() .unwrap()
} }
// #[allow(dead_code)] #[allow(clippy::needless_return)]
// async fn senderror() -> impl IntoResponse { async fn status(State(st): State<AppState>, Query(params): Query<HashMap<String, String>>) -> impl IntoResponse {
// Response::builder().status(StatusCode::BAD_GATEWAY).body(Body::from("No live upstream found!\n")).unwrap() if params.contains_key("live") {
// } let r = upstreams_liveness_json(&st.full_upstreams, &st.current_upstreams);
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.body(Body::from(format!("{}", r)))
.unwrap();
}
if params.contains_key("all") {
let resp = upstreams_to_json(&st.current_upstreams);
match resp {
Ok(j) => {
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.body(Body::from(j))
.unwrap()
}
Err(e) => {
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("Failed to get status: {}", e)))
.unwrap();
}
}
}
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from("Parameter mismatch"))
.unwrap()
}
#[allow(clippy::needless_return)]
async fn acme_create(State(state): State<AppState>, Query(params): Query<HashMap<String, String>>, headers: HeaderMap) -> impl IntoResponse {
if !key_authorization(&headers, &params, &state.master_key) {
return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Access Denied !\n")).unwrap();
}
match account::load_or_create(state.cert_creds.as_str()).await {
Ok(txt) => {
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/plain")
.body(Body::from(txt))
.unwrap()
}
Err(e) => {
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("Failed to create account: {}", e)))
.unwrap()
}
};
}
#[allow(clippy::needless_return)]
async fn acme_order(
State(state): State<AppState>,
axum::extract::Path(domain): axum::extract::Path<String>,
Query(params): Query<HashMap<String, String>>,
headers: HeaderMap,
) -> impl IntoResponse {
if !key_authorization(&headers, &params, &state.master_key) {
return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Access Denied !\n")).unwrap();
}
let domain_clean = domain.trim_matches('/');
match order::order(domain_clean, state.cert_creds.as_str(), state.certs_dir).await {
Ok(txt) => {
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/plain")
.body(Body::from(txt))
.unwrap()
}
Err(e) => {
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("Failed to order a certificate: {}", e)))
.unwrap()
}
};
}
pub async fn http01_challenge(axum::extract::Path(token): axum::extract::Path<String>) -> impl IntoResponse {
if let Ok(challenges) = CHALLENGES.read() {
// for k in challenges.iter() {
// println!(" ==> {} : {}", k.0, k.1);
// }
if let Some(key_authorization) = challenges.get(&token) {
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/plain")
.body(Body::from(key_authorization.clone()))
.unwrap();
}
}
Response::builder()
.status(StatusCode::NOT_FOUND)
.header("Content-Type", "text/plain")
.body(Body::from("Not found"))
.unwrap()
}
fn key_authorization(headers: &HeaderMap, params: &HashMap<String, String>, masterkey: &str) -> bool {
if let Some(s) = headers.get("x-api-key").and_then(|v| v.to_str().ok()).or(params.get("key").map(|s| s.as_str())) {
if s.as_bytes().ct_eq(masterkey.as_bytes()).into() {
return true;
}
}
false
}
// -- ⚝ by Dave -- in NeoVim ⚝ --