24 Commits

Author SHA1 Message Date
Ara Sadoyan
2ce290abcf README update 2026-05-15 18:32:58 +02:00
Ara Sadoyan
2380f83d8e Added log to file option. 2026-05-15 16:00:57 +02:00
Ara Sadoyan
3965a1de93 README typo 2026-05-14 10:54:27 +02:00
Ara Sadoyan
7bc8294c22 README typo 2026-05-14 10:53:42 +02:00
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
22 changed files with 882 additions and 786 deletions

1
.gitignore vendored
View File

@@ -21,3 +21,4 @@ crashlytics.properties
crashlytics-build.properties crashlytics-build.properties
/target /target
/z_shpo /z_shpo
Makefile

723
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@ panic = "abort"
strip = true strip = true
[dependencies] [dependencies]
tokio = { version = "1.52.1", features = ["full"] } tokio = { version = "1.52.3", features = ["full"] }
pingora = { version = "0.8.0", features = ["lb", "openssl"] } # openssl, rustls, boringssl pingora = { version = "0.8.0", features = ["lb", "openssl"] } # openssl, rustls, boringssl
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
dashmap = "7.0.0-rc2" dashmap = "7.0.0-rc2"
@@ -20,17 +20,16 @@ pingora-proxy = "0.8.0"
pingora-http = "0.8.0" pingora-http = "0.8.0"
pingora-limits = "0.8.0" pingora-limits = "0.8.0"
async-trait = "0.1.89" async-trait = "0.1.89"
env_logger = "0.11.10"
log = "0.4.29" log = "0.4.29"
futures = "0.3.32" futures = "0.3.32"
notify = "9.0.0-rc.3" notify = "9.0.0-rc.4"
axum = { version = "0.8.9" } axum = { version = "0.8.9" }
reqwest = { version = "0.13.2", features = ["json", "stream", "blocking"] } reqwest = { version = "0.13.3", features = ["json", "stream", "blocking"] }
serde_yml = "0.0.12" serde_yml = "0.0.12"
rand = "0.10.1" rand = "0.10.1"
base64 = "0.22.1" base64 = "0.22.1"
jsonwebtoken = { version = "10.3.0", default-features = false, features = ["use_pem", "rust_crypto"] } jsonwebtoken = { version = "10.4.0", default-features = false, features = ["use_pem", "rust_crypto"] }
tonic = "0.14.5" tonic = "0.14.6"
sha2 = { version = "0.11.0-rc.5", default-features = false } sha2 = { version = "0.11.0-rc.5", default-features = false }
base16ct = { version = "1.0.0", features = ["alloc"] } base16ct = { version = "1.0.0", features = ["alloc"] }
urlencoding = "2.1.3" urlencoding = "2.1.3"
@@ -39,12 +38,13 @@ mimalloc = { version = "0.1.50", default-features = false }
prometheus = "0.14.0" prometheus = "0.14.0"
x509-parser = "0.18.1" x509-parser = "0.18.1"
rustls-pemfile = "2.2.0" rustls-pemfile = "2.2.0"
tower-http = { version = "0.6.8", features = ["fs"] } tower-http = { version = "0.6.10", features = ["fs"] }
privdrop = "0.5.6" privdrop = "0.5.6"
ctrlc = "3.5.2" ctrlc = "3.5.2"
serde_json = "1.0.149" serde_json = "1.0.149"
subtle = "2.6.1" subtle = "2.6.1"
moka = { version = "0.12.1", features = ["sync"] } moka = { version = "0.12.15", features = ["sync"] }
ahash = "0.8.12" ahash = "0.8.12"
instant-acme = "0.8.5" instant-acme = "0.8.5"
rcgen = "0.14.7" rcgen = "0.14.7"
log4rs = "1.4.0"

356
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**
@@ -19,118 +19,93 @@ 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. - **Autoload of certificates** — Automatically loads new/changed certificates from a folder, without a restart.
- **Automatic loading of certificates** — Automatically reads and loads certificates from a folder, without a restart. - **Lets Encrypt Certificates** — 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. - **Auto WebSocket Support:** WS connection upgrades are handled automatically.
- 🔮 **Automatic GRPC Support:** Zero config, Requires `ssl` to proxy, gRPC handled seamlessly. - **Auto 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 |
| **log_file** | /full/path/to/aralez.log | Optional, the location of log file. If thi entry does not exist logs will be emitted to stdout. |
| **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-x86_64-compat-musl.gz` | Static Linux x86_64 binary, compatible with old pre Haswell CPUs | | `aralez-x86_64-compat-musl.gz` | Static Linux x86_64 binary, compatible with old pre Haswell CPUs |
| `aralez-x86_64-compat-glibc.gz` | Dynamic Linux x86_64 binary, compatible with old pre Haswell CPUs | | `aralez-x86_64-compat-glibc.gz` | Dynamic Linux x86_64 binary, compatible with old pre Haswell CPUs |
| `aralez-aarch64-musl.gz` | Static Linux ARM64 binary, without any system dependency | | `aralez-aarch64-musl.gz` | Static Linux ARM64 binary, without any system dependency |
| `aralez-aarch64-glibc.gz` | Dynamic Linux ARM64 binary, with minimal system dependencies | | `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) | | `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**
@@ -142,51 +117,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"
@@ -200,9 +174,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:
"/": "/":
@@ -219,6 +190,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:
@@ -228,6 +202,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:**
@@ -243,6 +222,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.
@@ -254,27 +234,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`
@@ -285,7 +268,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.
@@ -339,13 +322,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.
@@ -354,37 +337,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**
@@ -413,7 +401,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
@@ -422,16 +410,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] |
@@ -459,8 +447,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
@@ -478,16 +466,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] |
@@ -515,8 +503,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
@@ -527,7 +515,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.
@@ -543,3 +531,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

View File

@@ -25,7 +25,7 @@ pub async fn order(domain: &str, credsfile: &str, certs_dir: String) -> Result<S
let crt = certs_dir.clone() + "/" + domain + ".crt"; let crt = certs_dir.clone() + "/" + domain + ".crt";
let key = certs_dir.clone() + "/" + domain + ".key"; let key = certs_dir.clone() + "/" + domain + ".key";
if let None = DOMAINS.get(domain) { if DOMAINS.get(domain).is_none() {
DOMAINS.insert(domain.to_string(), true); DOMAINS.insert(domain.to_string(), true);
let mut newlist: Vec<String> = Vec::new(); let mut newlist: Vec<String> = Vec::new();
for item in DOMAINS.iter() { for item in DOMAINS.iter() {
@@ -40,15 +40,12 @@ pub async fn order(domain: &str, credsfile: &str, certs_dir: String) -> Result<S
} }
} }
let _ = match cert_expiry(crt.as_str()) { if let Ok(expiry) = cert_expiry(crt.as_str()) {
Ok(expiry) => { let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs();
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(); if expiry > now + 30 * 24 * 3600 {
if expiry > now + 30 * 24 * 3600 { // println!("Fresh certificate exists. Not renewing !");
// println!("Fresh certificate exists. Not renewing !"); return Ok("Fresh certificate exists. Not renewing ! \n".to_string());
return Ok("Fresh certificate exists. Not renewing ! \n".to_string());
}
} }
Err(_) => {}
}; };
let account = get_account(credsfile).await?; let account = get_account(credsfile).await?;
@@ -73,7 +70,7 @@ pub async fn order(domain: &str, credsfile: &str, certs_dir: String) -> Result<S
let private_key = KeyPair::generate()?; let private_key = KeyPair::generate()?;
let signing_request = params.serialize_request(&private_key)?; let signing_request = params.serialize_request(&private_key)?;
let csr_der = signing_request.der(); let csr_der = signing_request.der();
order.finalize_csr(&csr_der).await?; order.finalize_csr(csr_der).await?;
// poll for certificate // poll for certificate
let cert_chain_pem = order.poll_certificate(&RetryPolicy::default()).await?; let cert_chain_pem = order.poll_certificate(&RetryPolicy::default()).await?;

View File

@@ -16,17 +16,17 @@ const CIPHERS: CipherSuite = CipherSuite {
#[derive(Debug)] #[derive(Debug)]
pub enum TlsGrade { pub enum TlsGrade {
HIGH, High,
MEDIUM, Medium,
LEGACY, Legacy,
} }
impl TlsGrade { impl TlsGrade {
pub fn from_str(s: &str) -> Option<Self> { pub fn from_str(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() { match s.to_ascii_lowercase().as_str() {
"high" => Some(TlsGrade::HIGH), "high" => Some(TlsGrade::High),
"medium" => Some(TlsGrade::MEDIUM), "medium" => Some(TlsGrade::Medium),
"unsafe" => Some(TlsGrade::LEGACY), "unsafe" => Some(TlsGrade::Legacy),
_ => None, _ => None,
} }
} }
@@ -41,22 +41,22 @@ pub fn prefer_h2<'a>(_ssl: &mut SslRef, alpn_in: &'a [u8]) -> Result<&'a [u8], A
pub fn set_tsl_grade(tls_settings: &mut TlsSettings, grade: &str) { pub fn set_tsl_grade(tls_settings: &mut TlsSettings, grade: &str) {
let config_grade = TlsGrade::from_str(grade); let config_grade = TlsGrade::from_str(grade);
match config_grade { match config_grade {
Some(TlsGrade::HIGH) => { Some(TlsGrade::High) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1_2)); 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_max_proto_version(Some(SslVersion::TLS1_3));
let _ = tls_settings.set_cipher_list(CIPHERS.high); let _ = tls_settings.set_cipher_list(CIPHERS.high);
// let _ = tls_settings.set_ciphersuites(CIPHERS.high); // let _ = tls_settings.set_ciphersuites(CIPHERS.high);
let _ = tls_settings.set_cipher_list(CIPHERS.high); let _ = tls_settings.set_cipher_list(CIPHERS.high);
info!("TLS grade: {:?}, => HIGH", tls_settings.options()); info!("TLS grade: {:?}, => High", tls_settings.options());
} }
Some(TlsGrade::MEDIUM) => { Some(TlsGrade::Medium) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1)); let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1));
let _ = tls_settings.set_cipher_list(CIPHERS.medium); let _ = tls_settings.set_cipher_list(CIPHERS.medium);
// let _ = tls_settings.set_ciphersuites(CIPHERS.medium); // let _ = tls_settings.set_ciphersuites(CIPHERS.medium);
let _ = tls_settings.set_cipher_list(CIPHERS.medium); let _ = tls_settings.set_cipher_list(CIPHERS.medium);
info!("TLS grade: {:?}, => MEDIUM", tls_settings.options()); info!("TLS grade: {:?}, => Medium", tls_settings.options());
} }
Some(TlsGrade::LEGACY) => { Some(TlsGrade::Legacy) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::SSL3)); let _ = tls_settings.set_min_proto_version(Some(SslVersion::SSL3));
let _ = tls_settings.set_cipher_list(CIPHERS.legacy); let _ = tls_settings.set_cipher_list(CIPHERS.legacy);
// let _ = tls_settings.set_ciphersuites(CIPHERS.legacy); // let _ = tls_settings.set_ciphersuites(CIPHERS.legacy);
@@ -64,12 +64,12 @@ pub fn set_tsl_grade(tls_settings: &mut TlsSettings, grade: &str) {
warn!("TLS grade: {:?}, => UNSAFE", tls_settings.options()); warn!("TLS grade: {:?}, => UNSAFE", tls_settings.options());
} }
None => { None => {
// Defaults to MEDIUM // Defaults to Medium
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1)); let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1));
let _ = tls_settings.set_cipher_list(CIPHERS.medium); let _ = tls_settings.set_cipher_list(CIPHERS.medium);
// let _ = tls_settings.set_ciphersuites(CIPHERS.medium); // let _ = tls_settings.set_ciphersuites(CIPHERS.medium);
let _ = tls_settings.set_cipher_list(CIPHERS.medium); let _ = tls_settings.set_cipher_list(CIPHERS.medium);
warn!("TLS grade is not detected defaulting top MEDIUM"); warn!("TLS grade is not detected defaulting top Medium");
} }
} }
} }

View File

@@ -60,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(),
@@ -93,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");

View File

@@ -153,9 +153,9 @@ impl AuthValidator for ForwardAuth<'_> {
impl AuthValidator for BasicAuth<'_> { impl AuthValidator for BasicAuth<'_> {
async fn validate(&self, session: &mut 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(h) = header.to_str().ok() { if let Ok(h) = header.to_str() {
if let Some((_, val)) = h.split_once(' ') { if let Some((_, val)) = h.split_once(' ') {
if let Some(decoded) = STANDARD.decode(val).ok() { if let Ok(decoded) = STANDARD.decode(val) {
if decoded.as_slice().ct_eq(self.0.as_bytes()).into() { if decoded.as_slice().ct_eq(self.0.as_bytes()).into() {
return true; return true;
} }
@@ -171,7 +171,7 @@ impl AuthValidator for BasicAuth<'_> {
impl AuthValidator for ApiKeyAuth<'_> { impl AuthValidator for ApiKeyAuth<'_> {
async fn validate(&self, session: &mut 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") {
if let Some(h) = header.to_str().ok() { if let Ok(h) = header.to_str() {
return h.as_bytes().ct_eq(self.0.as_bytes()).into(); return h.as_bytes().ct_eq(self.0.as_bytes()).into();
} }
} }
@@ -227,6 +227,7 @@ pub fn get_query_param(session: &mut Session, key: &str) -> Option<String> {
params.get(key).and_then(|v| decode(v).ok()).map(|s| s.to_string()) params.get(key).and_then(|v| decode(v).ok()).map(|s| s.to_string())
} }
#[allow(clippy::needless_return)]
fn split_host_port(addr: &str, tls: bool) -> Option<(&str, u16, bool, &str)> { fn split_host_port(addr: &str, tls: bool) -> Option<(&str, u16, bool, &str)> {
match addr.split_once(':') { match addr.split_once(':') {
Some((h, p)) => match p.parse::<u16>() { Some((h, p)) => match p.parse::<u16>() {

View File

@@ -12,6 +12,7 @@ pub struct APIUpstreamProvider {
pub masterkey: String, pub masterkey: String,
pub certs_dir: String, pub certs_dir: String,
pub config_dir: String, pub config_dir: String,
pub upstreams_file: String,
// pub tls_address: Option<String>, // pub tls_address: Option<String>,
// pub tls_certificate: Option<String>, // pub tls_certificate: Option<String>,
// pub tls_key_file: Option<String>, // pub tls_key_file: Option<String>,

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.0; if let Some(snd) = snd {
match snd { toreturn.send(snd).await.unwrap();
Some(snd) => {
toreturn.send(snd).await.unwrap();
}
None => {}
}
} }
} }
} }

View File

@@ -52,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 {
@@ -75,7 +75,7 @@ async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str, client: &Clie
}; };
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
@@ -109,12 +109,12 @@ 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),
} }
} }
@@ -128,12 +128,8 @@ pub async fn ping_grpc(addr: &str) -> bool {
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

@@ -23,11 +23,8 @@ pub async fn for_consul(url: String, token: Option<String>, conf: &GlobalService
let upstreams: DashMap<Arc<str>, (Vec<Arc<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 prt = subsets.tagged_addresses.get("lan_ipv4").unwrap().port.clone();
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 redirect_link = conf.redirect_to.as_ref().map(|www| Arc::from(www.as_str()));
let to_add = Arc::from(InnerMap { let to_add = Arc::from(InnerMap {
address: Arc::from(&*addr), address: Arc::from(&*addr),
port: prt, port: prt,
@@ -41,7 +38,7 @@ pub async fn for_consul(url: String, token: Option<String>, conf: &GlobalService
}); });
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)
} }
@@ -66,7 +63,7 @@ pub async fn for_kuber(url: &str, token: &str, conf: &GlobalServiceMapping) -> O
// let redirect_link = conf.redirect_to.as_ref().map(|www| Arc::from(www.as_str())); // let redirect_link = conf.redirect_to.as_ref().map(|www| Arc::from(www.as_str()));
let to_add = Arc::from(InnerMap { let to_add = Arc::from(InnerMap {
address: Arc::from(addr.ip.clone()), address: Arc::from(addr.ip.clone()),
port: port.port.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),
@@ -78,7 +75,7 @@ pub async fn for_kuber(url: &str, token: &str, conf: &GlobalServiceMapping) -> O
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

@@ -52,12 +52,13 @@ pub struct ConsulTaggedAddress {
#[serde(rename = "Port")] #[serde(rename = "Port")]
pub port: u16, pub port: u16,
} }
#[allow(clippy::type_complexity)]
pub fn list_to_upstreams(lt: Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>>, upstreams: &UpstreamsDashMap, i: &GlobalServiceMapping) { 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(Arc::from(k.to_owned()), v); upstr.value().insert(k.to_owned(), v);
} }
} }
None => { None => {
@@ -134,7 +135,7 @@ impl ServiceDiscovery for KubernetesDiscovery {
} }
let url = format!("https://{}/api/v1/namespaces/{}/endpoints/{}", server, namespace, service.hostname); let url = format!("https://{}/api/v1/namespaces/{}/endpoints/{}", server, namespace, service.hostname);
// let url = format!("https://{}/api/v1/namespaces/{}/endpoints?labelSelector=app", server, namespace); // let url = format!("https://{}/api/v1/namespaces/{}/endpoints?labelSelector=app", server, namespace);
let list = httpclient::for_kuber(&*url, &*token, &service).await; let list = httpclient::for_kuber(&url, &token, &service).await;
// println!("{:?}", list); // println!("{:?}", list);
list_to_upstreams(list, &upstreams, &service); list_to_upstreams(list, &upstreams, &service);
} }
@@ -209,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(),
@@ -219,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,8 +1,9 @@
use pingora_http::Method; use pingora_http::Method;
use pingora_http::StatusCode; 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::Arc;
use std::sync::LazyLock;
use std::time::Duration; use std::time::Duration;
pub struct MetricTypes { pub struct MetricTypes {
@@ -13,22 +14,13 @@ pub struct MetricTypes {
pub version: Version, pub version: Version,
} }
use std::sync::LazyLock; pub static ACTIVE_SESSIONS: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_active_sessions", "Current number of active sessions").unwrap());
pub static REQUEST_COUNT: LazyLock<IntCounter> = LazyLock::new(|| register_int_counter!("aralez_requests_total", "Total number of requests handled by Aralez").unwrap()); pub static REQUEST_COUNT: LazyLock<IntCounter> = LazyLock::new(|| register_int_counter!("aralez_requests_total", "Total number of requests handled by Aralez").unwrap());
pub static RESPONSE_CODES: LazyLock<IntCounterVec> = pub static RESPONSE_CODES: LazyLock<IntCounterVec> =
LazyLock::new(|| register_int_counter_vec!("aralez_responses_total", "Responses grouped by status code", &["status"]).unwrap()); LazyLock::new(|| register_int_counter_vec!("aralez_responses_total", "Responses grouped by status code", &["status"]).unwrap());
pub static REQUEST_LATENCY: LazyLock<Histogram> = LazyLock::new(|| {
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 RESPONSE_LATENCY: LazyLock<Histogram> = LazyLock::new(|| { pub static RESPONSE_LATENCY: LazyLock<Histogram> = LazyLock::new(|| {
register_histogram!( register_histogram!(
"aralez_response_latency_seconds", "aralez_response_latency_seconds",
@@ -49,19 +41,16 @@ pub static REQUESTS_BY_VERSION: LazyLock<IntCounterVec> =
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.unwrap_or(StatusCode::GONE).as_str()]).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(); 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

@@ -3,14 +3,20 @@ use crate::utils::state::{is_first_run, mark_not_first_run};
use crate::utils::structs::*; use crate::utils::structs::*;
use crate::utils::tools::{clone_dashmap, clone_dashmap_into, print_upstreams}; use crate::utils::tools::{clone_dashmap, clone_dashmap_into, print_upstreams};
use dashmap::DashMap; use dashmap::DashMap;
use log::LevelFilter;
use log::{error, info, warn}; use log::{error, info, warn};
use log4rs::{
append::{console::ConsoleAppender, file::FileAppender},
config::{Appender, Config as Log4rsConfig, Root},
encode::pattern::PatternEncoder,
};
use std::collections::HashMap; use std::collections::HashMap;
use std::fs;
use std::path::Path; use std::path::Path;
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
use std::{env, fs};
pub static DOMAINS: LazyLock<DashMap<String, bool>> = LazyLock::new(|| DashMap::new()); pub static DOMAINS: LazyLock<DashMap<String, bool>> = LazyLock::new(DashMap::new);
pub async fn load_configuration(d: &str, kind: &str) -> (Option<Configuration>, String) { pub async fn load_configuration(d: &str, kind: &str) -> (Option<Configuration>, String) {
let mut conf_files = Vec::new(); let mut conf_files = Vec::new();
@@ -21,7 +27,7 @@ pub async fn load_configuration(d: &str, kind: &str) -> (Option<Configuration>,
let mut autocfg = Path::new(d).parent().unwrap().to_path_buf(); let mut autocfg = Path::new(d).parent().unwrap().to_path_buf();
autocfg.push("autoconfigs"); autocfg.push("autoconfigs");
if !fs::metadata(autocfg.clone()).is_ok() { if fs::metadata(autocfg.clone()).is_err() {
fs::create_dir_all(autocfg.clone()).ok(); fs::create_dir_all(autocfg.clone()).ok();
} }
autocfg.push("domains.json"); autocfg.push("domains.json");
@@ -228,9 +234,9 @@ 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_yml::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_yml::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.log_file);
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 {
reply.insert(k.to_string(), v.to_string()); reply.insert(k.to_string(), v.to_string());
@@ -240,14 +246,6 @@ 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(':') {
cfo.proxy_port_tls = Some(port_str.to_string()); cfo.proxy_port_tls = Some(port_str.to_string());
@@ -289,7 +287,8 @@ fn parce_tls_grades(what: Option<String>) -> Option<String> {
} }
} }
fn log_builder(conf: &AppConfig) { /*
fn log_builder1(conf: &AppConfig) {
let log_level = conf.log_level.clone(); let log_level = conf.log_level.clone();
unsafe { unsafe {
match log_level.as_str() { match log_level.as_str() {
@@ -307,6 +306,7 @@ 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<(String, 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 {
@@ -317,3 +317,34 @@ pub fn build_headers(path_config: &Option<Vec<String>>, _config: &Configuration,
} }
} }
} }
fn log_builder(conf: &AppConfig, location: &Option<String>) {
let log_level = match conf.log_level.as_str() {
"info" => LevelFilter::Info,
"error" => LevelFilter::Error,
"warn" => LevelFilter::Warn,
"debug" => LevelFilter::Debug,
"trace" => LevelFilter::Trace,
"off" => LevelFilter::Off,
_ => {
println!("Error reading log level, defaulting to: INFO");
LevelFilter::Info
}
};
let pattern = "{d(%Y-%m-%d %H:%M:%S)} {l} {t} - {m}{n}";
if let Some(location) = location {
let file = FileAppender::builder().encoder(Box::new(PatternEncoder::new(pattern))).build(location).unwrap();
let config = Log4rsConfig::builder()
.appender(Appender::builder().build("file", Box::new(file)))
.build(Root::builder().appender("file").build(log_level))
.unwrap();
log4rs::init_config(config).unwrap();
} else {
let stdout = ConsoleAppender::builder().encoder(Box::new(PatternEncoder::new(pattern))).build();
let config = Log4rsConfig::builder()
.appender(Appender::builder().build("stdout", Box::new(stdout)))
.build(Root::builder().appender("stdout").build(log_level))
.unwrap();
log4rs::init_config(config).unwrap();
}
}

View File

@@ -125,15 +125,16 @@ pub struct AppConfig {
pub file_server_folder: Option<String>, pub file_server_folder: Option<String>,
pub runuser: Option<String>, pub runuser: Option<String>,
pub rungroup: Option<String>, pub rungroup: Option<String>,
pub log_file: Option<String>,
} }
#[derive(Debug, Default, Clone, PartialEq, Eq)] #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct InnerAuth { pub struct InnerAuth {
pub auth_type: Arc<str>, pub auth_type: Arc<str>,
pub auth_cred: Arc<str>, pub auth_cred: Arc<str>,
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct InnerMap { pub struct InnerMap {
pub address: Arc<str>, pub address: Arc<str>,
pub port: u16, pub port: u16,

View File

@@ -20,17 +20,15 @@ use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use std::{fs, process, thread, time}; use std::{fs, process, thread, time};
#[allow(dead_code)]
pub fn print_upstreams(upstreams: &UpstreamsDashMap) { pub fn print_upstreams(upstreams: &UpstreamsDashMap) {
let mut out = String::new();
for host_entry in upstreams.iter() { for host_entry in upstreams.iter() {
let hostname = host_entry.key(); writeln!(out, "Hostname: {}", host_entry.key()).unwrap();
println!("Hostname: {}", hostname);
for path_entry in host_entry.value().iter() { for path_entry in host_entry.value().iter() {
let path = path_entry.key(); writeln!(out, " Path: {}", path_entry.key()).unwrap();
println!(" Path: {}", path);
for f in path_entry.value().0.clone() { for f in path_entry.value().0.clone() {
println!( writeln!(
out,
" IP: {}, Port: {}, SSL: {}, H2: {}, To HTTPS: {}, Rate Limit: {}", " IP: {}, Port: {}, SSL: {}, H2: {}, To HTTPS: {}, Rate Limit: {}",
f.address, f.address,
f.port, f.port,
@@ -38,12 +36,13 @@ pub fn print_upstreams(upstreams: &UpstreamsDashMap) {
f.is_http2, f.is_http2,
f.to_https, f.to_https,
f.rate_limit.unwrap_or(0) f.rate_limit.unwrap_or(0)
); )
.unwrap();
} }
} }
} }
info!("\n{}", out.trim_end());
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn typeoff<T>(_: T) { pub fn typeoff<T>(_: T) {
let to = type_name::<T>(); let to = type_name::<T>();
@@ -101,46 +100,31 @@ 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() { if vec1.len() != vec2.len() {
return false; return false;
} }
for item in vec1.iter() { let set1: HashSet<_> = vec1.iter().collect();
let count1 = vec1.iter().filter(|&x| x == item).count(); let set2: HashSet<_> = vec2.iter().collect();
let count2 = vec2.iter().filter(|&x| x == item).count(); if set1 != set2 {
if count1 != count2 { return false;
return false;
}
} }
// let set1: HashSet<_> = vec1.iter().collect();
// let set2: HashSet<_> = vec2.iter().collect();
// if set1 != set2 {
// return false;
// }
} }
} }
true true
@@ -150,7 +134,7 @@ pub fn merge_headers(target: &DashMap<Arc<str>, Vec<(String, Arc<str>)>>, source
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);
} }
} }
@@ -198,8 +182,7 @@ pub fn clone_idmap_into(original: &UpstreamsDashMap, cloned: &UpstreamsIdMap) {
authorization: None, authorization: None,
}; };
cloned.insert(id, Arc::from(to_add)); cloned.insert(id, Arc::from(to_add));
cloned.insert(hh, Arc::from(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);
} }
@@ -268,14 +251,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 port_is_available(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 port_is_available(tls_addr.clone()) { if TcpListener::bind(&tls_addr).is_err() {
break; break;
} }
} }
@@ -287,24 +270,14 @@ pub fn drop_priv(user: String, group: String, http_addr: String, tls_addr: Optio
} }
} }
fn port_is_available(addr: String) -> bool {
match TcpListener::bind(addr) {
Ok(_) => false,
Err(_) => true,
}
}
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 => {}
} }
} }
@@ -397,7 +370,7 @@ pub fn prepend(prefix: &str, val: &Option<Arc<str>>, uri: &str, port: &str) -> O
let mut buf = String::with_capacity(32); let mut buf = String::with_capacity(32);
buf.push_str(prefix); buf.push_str(prefix);
buf.push_str(s); buf.push_str(s);
buf.push_str(":"); buf.push(':');
buf.push_str(port); buf.push_str(port);
buf.push_str(uri); buf.push_str(uri);
buf buf

View File

@@ -32,19 +32,20 @@ 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);
@@ -57,7 +58,8 @@ impl BackgroundService for LB {
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,
upstreams_file: self.config.upstreams_conf.clone(),
// certs_dir: self.config.proxy_certificates.clone().unwrap_or_else(|| "/tmp".to_string()), // certs_dir: self.config.proxy_certificates.clone().unwrap_or_else(|| "/tmp".to_string()),
config_dir: confdir.clone(), config_dir: confdir.clone(),
certs_dir: certdir.clone(), certs_dir: certdir.clone(),
@@ -71,14 +73,16 @@ impl BackgroundService for LB {
}; };
// let crtdir = api_load.certs_dir.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 {
let _ = tokio::spawn(async move { refresh_order(certdir, confdir).await }); 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! {
@@ -86,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.to_https = ss.extraparams.to_https; new.sticky_sessions = ss.extraparams.sticky_sessions;
new.sticky_sessions = ss.extraparams.sticky_sessions; 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

@@ -19,9 +19,6 @@ pub trait GetHost {
} }
#[async_trait] #[async_trait]
impl GetHost for LB { impl GetHost for LB {
// fn get_upstreams(&self) -> Arc<UpstreamsDashMap> {
// self.ump_full.clone()
// }
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<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) {

View File

@@ -12,10 +12,8 @@ use pingora::prelude::*;
use pingora::ErrorSource::Upstream; use pingora::ErrorSource::Upstream;
use pingora_core::listeners::ALPN; use pingora_core::listeners::ALPN;
use pingora_core::prelude::HttpPeer; use pingora_core::prelude::HttpPeer;
// use pingora_core::protocols::TcpKeepalive;
use pingora_limits::rate::Rate; use pingora_limits::rate::Rate;
use pingora_proxy::{ProxyHttp, Session}; use pingora_proxy::{ProxyHttp, Session};
// use prometheus::{register_int_counter, IntCounter};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::cell::RefCell; use std::cell::RefCell;
use std::fmt::Write; use std::fmt::Write;
@@ -23,11 +21,10 @@ 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);
// static REVERSE_STORE: Lazy<DashMap<String, String>> = Lazy::new(|| DashMap::new());
static REVERSE_STORE: LazyLock<DashMap<String, String>> = LazyLock::new(|| DashMap::new());
thread_local! {static IP_BUFFER: RefCell<String> = RefCell::new(String::with_capacity(50));} 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 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 {
@@ -43,7 +40,6 @@ pub struct LB {
pub struct Context { pub struct Context {
backend_id: Option<String>, backend_id: Option<String>,
sticky_sessions: bool, sticky_sessions: bool,
// redirect_to: Option<String>,
start_time: Instant, start_time: Instant,
hostname: Option<Arc<str>>, hostname: Option<Arc<str>>,
upstream_peer: Option<Arc<InnerMap>>, upstream_peer: Option<Arc<InnerMap>>,
@@ -58,7 +54,6 @@ impl ProxyHttp for LB {
Context { Context {
backend_id: None, backend_id: None,
sticky_sessions: false, sticky_sessions: false,
// redirect_to: None,
start_time: Instant::now(), start_time: Instant::now(),
hostname: None, hostname: None,
upstream_peer: None, upstream_peer: None,
@@ -67,6 +62,7 @@ impl ProxyHttp for LB {
} }
} }
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> {
ACTIVE_SESSIONS.inc();
let hostname = return_header_host_from_upstream(session, &self.ump_upst); let hostname = return_header_host_from_upstream(session, &self.ump_upst);
_ctx.hostname = hostname; _ctx.hostname = hostname;
let mut backend_id = None; let mut backend_id = None;
@@ -132,8 +128,8 @@ impl ProxyHttp for LB {
s.push_str("https://"); s.push_str("https://");
s.push_str(host); s.push_str(host);
if port != "443" { if port != "443" {
s.push_str(":"); s.push(':');
s.push_str(&port); s.push_str(port);
} }
s.push_str(uri); s.push_str(uri);
let mut resp = ResponseHeader::build(StatusCode::MOVED_PERMANENTLY, None)?; let mut resp = ResponseHeader::build(StatusCode::MOVED_PERMANENTLY, None)?;
@@ -165,21 +161,6 @@ impl ProxyHttp for LB {
peer.options.verify_cert = false; peer.options.verify_cert = false;
peer.options.verify_hostname = false; peer.options.verify_hostname = false;
} }
/*
Experimental optionsv
The following TCP optimizations were tested but caused performance degrade under heavy load:
peer.options.tcp_keepalive = Some(TcpKeepalive {
idle: Duration::from_secs(60),
interval: Duration::from_secs(10),
count: 5,
user_timeout: Duration::from_secs(30),
});
peer.options.idle_timeout = Some(Duration::from_secs(300));
peer.options.tcp_recv_buf = Some(128 * 1024);
End of experimental options
*/
if ctx.extraparams.sticky_sessions { if ctx.extraparams.sticky_sessions {
let mut s = String::with_capacity(64); let mut s = String::with_capacity(64);
write!( write!(
@@ -229,10 +210,6 @@ 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_deref() {
// upstream_request.insert_header("Host", hostname)?;
// }
if let Some(client_ip) = session.client_addr() { if let Some(client_ip) = session.client_addr() {
IP_BUFFER.with(|buffer| { IP_BUFFER.with(|buffer| {
let mut buf = buffer.borrow_mut(); let mut buf = buffer.borrow_mut();
@@ -278,7 +255,7 @@ impl ProxyHttp for LB {
let mut buf = String::with_capacity(80); let mut buf = String::with_capacity(80);
buf.push_str("backend_id="); buf.push_str("backend_id=");
buf.push_str(&tt); buf.push_str(&tt);
buf.push_str("; Path=/; Max-Age=600; HttpOnly; SameSite=Lax"); buf.push_str("; Path=/; Max-Age=86400; HttpOnly; SameSite=Lax");
let _ = _upstream_response.insert_header("set-cookie", buf.as_str()); let _ = _upstream_response.insert_header("set-cookie", buf.as_str());
} }
} }
@@ -288,9 +265,6 @@ impl ProxyHttp for LB {
_upstream_response.append_header(k.clone(), v.as_ref())?; _upstream_response.append_header(k.clone(), v.as_ref())?;
} }
} }
// session.set_keepalive(Some(300));
// println!("session.get_keepalive: {:?}", session.get_keepalive());
Ok(()) Ok(())
} }
@@ -302,10 +276,10 @@ impl ProxyHttp for LB {
code: session.response_written().map(|resp| resp.status), 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.clone().unwrap_or(Arc::from("localhost")), upstream: ctx.hostname.take().unwrap_or_else(|| LOCALHOST.clone()),
upstream: ctx.hostname.take().unwrap_or_else(|| Arc::from("localhost")),
}; };
calc_metrics(m); calc_metrics(m);
ACTIVE_SESSIONS.dec();
} }
} }
@@ -316,5 +290,6 @@ fn return_header_host_from_upstream(session: &Session, ump_upst: &UpstreamsDashM
let h = session.req_header().headers.get("host")?.to_str().ok()?; let h = session.req_header().headers.get("host")?.to_str().ok()?;
h.split_once(':').map_or(h, |(host, _)| host) h.split_once(':').map_or(h, |(host, _)| host)
}; };
ump_upst.get(host_str).map(|entry| entry.key().clone())
ump_upst.get(host_str).or_else(|| ump_upst.get("DEFAULT")).map(|entry| entry.key().clone())
} }

View File

@@ -15,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());
@@ -60,47 +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();
let certs_path = cfg.proxy_configs.clone().unwrap() + "/certificates";
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(); if fs::metadata(certs_path.clone()).is_err() {
let new_certs = load::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");
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());
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

@@ -15,7 +15,7 @@ use axum::{Json, Router};
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::Serialize; use serde::Serialize;
use std::collections::HashMap; use std::collections::HashMap;
@@ -35,6 +35,7 @@ struct AppState {
master_key: String, master_key: String,
cert_creds: String, cert_creds: String,
certs_dir: 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>, current_upstreams: Arc<UpstreamsDashMap>,
@@ -48,8 +49,9 @@ pub async fn run_server(config: &APIUpstreamProvider, mut to_return: Sender<Conf
master_key: config.masterkey.clone(), master_key: config.masterkey.clone(),
cert_creds: credsfile, cert_creds: credsfile,
certs_dir: config.certs_dir.clone(), 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, current_upstreams: upstreams_curr,
full_upstreams: upstreams_full, full_upstreams: upstreams_full,
}; };
@@ -85,7 +87,7 @@ pub async fn run_server(config: &APIUpstreamProvider, mut to_return: Sender<Conf
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();
@@ -98,12 +100,18 @@ async fn conf(State(st): State<AppState>, Query(params): Query<HashMap<String, S
return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Config API is disabled !\n")).unwrap(); return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Config 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) = headers.get("x-api-key").and_then(|v| v.to_str().ok()).or(params.get("key").map(|s| s.as_str())) {
if key_authorization(&headers, &params, &st.master_key) { if key_authorization(&headers, &params, &st.master_key) {
let strcontent = content.as_str(); let strcontent = content.as_str();
let parsed = serde_yml::from_str::<Config>(strcontent); let parsed = serde_yml::from_str::<Config>(strcontent);
match parsed { match parsed {
Ok(_) => { Ok(_) => {
let _ = tokio::spawn(async move { apply_config(content.as_str(), st).await }); if let Some(_) = params.get("save") {
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(); return Response::builder().status(StatusCode::OK).body(Body::from("Accepted! Applying in background\n")).unwrap();
} }
Err(err) => { Err(err) => {
@@ -115,9 +123,15 @@ async fn conf(State(st): State<AppState>, Query(params): Query<HashMap<String, S
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 apply_config(content: &str, mut st: AppState) { async fn apply_config(content: &str, mut st: AppState, save: bool) {
let sl = crate::utils::parceyaml::load_configuration(content, "content").await; let sl = crate::utils::parceyaml::load_configuration(content, "content").await;
if let Some(serverlist) = sl.0 { 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; let _ = st.config_sender.send(serverlist).await;
} }
} }
@@ -136,7 +150,7 @@ async fn jwt_gen(State(state): State<AppState>, Json(payload): Json<Claims>) ->
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.token); debug!("Generating token: {:?}", tok.token);
(StatusCode::CREATED, Json(tok)) (StatusCode::CREATED, Json(tok))
} }
Err(e) => { Err(e) => {
@@ -172,8 +186,9 @@ async fn metrics() -> impl IntoResponse {
.unwrap() .unwrap()
} }
#[allow(clippy::needless_return)]
async fn status(State(st): State<AppState>, Query(params): Query<HashMap<String, String>>) -> impl IntoResponse { async fn status(State(st): State<AppState>, Query(params): Query<HashMap<String, String>>) -> impl IntoResponse {
if let Some(_) = params.get("live") { if params.contains_key("live") {
let r = upstreams_liveness_json(&st.full_upstreams, &st.current_upstreams); let r = upstreams_liveness_json(&st.full_upstreams, &st.current_upstreams);
return Response::builder() return Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
@@ -181,7 +196,7 @@ async fn status(State(st): State<AppState>, Query(params): Query<HashMap<String,
.body(Body::from(format!("{}", r))) .body(Body::from(format!("{}", r)))
.unwrap(); .unwrap();
} }
if let Some(_) = params.get("all") { if params.contains_key("all") {
let resp = upstreams_to_json(&st.current_upstreams); let resp = upstreams_to_json(&st.current_upstreams);
match resp { match resp {
Ok(j) => { Ok(j) => {
@@ -201,16 +216,17 @@ async fn status(State(st): State<AppState>, Query(params): Query<HashMap<String,
} }
Response::builder() Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR) .status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("{}", "Parameter mismatch"))) .body(Body::from("Parameter mismatch"))
.unwrap() .unwrap()
} }
#[allow(clippy::needless_return)]
async fn acme_create(State(state): State<AppState>, Query(params): Query<HashMap<String, String>>, headers: HeaderMap) -> impl IntoResponse { 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) { if !key_authorization(&headers, &params, &state.master_key) {
return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Access Denied !\n")).unwrap(); return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Access Denied !\n")).unwrap();
} }
let _ = match account::load_or_create(state.cert_creds.as_str()).await { match account::load_or_create(state.cert_creds.as_str()).await {
Ok(txt) => { Ok(txt) => {
return Response::builder() return Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
@@ -226,6 +242,7 @@ async fn acme_create(State(state): State<AppState>, Query(params): Query<HashMap
} }
}; };
} }
#[allow(clippy::needless_return)]
async fn acme_order( async fn acme_order(
State(state): State<AppState>, State(state): State<AppState>,
axum::extract::Path(domain): axum::extract::Path<String>, axum::extract::Path(domain): axum::extract::Path<String>,
@@ -237,7 +254,7 @@ async fn acme_order(
} }
let domain_clean = domain.trim_matches('/'); let domain_clean = domain.trim_matches('/');
let _ = match order::order(domain_clean, state.cert_creds.as_str(), state.certs_dir).await { match order::order(domain_clean, state.cert_creds.as_str(), state.certs_dir).await {
Ok(txt) => { Ok(txt) => {
return Response::builder() return Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
@@ -283,3 +300,5 @@ fn key_authorization(headers: &HeaderMap, params: &HashMap<String, String>, mast
} }
false false
} }
// -- ⚝ by Dave -- in NeoVim ⚝ --